diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 022da0a2ea..03fd21829d 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -15,6 +15,8 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
+M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
+M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
diff --git a/osu.Android.props b/osu.Android.props
index 2a678f1c61..f251e8ee71 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFlashlight.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFlashlight.cs
new file mode 100644
index 0000000000..538fc7fac6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFlashlight.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 NUnit.Framework;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ public class TestSceneCatchModFlashlight : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.25f)]
+ [TestCase(1.5f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
index 7f513728af..f3161f32be 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -1,10 +1,10 @@
// 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.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
@@ -12,6 +12,8 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
@@ -19,15 +21,28 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneComboCounter : CatchSkinnableTestScene
{
- private ScoreProcessor scoreProcessor;
+ private ScoreProcessor scoreProcessor = null!;
private Color4 judgedObjectColour = Color4.White;
+ private readonly Bindable showHud = new Bindable(true);
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.CacheAs(new TestPlayer
+ {
+ ShowingOverlayComponents = { BindTarget = showHud },
+ });
+ }
+
[SetUp]
public void SetUp() => Schedule(() =>
{
scoreProcessor = new ScoreProcessor(new CatchRuleset());
+ showHud.Value = true;
+
SetContents(_ => new CatchComboDisplay
{
Anchor = Anchor.Centre,
@@ -51,9 +66,15 @@ namespace osu.Game.Rulesets.Catch.Tests
1f
);
});
+
+ AddStep("set hud to never show", () => showHud.Value = false);
+ AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5);
+
+ AddStep("set hud to show", () => showHud.Value = true);
+ AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5);
}
- private void performJudgement(HitResult type, Judgement judgement = null)
+ private void performJudgement(HitResult type, Judgement? judgement = null)
{
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
index 58f493b4b8..a0a11424d0 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -36,5 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return base.CreateHitObjectBlueprintFor(hitObject);
}
+
+ protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs
new file mode 100644
index 0000000000..0a0f91c781
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs
@@ -0,0 +1,49 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchEditorPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
+ {
+ protected override Container Content => content;
+ private readonly Container content;
+
+ public CatchEditorPlayfieldAdjustmentContainer()
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ Size = new Vector2(0.8f, 0.9f);
+
+ InternalChild = new ScalingContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Child = content = new Container { RelativeSizeAxes = Axes.Both },
+ };
+ }
+
+ private class ScalingContainer : Container
+ {
+ public ScalingContainer()
+ {
+ RelativeSizeAxes = Axes.Y;
+ Width = CatchPlayfield.WIDTH;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT));
+ Height = 1 / Scale.Y;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index f31dc3ef9c..54d50b01c4 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -12,8 +12,10 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
@@ -37,6 +39,12 @@ namespace osu.Game.Rulesets.Catch.Edit
private InputManager inputManager;
+ private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
+ {
+ MinValue = 1,
+ MaxValue = 10,
+ };
+
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
@@ -51,7 +59,10 @@ namespace osu.Game.Rulesets.Catch.Edit
LayerBelowRuleset.Add(new PlayfieldBorder
{
- RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = CatchPlayfield.HEIGHT,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
@@ -77,8 +88,30 @@ namespace osu.Game.Rulesets.Catch.Edit
updateDistanceSnapGrid();
}
+ public override bool OnPressed(KeyBindingPressEvent e)
+ {
+ switch (e.Action)
+ {
+ // Note that right now these are hard to use as the default key bindings conflict with existing editor key bindings.
+ // In the future we will want to expose this via UI and potentially change the key bindings to be editor-specific.
+ // May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts.
+ case GlobalAction.IncreaseScrollSpeed:
+ this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint);
+ break;
+
+ case GlobalAction.DecreaseScrollSpeed:
+ this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint);
+ break;
+ }
+
+ return base.OnPressed(e);
+ }
+
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
- new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
+ new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
+ {
+ TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
+ };
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
index c81afafae5..67238f66d4 100644
--- a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@@ -13,11 +14,24 @@ namespace osu.Game.Rulesets.Catch.Edit
{
public class DrawableCatchEditorRuleset : DrawableCatchRuleset
{
+ public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
+
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
: base(ruleset, beatmap, mods)
{
}
+ protected override void Update()
+ {
+ base.Update();
+
+ double gamePlayTimeRange = GetTimeRange(Beatmap.Difficulty.ApproachRate);
+ float playfieldStretch = Playfield.DrawHeight / CatchPlayfield.HEIGHT;
+ TimeRange.Value = gamePlayTimeRange * TimeRangeMultiplier.Value * playfieldStretch;
+ }
+
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
+
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index ff957b9b73..a9e9e8fbd5 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
this.playfield = playfield;
- FlashlightSize = new Vector2(0, GetSizeFor(0));
+ FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f;
}
@@ -66,9 +66,9 @@ namespace osu.Game.Rulesets.Catch.Mods
FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 69ae8328e9..3f7560844c 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -19,17 +19,20 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override LocalisableString Description => @"Use the mouse to control the catcher.";
- private DrawableRuleset drawableRuleset = null!;
+ private DrawableCatchRuleset drawableRuleset = null!;
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- this.drawableRuleset = drawableRuleset;
+ this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
}
public void ApplyToPlayer(Player player)
{
if (!drawableRuleset.HasReplayLoaded.Value)
- drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
+ {
+ var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
+ catchPlayfield.CatcherArea.Add(new MouseInputHelper(catchPlayfield.CatcherArea));
+ }
}
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
@@ -38,9 +41,10 @@ namespace osu.Game.Rulesets.Catch.Mods
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- public MouseInputHelper(CatchPlayfield playfield)
+ public MouseInputHelper(CatcherArea catcherArea)
{
- catcherArea = playfield.CatcherArea;
+ this.catcherArea = catcherArea;
+
RelativeSizeAxes = Axes.Both;
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index c06d9f520f..a73b34c9b6 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -13,6 +13,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class CatchLegacySkinTransformer : LegacySkinTransformer
{
+ public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasPear;
+
+ private bool hasPear => GetTexture("fruit-pear") != null;
+
///
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
///
@@ -49,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
- if (GetTexture("fruit-pear") != null)
+ if (hasPear)
return new LegacyFruitPiece();
return null;
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
index b2dd29841b..b4d29988d9 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.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.Game.Rulesets.Catch.UI;
diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
index e9c289e46a..a5b7d8d0af 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
@@ -1,12 +1,13 @@
// 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 JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -19,14 +20,29 @@ namespace osu.Game.Rulesets.Catch.UI
{
private int currentCombo;
- [CanBeNull]
- public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
+ public ICatchComboCounter? ComboCounter => Drawable as ICatchComboCounter;
+
+ private readonly IBindable showCombo = new BindableBool(true);
public CatchComboDisplay()
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
{
}
+ [Resolved(canBeNull: true)]
+ private Player? player { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (player != null)
+ {
+ showCombo.BindTo(player.ShowingOverlayComponents);
+ showCombo.BindValueChanged(s => this.FadeTo(s.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
+ }
+ }
+
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index dad22fbe69..ce000b0fad 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const float WIDTH = 512;
+ ///
+ /// The height of the playfield.
+ /// This doesn't include the catcher area.
+ ///
+ public const float HEIGHT = 384;
+
///
/// The center position of the playfield.
///
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index 27f7886d79..e02b915508 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
- TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
+ TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
}
[BackgroundDependencyLoader]
@@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
KeyBindingInputManager.Add(new CatchTouchInputMapper());
}
+ protected double GetTimeRange(float approachRate) => IBeatmapDifficultyInfo.DifficultyRange(approachRate, 1800, 1200, 450);
+
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
index eb380c07a6..e456659ac4 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
@@ -3,9 +3,11 @@
#nullable disable
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Game.Input.Bindings;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
@@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
}
- protected override void ReloadMappings()
+ protected override void ReloadMappings(IQueryable realmKeyBindings)
{
KeyBindings = DefaultKeyBindings;
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFlashlight.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFlashlight.cs
new file mode 100644
index 0000000000..0e222fea89
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFlashlight.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 NUnit.Framework;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public class TestSceneManiaModFlashlight : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.5f)]
+ [TestCase(3f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index a925e7c0ac..440dec82af 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- scoreAccuracy = customAccuracy;
+ scoreAccuracy = calculateCustomAccuracy();
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
@@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
///
/// Accuracy used to weight judgements independently from the score's actual accuracy.
///
- private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
+ private double calculateCustomAccuracy()
+ {
+ if (totalHits == 0)
+ return 0;
+
+ return (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
index ad75afff8e..f438d6497c 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -33,5 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit
}
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
+
+ protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 6eaede2112..947915cdf9 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public ManiaFlashlight(ManiaModFlashlight modFlashlight)
: base(modFlashlight)
{
- FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0));
+ FlashlightSize = new Vector2(DrawWidth, GetSize());
AddLayout(flashlightProperties);
}
@@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Mania.Mods
}
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "RectangularFlashlight";
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 19792086a7..48647f9f5f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -4,12 +4,14 @@
#nullable disable
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.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Audio;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private Container tailContainer;
private Container tickContainer;
+ private PausableSkinnableSound slidingSample;
+
///
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
///
@@ -108,6 +112,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
},
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ slidingSample = new PausableSkinnableSound { Looping = true }
});
maskedContents.AddRange(new[]
@@ -118,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ isHitting.BindValueChanged(updateSlidingSample, true);
+ }
+
protected override void OnApply()
{
base.OnApply();
@@ -322,5 +334,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
HoldStartTime = null;
isHitting.Value = false;
}
+
+ protected override void LoadSamples()
+ {
+ // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
+
+ if (HitObject.SampleControlPoint == null)
+ {
+ throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
+ }
+
+ slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ }
+
+ public override void StopAllSamples()
+ {
+ base.StopAllSamples();
+ slidingSample?.Stop();
+ }
+
+ private void updateSlidingSample(ValueChangedEvent tracking)
+ {
+ if (tracking.NewValue)
+ slidingSample?.Play();
+ else
+ slidingSample?.Stop();
+ }
+
+ protected override void OnFree()
+ {
+ slidingSample.Samples = null;
+ base.OnFree();
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
index 27926c11eb..ae313e0b91 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -1,6 +1,7 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
@@ -89,13 +90,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
Color4 colour;
+ const int total_colours = 7;
+
if (stage.IsSpecialColumn(column))
colour = new Color4(159, 101, 255, 255);
else
{
- switch (column % 8)
+ switch (column % total_colours)
{
- default:
+ case 0:
colour = new Color4(240, 216, 0, 255);
break;
@@ -112,20 +115,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
break;
case 4:
- colour = new Color4(178, 0, 240, 255);
- break;
-
- case 5:
colour = new Color4(0, 96, 240, 255);
break;
- case 6:
+ case 5:
colour = new Color4(0, 226, 240, 255);
break;
- case 7:
+ case 6:
colour = new Color4(0, 240, 96, 255);
break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 1d39721a2b..a07dbea368 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class ManiaLegacySkinTransformer : LegacySkinTransformer
{
+ public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasKeyTexture.Value;
+
///
/// Mapping of to their corresponding
/// value.
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index c102678e00..dc74d38cdc 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
[Cached]
private readonly EditorClock editorClock;
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
index 51871dd9e5..0601dc6068 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
@@ -148,6 +148,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
}
+ [Test]
+ public void TestFloatEdgeCaseConversion()
+ {
+ Slider slider = null;
+
+ AddStep("select first slider", () =>
+ {
+ slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
+ EditorClock.Seek(slider.StartTime);
+ EditorBeatmap.SelectedHitObjects.Add(slider);
+ });
+
+ AddStep("change to these specific circumstances", () =>
+ {
+ EditorBeatmap.Difficulty.SliderMultiplier = 1;
+ var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(slider.StartTime);
+ timingPoint.BeatLength = 352.941176470588;
+ slider.Path.ControlPoints[^1].Position = new Vector2(-110, 16);
+ slider.Path.ExpectedDistance.Value = 100;
+ });
+
+ convertToStream();
+
+ AddAssert("stream created", () => streamCreatedFor(slider,
+ (time: 0, pathPosition: 0),
+ (time: 0.25, pathPosition: 0.25),
+ (time: 0.5, pathPosition: 0.5),
+ (time: 0.75, pathPosition: 0.75),
+ (time: 1, pathPosition: 1)));
+ }
+
private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles)
{
if (EditorBeatmap.HitObjects.Contains(slider))
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
new file mode 100644
index 0000000000..704a548c61
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModFlashlight : OsuModTestScene
+ {
+ [TestCase(600)]
+ [TestCase(120)]
+ [TestCase(1200)]
+ public void TestFollowDelay(double followDelay) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { FollowDelay = { Value = followDelay } }, PassCondition = () => true });
+
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.5f)]
+ [TestCase(2f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
index 44404ca245..da6fac3269 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
@@ -145,6 +147,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private bool isBreak() => Player.IsBreakTime.Value;
- private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha, 0.1f);
+ private OsuPlayfield playfield => (OsuPlayfield)Player.DrawableRuleset.Playfield;
+
+ private bool cursorAlphaAlmostEquals(float alpha) =>
+ Precision.AlmostEquals(playfield.Cursor.AsNonNull().Alpha, alpha, 0.1f) &&
+ Precision.AlmostEquals(playfield.Smoke.Alpha, alpha, 0.1f);
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
deleted file mode 100644
index e0d1646cb0..0000000000
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// 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.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Tests.Visual;
-
-namespace osu.Game.Rulesets.Osu.Tests
-{
- public class TestSceneOsuFlashlight : TestSceneOsuPlayer
- {
- protected override TestPlayer CreatePlayer(Ruleset ruleset)
- {
- SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
-
- return base.CreatePlayer(ruleset);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index 0169627867..728aa27da2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -68,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () =>
{
- var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
- tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
-
- var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
+ var skin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
+ var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider)
{
@@ -92,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
- AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White);
+ AddAssert("ball is white", () => dho.ChildrenOfType().Single().BallColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
- AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red);
+ AddAssert("ball is red", () => dho.ChildrenOfType().Single().BallColour == Color4.Red);
}
private Slider prepareObject(Slider slider)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 7c289b5b05..265a1d21b1 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
// every second span is in the reverse direction - need to reverse the path position.
- if (Precision.AlmostBigger(positionWithRepeats % 2, 1))
+ if (positionWithRepeats % 2 >= 1)
pathPosition = 1 - pathPosition;
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 60896b17bf..6b4a6e39d9 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader]
private void load()
{
+ // Give a bit of breathing room around the playfield content.
+ PlayfieldContentContainer.Padding = new MarginPadding(10);
+
LayerBelowRuleset.AddRange(new Drawable[]
{
distanceSnapGridContainer = new Container
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 66f367c79b..1a86901d9c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
followDelay = modFlashlight.FollowDelay.Value;
- FlashlightSize = new Vector2(0, GetSizeFor(0));
+ FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f;
}
@@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Osu.Mods
return base.OnMouseMove(e);
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index fbde9e0491..38d90eb121 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Timing;
@@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield)
{
- var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+ var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
index 2f84c30581..d1bbae8e1a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
@@ -9,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Utils;
@@ -33,9 +35,15 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield)
{
- bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
+ var osuPlayfield = (OsuPlayfield)playfield;
+ Debug.Assert(osuPlayfield.Cursor != null);
+
+ bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(osuPlayfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha;
- playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
+ float currentAlpha = (float)Interpolation.Lerp(osuPlayfield.Cursor.Alpha, targetAlpha, Math.Clamp(osuPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
+
+ osuPlayfield.Cursor.Alpha = currentAlpha;
+ osuPlayfield.Smoke.Alpha = currentAlpha;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index fac1cbfd47..753de6231a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
+
+ public override Type[] IncompatibleMods =>
+ base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
@@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
return;
}
- osuInputManager.AllowUserPresses = false;
+ osuInputManager.AllowGameplayInputs = false;
}
public void Update(Playfield playfield)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
index 911363a27e..31a6b69d6b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils;
@@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield)
{
- var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+ var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index d58a435728..785d15c15b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -14,12 +14,10 @@ using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -106,7 +104,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
- updateBallTint();
}, true);
Tracking.BindValueChanged(updateSlidingSample);
@@ -257,22 +254,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
SliderBody?.RecyclePath();
}
- protected override void ApplySkin(ISkinSource skin, bool allowFallback)
- {
- base.ApplySkin(skin, allowFallback);
-
- updateBallTint();
- }
-
- private void updateBallTint()
- {
- if (CurrentSkin == null)
- return;
-
- bool allowBallTint = CurrentSkin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
- Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
- }
-
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered || Time.Current < HitObject.EndTime)
@@ -331,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- const float fade_out_time = 450;
+ const float fade_out_time = 240;
switch (state)
{
@@ -341,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
- this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
+ this.FadeOut(fade_out_time).Expire();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index 6bfb4e8aae..de6ca7dd38 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -11,28 +11,20 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
+ public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
{
public const float FOLLOW_AREA = 2.4f;
public Func GetInitialHitAction;
- public Color4 AccentColour
- {
- get => ball.Colour;
- set => ball.Colour = value;
- }
-
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
@@ -186,17 +178,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Vector2? lastPosition;
+ private bool rewinding;
+
public void UpdateProgress(double completionProgress)
{
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
+ if (Clock.ElapsedFrameTime != 0)
+ rewinding = Clock.ElapsedFrameTime < 0;
+
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
if (diff.LengthFast < 0.01f)
return;
- ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
+ ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0);
lastPosition = Position;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 53f4d21975..6ae9d5bc34 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public readonly IBindable SpinsPerMinute = new BindableDouble();
- private const double fade_out_duration = 160;
+ private const double fade_out_duration = 240;
public DrawableSpinner()
: this(null)
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index e3c1b1e168..6c2be8a49a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -34,21 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override IList AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
- public IList CreateSlidingSamples()
- {
- var slidingSamples = new List();
-
- var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
- if (normalSample != null)
- slidingSamples.Add(normalSample.With("sliderslide"));
-
- var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
- if (whistleSample != null)
- slidingSamples.Add(whistleSample.With("sliderwhistle"));
-
- return slidingSamples;
- }
-
private readonly Cached endPositionCache = new Cached();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index dec965e567..1e59e19246 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -5,10 +5,12 @@
using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
+using osu.Game.Input.Bindings;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@@ -17,9 +19,16 @@ namespace osu.Game.Rulesets.Osu
{
public IEnumerable PressedActions => KeyBindingContainer.PressedActions;
- public bool AllowUserPresses
+ ///
+ /// Whether gameplay input buttons should be allowed.
+ /// Defaults to true, generally used for mods like Relax which turn off main inputs.
+ ///
+ ///
+ /// Of note, auxiliary inputs like the "smoke" key are left usable.
+ ///
+ public bool AllowGameplayInputs
{
- set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value;
+ set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value;
}
///
@@ -58,18 +67,36 @@ namespace osu.Game.Rulesets.Osu
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
- public bool AllowUserPresses = true;
+ private bool allowGameplayInputs = true;
+
+ ///
+ /// Whether gameplay input buttons should be allowed.
+ /// Defaults to true, generally used for mods like Relax which turn off main inputs.
+ ///
+ ///
+ /// Of note, auxiliary inputs like the "smoke" key are left usable.
+ ///
+ public bool AllowGameplayInputs
+ {
+ get => allowGameplayInputs;
+ set
+ {
+ allowGameplayInputs = value;
+ ReloadMappings();
+ }
+ }
public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
{
}
- protected override bool Handle(UIEvent e)
+ protected override void ReloadMappings(IQueryable realmKeyBindings)
{
- if (!AllowUserPresses) return false;
+ base.ReloadMappings(realmKeyBindings);
- return base.Handle(e);
+ if (!AllowGameplayInputs)
+ KeyBindings = KeyBindings.Where(b => b.GetAction() == OsuAction.Smoke).ToList();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
index b08b7b4e85..bb68c7298f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
default:
JudgementText
+ .FadeInFromZero(300, Easing.OutQuint)
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break;
@@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
ringExplosion?.PlayAnimation();
}
- public Drawable? GetAboveHitObjectsProxiedContent() => null;
+ public Drawable? GetAboveHitObjectsProxiedContent() => JudgementText.CreateProxy();
private class RingExplosion : CompositeDrawable
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
index ffdcba3cdb..36dc8c801d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -108,18 +108,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
base.LoadComplete();
- accentColour.BindValueChanged(colour =>
- {
- outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
- outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
- innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
- flash.Colour = colour.NewValue;
- }, true);
-
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
+ accentColour.BindValueChanged(colour =>
+ {
+ // A colour transform is applied.
+ // Without removing transforms first, when it is rewound it may apply an old colour.
+ outerGradient.ClearTransforms(targetMember: nameof(Colour));
+ outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
+
+ outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
+ innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
+ flash.Colour = colour.NewValue;
+
+ updateStateTransforms(drawableObject, drawableObject.State.Value);
+ }, true);
+
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
- updateStateTransforms(drawableObject, drawableObject.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
@@ -173,11 +178,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
.FadeOut(flash_in_duration);
}
- // The flash layer starts white to give the wanted brightness, but is almost immediately
- // recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
- // but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
- flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index 1b2ab82044..a6e62b83e4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new[]
{
- CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
+ CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
+ Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -134,10 +134,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (state)
{
case ArmedState.Hit:
- CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
+ CircleSprite.FadeOut(legacy_fade_duration);
CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
+ OverlaySprite.FadeOut(legacy_fade_duration);
OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
@@ -146,11 +146,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
- hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ hitCircleText.FadeOut(legacy_fade_duration / 4);
else
{
// old skins scale and fade it normally along other pieces.
- hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.FadeOut(legacy_fade_duration);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index 22944becf3..71c3e4c9f0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
- this.FadeInFromZero(spinner.TimeFadeIn / 2);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
+ this.FadeInFromZero(spinner.TimeFadeIn);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
index 414879f42d..60d71ae843 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
+ public Color4 BallColour => animationContent.Colour;
+
private Sprite layerNd = null!;
private Sprite layerSpec = null!;
@@ -61,6 +64,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
}
+ private readonly IBindable accentColour = new Bindable();
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -69,6 +74,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
+
+ if (skin.GetConfig(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
+ {
+ accentColour.BindTo(parentObject.AccentColour);
+ accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 004222ad7a..a817e5f2b7 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
spin = new Sprite
{
+ Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-spin"),
@@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
},
bonusCounter = new LegacySpriteText(LegacyFont.Score)
{
- Alpha = 0f,
+ Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Scale = new Vector2(SPRITE_SCALE),
@@ -179,6 +180,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
}
+ using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn / 2))
+ spin.FadeInFromZero(d.HitObject.TimeFadeIn / 2);
+
using (BeginAbsoluteSequence(d.HitObject.StartTime))
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index b778bc21d1..856ccb5044 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class OsuLegacySkinTransformer : LegacySkinTransformer
{
+ public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle.Value;
+
private readonly Lazy hasHitCircle;
///
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 306a1e38b9..1c0a62454b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
SliderBorderSize,
SliderPathRadius,
- AllowSliderBallTint,
CursorCentre,
CursorExpand,
CursorRotate,
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
index 6c998e244c..46c8e7c02a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -21,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
{
- private const int max_point_count = 18_000;
-
// fade anim values
private const double initial_fade_out_duration = 4000;
@@ -84,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
totalDistance = pointInterval;
}
- private Vector2 nextPointDirection()
- {
- float angle = RNG.NextSingle(0, 2 * MathF.PI);
- return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
- }
-
public void AddPosition(Vector2 position, double time)
{
lastPosition ??= position;
@@ -106,33 +100,27 @@ namespace osu.Game.Rulesets.Osu.Skinning
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
increment *= pointInterval;
- if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
- {
- int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
- SmokePoints.RemoveRange(index, SmokePoints.Count - index);
- }
-
totalDistance %= pointInterval;
- for (int i = 0; i < count; i++)
+ if (SmokePoints.Count == 0 || SmokePoints[^1].Time <= time)
{
- SmokePoints.Add(new SmokePoint
+ for (int i = 0; i < count; i++)
{
- Position = pointPos,
- Time = time,
- Direction = nextPointDirection(),
- });
+ SmokePoints.Add(new SmokePoint
+ {
+ Position = pointPos,
+ Time = time,
+ Angle = RNG.NextSingle(0, 2 * MathF.PI),
+ });
- pointPos += increment;
+ pointPos += increment;
+ }
}
Invalidate(Invalidation.DrawNode);
}
lastPosition = position;
-
- if (SmokePoints.Count >= max_point_count)
- FinishDrawing(time);
}
public void FinishDrawing(double time)
@@ -156,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public Vector2 Position;
public double Time;
- public Vector2 Direction;
+ public float Angle;
public struct UpperBoundComparer : IComparer
{
@@ -170,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
return x.Time > target.Time ? 1 : -1;
}
}
+
+ public struct LowerBoundComparer : IComparer
+ {
+ public int Compare(SmokePoint x, SmokePoint target)
+ {
+ // Similar logic as UpperBoundComparer, except returned index will always be
+ // the first element larger or equal
+
+ return x.Time < target.Time ? -1 : 1;
+ }
+ }
}
protected class SmokeDrawNode : TexturedShaderDrawNode
@@ -185,17 +184,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float radius;
private Vector2 drawSize;
private Texture? texture;
+ private int rotationSeed;
+ private int firstVisiblePointIndex;
// anim calculation vars (color, scale, direction)
private double initialFadeOutDurationTrunc;
- private double firstVisiblePointTime;
+ private double firstVisiblePointTimeAfterSmokeEnded;
private double initialFadeOutTime;
private double reFadeInTime;
private double finalFadeOutTime;
- private Random rotationRNG = new Random();
-
public SmokeDrawNode(ITexturedShaderDrawable source)
: base(source)
{
@@ -205,9 +204,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.ApplyState();
- points.Clear();
- points.AddRange(Source.SmokePoints);
-
radius = Source.radius;
drawSize = Source.DrawSize;
texture = Source.Texture;
@@ -216,14 +212,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
SmokeEndTime = Source.smokeEndTime;
CurrentTime = Source.Clock.CurrentTime;
- rotationRNG = new Random(Source.rotationSeed);
+ rotationSeed = Source.rotationSeed;
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
- firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
+ firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc;
- initialFadeOutTime = CurrentTime;
- reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
- finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
+ initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
+ reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_speed);
+ finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / final_fade_out_speed);
+
+ double firstVisiblePointTime = Math.Min(SmokeEndTime, CurrentTime) - initialFadeOutDurationTrunc;
+ firstVisiblePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = firstVisiblePointTime }, new SmokePoint.LowerBoundComparer());
+ int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
+
+ points.Clear();
+ points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
}
public sealed override void Draw(IRenderer renderer)
@@ -233,7 +236,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (points.Count == 0)
return;
- quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10);
+ quadBatch ??= renderer.CreateQuadBatch(200, 4);
+
+ if (points.Count > quadBatch.Size && quadBatch.Size != IRenderer.MAX_QUADS)
+ {
+ int batchSize = Math.Min(quadBatch.Size * 2, IRenderer.MAX_QUADS);
+ quadBatch = renderer.CreateQuadBatch(batchSize, 4);
+ }
+
texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect();
@@ -245,8 +255,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
shader.Bind();
texture.Bind();
- foreach (var point in points)
- drawPointQuad(point, textureRect);
+ for (int i = 0; i < points.Count; i++)
+ drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
shader.Unbind();
renderer.PopLocalMatrix();
@@ -260,30 +270,34 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var color = Color4.White;
- double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
+ double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
- if (timeDoingInitialFadeOut > 0)
+ if (timeDoingFinalFadeOut > 0 && point.Time >= firstVisiblePointTimeAfterSmokeEnded)
{
- float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
- color.A = (1 - fraction) * initial_alpha;
+ float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
+ fraction = MathF.Pow(fraction, 5);
+ color.A = (1 - fraction) * re_fade_in_alpha;
}
-
- if (color.A > 0)
+ else
{
- double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
- double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
+ double timeDoingInitialFadeOut = initialFadeOutTime - point.Time;
- if (timeDoingFinalFadeOut > 0)
+ if (timeDoingInitialFadeOut > 0)
{
- float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
- fraction = MathF.Pow(fraction, 5);
- color.A = (1 - fraction) * re_fade_in_alpha;
+ float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
+ color.A = (1 - fraction) * initial_alpha;
}
- else if (timeDoingReFadeIn > 0)
+
+ if (point.Time > firstVisiblePointTimeAfterSmokeEnded)
{
- float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
- fraction = 1 - MathF.Pow(1 - fraction, 5);
- color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
+
+ if (timeDoingReFadeIn > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ }
}
}
@@ -298,33 +312,33 @@ namespace osu.Game.Rulesets.Osu.Skinning
return fraction * (final_scale - initial_scale) + initial_scale;
}
- protected virtual Vector2 PointDirection(SmokePoint point)
+ protected virtual Vector2 PointDirection(SmokePoint point, int index)
{
- float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
- float finalAngle = initialAngle + nextRotation();
-
double timeDoingRotation = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
- float angle = fraction * (finalAngle - initialAngle) + initialAngle;
+ float angle = fraction * getRotation(index) + point.Angle;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
- private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
+ private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
- private void drawPointQuad(SmokePoint point, RectangleF textureRect)
+ private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
{
Debug.Assert(quadBatch != null);
var colour = PointColour(point);
- float scale = PointScale(point);
- var dir = PointDirection(point);
- var ortho = dir.PerpendicularLeft;
-
- if (colour.A == 0 || scale == 0)
+ if (colour.A == 0)
return;
+ float scale = PointScale(point);
+ if (scale == 0)
+ return;
+
+ var dir = PointDirection(point, index);
+ var ortho = dir.PerpendicularLeft;
+
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 2e67e91460..e9a6c84c0b 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
+ public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
- new SmokeContainer { RelativeSizeAxes = Axes.Both },
+ Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs
new file mode 100644
index 0000000000..417b59f5d2
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.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 NUnit.Framework;
+using osu.Game.Rulesets.Taiko.Mods;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Mods
+{
+ public class TestSceneTaikoModFlashlight : TaikoModTestScene
+ {
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.25f)]
+ [TestCase(1.5f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
index 2d27e0e40e..e42dc254ac 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
@@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 },
};
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void DrumrollTest()
{
AddStep("Drum roll", () => SetContents(_ =>
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
new file mode 100644
index 0000000000..53977150e7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ // track needs to be playing for BeatSyncedContainer to work.
+ Beatmap.Value.Track.Start();
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index d5a97f8f88..8e9c487c2f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -4,7 +4,6 @@
#nullable disable
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -16,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestHits()
{
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
new file mode 100644
index 0000000000..fac0530749
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableHitKiai : TestSceneDrawableHit
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ // track needs to be playing for BeatSyncedContainer to work.
+ Beatmap.Value.Track.Start();
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
new file mode 100644
index 0000000000..095fddc33f
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
@@ -0,0 +1,87 @@
+// 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.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneBarLineGeneration : OsuTestScene
+ {
+ [Test]
+ public void TestCloseBarLineGeneration()
+ {
+ const double start_time = 1000;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new Hit
+ {
+ Type = HitType.Centre,
+ StartTime = start_time
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new TaikoRuleset().RulesetInfo
+ },
+ };
+
+ beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint());
+ beatmap.ControlPointInfo.Add(start_time + 1, new TimingControlPoint());
+
+ var barlines = new BarLineGenerator(beatmap).BarLines;
+
+ AddAssert("first barline generated", () => barlines.Any(b => b.StartTime == start_time));
+ AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + 1));
+ }
+
+ [Test]
+ public void TestOmitBarLineEffectPoint()
+ {
+ const double start_time = 1000;
+ const double beat_length = 500;
+
+ const int time_signature_numerator = 4;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new Hit
+ {
+ Type = HitType.Centre,
+ StartTime = start_time
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new TaikoRuleset().RulesetInfo
+ },
+ };
+
+ beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint
+ {
+ BeatLength = beat_length,
+ TimeSignature = new TimeSignature(time_signature_numerator)
+ });
+
+ beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true });
+
+ var barlines = new BarLineGenerator(beatmap).BarLines;
+
+ AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time));
+ AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + (beat_length * time_signature_numerator)));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index df1450bf77..863a2c9eac 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (e.Button)
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index 23a005190a..70364cabf1 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private double originalStartTime;
private Vector2 originalPosition;
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left)
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
index 1c1a5c325f..161799c980 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.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.Collections.Generic;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index fca69e86cc..98f954ad29 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -47,21 +47,21 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
this.taikoPlayfield = taikoPlayfield;
- FlashlightSize = getSizeFor(0);
+ FlashlightSize = adjustSize(GetSize());
FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties);
}
- private Vector2 getSizeFor(int combo)
+ private Vector2 adjustSize(float size)
{
// Preserve flashlight size through the playfield's aspect adjustment.
- return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
+ return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), getSizeFor(e.NewValue), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), adjustSize(size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize));
- FlashlightSize = getSizeFor(Combo.Value);
+ FlashlightSize = adjustSize(Combo.Value);
flashlightProperties.Validate();
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index a7ab1bcd4a..6b5a9ae6d2 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,6 +14,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
@@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80;
+ private const float flash_opacity = 0.3f;
+
private Color4 accentColour;
///
@@ -152,11 +156,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
};
}
+ [Resolved]
+ private DrawableHitObject drawableHitObject { get; set; }
+
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
+ if (drawableHitObject.State.Value == ArmedState.Idle)
+ {
+ FlashBox
+ .FadeTo(flash_opacity)
+ .Then()
+ .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
+ }
+
if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
return;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
index 399bd9260d..6bbeb0ed4c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
- AddInternal(backgroundLayer = getDrawableFor("circle"));
+ AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 992316ca53..020cdab4dc 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -14,8 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class TaikoLegacySkinTransformer : LegacySkinTransformer
{
+ public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle || hasBarLeft;
+
private readonly Lazy hasExplosion;
+ private bool hasHitCircle => GetTexture("taikohitcircle") != null;
+ private bool hasBarLeft => GetTexture("taiko-bar-left") != null;
+
public TaikoLegacySkinTransformer(ISkin skin)
: base(skin)
{
@@ -42,14 +47,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null;
case TaikoSkinComponents.InputDrum:
- if (GetTexture("taiko-bar-left") != null)
+ if (hasBarLeft)
return new LegacyInputDrum();
return null;
case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit:
- if (GetTexture("taikohitcircle") != null)
+ if (hasHitCircle)
return new LegacyHit(taikoComponent.Component);
return null;
diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs
index f14288e7ba..4ef466efbf 100644
--- a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs
+++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs
@@ -9,8 +9,10 @@ using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
@@ -96,5 +98,41 @@ namespace osu.Game.Tests.Beatmaps
var second = beatmaps.GetWorkingBeatmap(beatmap, true);
Assert.That(first, Is.Not.SameAs(second));
});
+
+ [Test]
+ public void TestSavePreservesCollections() => AddStep("run test", () =>
+ {
+ var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach());
+
+ var working = beatmaps.GetWorkingBeatmap(beatmap);
+
+ Assert.That(working.BeatmapInfo.BeatmapSet?.Files, Has.Count.GreaterThan(0));
+
+ string initialHash = working.BeatmapInfo.MD5Hash;
+
+ var preserveCollection = new BeatmapCollection("test contained");
+ preserveCollection.BeatmapMD5Hashes.Add(initialHash);
+
+ var noNewCollection = new BeatmapCollection("test not contained");
+
+ Realm.Write(r =>
+ {
+ r.Add(preserveCollection);
+ r.Add(noNewCollection);
+ });
+
+ Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(initialHash));
+ Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash));
+
+ beatmaps.Save(working.BeatmapInfo, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
+
+ string finalHash = working.BeatmapInfo.MD5Hash;
+
+ Assert.That(finalHash, Is.Not.SameAs(initialHash));
+
+ Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash));
+ Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(finalHash));
+ Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(finalHash));
+ });
}
}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 1e87ed27df..a98f931e7a 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -71,9 +71,9 @@ namespace osu.Game.Tests.Editing
[TestCase(1)]
[TestCase(2)]
- public void TestSpeedMultiplier(float multiplier)
+ public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{
- assertSnapDistance(100 * multiplier, new HitObject
+ assertSnapDistance(100, new HitObject
{
DifficultyControlPoint = new DifficultyControlPoint
{
diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20221012.osk b/osu.Game.Tests/Resources/Archives/modified-default-20221012.osk
new file mode 100644
index 0000000000..74ff4f31d5
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-default-20221012.osk differ
diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
index 1b03f8ef6b..989459632e 100644
--- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
+++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
@@ -38,7 +38,9 @@ namespace osu.Game.Tests.Skins
// Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk",
// Covers clicks/s counter
- "Archives/modified-default-20220818.osk"
+ "Archives/modified-default-20220818.osk",
+ // Covers longest combo counter
+ "Archives/modified-default-20221012.osk"
};
///
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 5aadd6f56a..917434ae22 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -244,7 +244,10 @@ namespace osu.Game.Tests.Visual.Background
public void TestResumeFromPlayer()
{
performFullSetup();
- AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos));
+ AddStep("Move mouse to Visual Settings location", () => InputManager.MoveMouseTo(playerLoader.ScreenSpaceDrawQuad.TopRight
+ + new Vector2(-playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Width,
+ playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Height / 2
+ )));
AddStep("Resume PlayerLoader", () => player.Restart());
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
index 56435c69a4..6a69347651 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
@@ -4,8 +4,10 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing
@@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene
{
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
public TestSceneEditorComposeRadioButtons()
{
EditorRadioButtonCollection collection;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index adb495f3d3..58b5b41702 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -148,10 +148,6 @@ namespace osu.Game.Tests.Visual.Editing
});
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
-
- AddStep("place circle", () => InputManager.Click(MouseButton.Left));
-
- AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
index 7e0981ce69..54ad4e25e4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
@@ -29,16 +29,18 @@ namespace osu.Game.Tests.Visual.Editing
private TimelineBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType().First();
+ private Vector2 getPosition(HitObject hitObject) =>
+ blueprintContainer.SelectionBlueprints.First(s => s.Item == hitObject).ScreenSpaceDrawQuad.Centre;
+
+ private Vector2 getMiddlePosition(HitObject hitObject1, HitObject hitObject2) =>
+ (getPosition(hitObject1) + getPosition(hitObject2)) / 2;
+
private void moveMouseToObject(Func targetFunc)
{
AddStep("move mouse to object", () =>
{
- var pos = blueprintContainer.SelectionBlueprints
- .First(s => s.Item == targetFunc())
- .ChildrenOfType()
- .First().ScreenSpaceDrawQuad.Centre;
-
- InputManager.MoveMouseTo(pos);
+ var hitObject = targetFunc();
+ InputManager.MoveMouseTo(getPosition(hitObject));
});
}
@@ -262,6 +264,56 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
+ [Test]
+ public void TestBasicDragSelection()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 500, Position = new Vector2(100) },
+ new HitCircle { StartTime = 1000, Position = new Vector2(200) },
+ new HitCircle { StartTime = 1500, Position = new Vector2(300) },
+ };
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1])));
+ AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left));
+
+ AddStep("drag to select", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[2], addedObjects[3])));
+ assertSelectionIs(new[] { addedObjects[1], addedObjects[2] });
+
+ AddStep("drag to deselect", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[1], addedObjects[2])));
+ assertSelectionIs(new[] { addedObjects[1] });
+
+ AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left));
+ assertSelectionIs(new[] { addedObjects[1] });
+ }
+
+ [Test]
+ public void TestFastDragSelection()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 500 },
+ new HitCircle { StartTime = 20000, Position = new Vector2(100) },
+ new HitCircle { StartTime = 31000, Position = new Vector2(200) },
+ new HitCircle { StartTime = 60000, Position = new Vector2(300) },
+ };
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1])));
+ AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left));
+ AddStep("start drag", () => InputManager.MoveMouseTo(getPosition(addedObjects[1])));
+
+ AddStep("jump editor clock", () => EditorClock.Seek(30000));
+ AddStep("jump editor clock", () => EditorClock.Seek(60000));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+ assertSelectionIs(addedObjects.Skip(1));
+ AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count);
+ }
+
private void assertSelectionIs(IEnumerable hitObjects)
=> AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 1858aee76b..89c5b9b23b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
}
}
},
- new MenuCursor()
+ new MenuCursorContainer()
};
scrollContainer.Add(innerBox = new Box
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index da6604a653..a984f508ea 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Tests.Gameplay;
@@ -148,6 +149,42 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
}
+ [Test]
+ public void TestInputDoesntWorkWhenHUDHidden()
+ {
+ SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType().Single();
+
+ bool seeked = false;
+
+ createNew();
+
+ AddStep("bind seek", () =>
+ {
+ seeked = false;
+
+ var progress = getSongProgress();
+
+ progress.ShowHandle = true;
+ progress.OnSeek += _ => seeked = true;
+ });
+
+ AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
+ AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
+
+ AddStep("attempt seek", () =>
+ {
+ InputManager.MoveMouseTo(getSongProgress());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("seek not performed", () => !seeked);
+
+ AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
+
+ AddStep("attempt seek", () => InputManager.Click(MouseButton.Left));
+ AddAssert("seek performed", () => seeked);
+ }
+
[Test]
public void TestHiddenHUDDoesntBlockComponentUpdates()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
new file mode 100644
index 0000000000..f8b5085a70
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
@@ -0,0 +1,498 @@
+// 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.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneScoring : OsuTestScene
+ {
+ private GraphContainer graphs = null!;
+ private SettingsSlider sliderMaxCombo = null!;
+
+ private FillFlowContainer legend = null!;
+
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("setup tests", () =>
+ {
+ Children = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ graphs = new GraphContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ new Drawable[]
+ {
+ legend = new FillFlowContainer
+ {
+ Padding = new MarginPadding(20),
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ },
+ new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ Padding = new MarginPadding(20),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Full,
+ Children = new Drawable[]
+ {
+ sliderMaxCombo = new SettingsSlider
+ {
+ Width = 0.5f,
+ TransferValueOnCommit = true,
+ Current = new BindableInt(1024)
+ {
+ MinValue = 96,
+ MaxValue = 8192,
+ },
+ LabelText = "max combo",
+ },
+ new OsuTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ AutoSizeAxes = Axes.Y,
+ Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
+ }
+ }
+ },
+ },
+ }
+ }
+ };
+
+ sliderMaxCombo.Current.BindValueChanged(_ => rerun());
+
+ graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
+ graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
+
+ graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
+
+ rerun();
+ });
+ }
+
+ private const int base_great = 300;
+ private const int base_ok = 100;
+
+ private void rerun()
+ {
+ graphs.Clear();
+ legend.Clear();
+
+ runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
+ runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
+
+ runScoreV1();
+ runScoreV2();
+ }
+
+ private void runScoreV1()
+ {
+ int totalScore = 0;
+ int currentCombo = 0;
+
+ void applyHitV1(int baseScore)
+ {
+ if (baseScore == 0)
+ {
+ currentCombo = 0;
+ return;
+ }
+
+ const float score_multiplier = 1;
+
+ totalScore += baseScore;
+
+ // combo multiplier
+ // ReSharper disable once PossibleLossOfFraction
+ totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
+
+ currentCombo++;
+ }
+
+ runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
+ () => applyHitV1(base_great),
+ () => applyHitV1(base_ok),
+ () => applyHitV1(0),
+ () =>
+ {
+ // Arbitrary value chosen towards the upper range.
+ const double score_multiplier = 4;
+
+ return (int)(totalScore * score_multiplier);
+ });
+ }
+
+ private void runScoreV2()
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ int currentCombo = 0;
+ double comboPortion = 0;
+ double currentBaseScore = 0;
+ double maxBaseScore = 0;
+ int currentHits = 0;
+
+ for (int i = 0; i < maxCombo; i++)
+ applyHitV2(base_great);
+
+ double comboPortionMax = comboPortion;
+
+ currentCombo = 0;
+ comboPortion = 0;
+ currentBaseScore = 0;
+ maxBaseScore = 0;
+ currentHits = 0;
+
+ void applyHitV2(int baseScore)
+ {
+ maxBaseScore += base_great;
+ currentBaseScore += baseScore;
+ comboPortion += baseScore * (1 + ++currentCombo / 10.0);
+
+ currentHits++;
+ }
+
+ runForAlgorithm("ScoreV2", Color4.OrangeRed,
+ () => applyHitV2(base_great),
+ () => applyHitV2(base_ok),
+ () =>
+ {
+ currentHits++;
+ maxBaseScore += base_great;
+ currentCombo = 0;
+ }, () =>
+ {
+ double accuracy = currentBaseScore / maxBaseScore;
+
+ return (int)Math.Round
+ (
+ 700000 * comboPortion / comboPortionMax +
+ 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
+ );
+ });
+ }
+
+ private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ var beatmap = new OsuBeatmap();
+ for (int i = 0; i < maxCombo; i++)
+ beatmap.HitObjects.Add(new HitCircle());
+
+ processor.ApplyBeatmap(beatmap);
+
+ runForAlgorithm(name, colour,
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
+ () => (int)processor.TotalScore.Value);
+ }
+
+ private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore)
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ List results = new List();
+
+ for (int i = 0; i < maxCombo; i++)
+ {
+ if (graphs.MissLocations.Contains(i))
+ applyMiss();
+ else if (graphs.NonPerfectLocations.Contains(i))
+ applyNonPerfect();
+ else
+ applyHit();
+
+ results.Add(getTotalScore());
+ }
+
+ graphs.Add(new LineGraph
+ {
+ Name = name,
+ RelativeSizeAxes = Axes.Both,
+ LineColour = colour,
+ Values = results
+ });
+
+ legend.Add(new OsuSpriteText
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
+ });
+
+ legend.Add(new OsuSpriteText
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ Text = $"final score {getTotalScore():#,0}"
+ });
+ }
+ }
+
+ public class GraphContainer : Container, IHasCustomTooltip>
+ {
+ public readonly BindableList MissLocations = new BindableList();
+ public readonly BindableList NonPerfectLocations = new BindableList();
+
+ public Bindable MaxCombo = new Bindable();
+
+ protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+
+ private readonly Box hoverLine;
+
+ private readonly Container missLines;
+ private readonly Container verticalGridLines;
+
+ public int CurrentHoverCombo { get; private set; }
+
+ public GraphContainer()
+ {
+ InternalChild = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.1f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ verticalGridLines = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ hoverLine = new Box
+ {
+ Colour = Color4.Yellow,
+ RelativeSizeAxes = Axes.Y,
+ Origin = Anchor.TopCentre,
+ Alpha = 0,
+ Width = 1,
+ },
+ missLines = new Container
+ {
+ Alpha = 0.6f,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Content,
+ }
+ };
+
+ MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
+ NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
+
+ MaxCombo.BindValueChanged(_ =>
+ {
+ updateMissLocations();
+ updateVerticalGridLines();
+ }, true);
+ }
+
+ private void updateVerticalGridLines()
+ {
+ verticalGridLines.Clear();
+
+ for (int i = 0; i < MaxCombo.Value; i++)
+ {
+ if (i % 100 == 0)
+ {
+ verticalGridLines.AddRange(new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)i / MaxCombo.Value,
+ },
+ new OsuSpriteText
+ {
+ RelativePositionAxes = Axes.X,
+ X = (float)i / MaxCombo.Value,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Text = $"{i:#,0}",
+ Rotation = -30,
+ Y = -20,
+ }
+ });
+ }
+ }
+ }
+
+ private void updateMissLocations()
+ {
+ missLines.Clear();
+
+ foreach (int miss in MissLocations)
+ {
+ missLines.Add(new Box
+ {
+ Colour = Color4.Red,
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)miss / MaxCombo.Value,
+ });
+ }
+
+ foreach (int miss in NonPerfectLocations)
+ {
+ missLines.Add(new Box
+ {
+ Colour = Color4.Orange,
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)miss / MaxCombo.Value,
+ });
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ hoverLine.Show();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ hoverLine.Hide();
+ base.OnHoverLost(e);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
+
+ hoverLine.X = e.MousePosition.X;
+ return base.OnMouseMove(e);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button == MouseButton.Left)
+ MissLocations.Add(CurrentHoverCombo);
+ else
+ NonPerfectLocations.Add(CurrentHoverCombo);
+
+ return true;
+ }
+
+ private GraphTooltip? tooltip;
+
+ public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
+
+ public IEnumerable TooltipContent => Content.OfType();
+
+ public class GraphTooltip : CompositeDrawable, ITooltip>
+ {
+ private readonly GraphContainer graphContainer;
+
+ private readonly OsuTextFlowContainer textFlow;
+
+ public GraphTooltip(GraphContainer graphContainer)
+ {
+ this.graphContainer = graphContainer;
+ AutoSizeAxes = Axes.Both;
+
+ Masking = true;
+ CornerRadius = 10;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.15f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ textFlow = new OsuTextFlowContainer
+ {
+ Colour = Color4.White,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding(10),
+ }
+ };
+ }
+
+ private int? lastContentCombo;
+
+ public void SetContent(IEnumerable content)
+ {
+ int relevantCombo = graphContainer.CurrentHoverCombo;
+
+ if (lastContentCombo == relevantCombo)
+ return;
+
+ lastContentCombo = relevantCombo;
+ textFlow.Clear();
+
+ textFlow.AddParagraph($"At combo {relevantCombo}:");
+
+ foreach (var graph in content)
+ {
+ float valueAtHover = graph.Values.ElementAt(relevantCombo);
+ float ofTotal = valueAtHover / graph.Values.Last();
+
+ textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
+ }
+ }
+
+ public void Move(Vector2 pos) => this.MoveTo(pos);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 156a1ee34a..6d036f8e9b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addControlPoints(IList controlPoints, double sequenceStartTime)
{
- controlPoints.ForEach(point => point.StartTime += sequenceStartTime);
+ controlPoints.ForEach(point => point.Time += sequenceStartTime);
scrollContainers.ForEach(container =>
{
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var playfield in playfields)
{
foreach (var controlPoint in controlPoints)
- playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime));
+ playfield.Add(createDrawablePoint(playfield, controlPoint.Time));
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index f31261dc1f..63677ce378 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -97,14 +97,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
- public void TestCurrentItemDoesNotHaveDeleteButton()
+ public void TestSingleItemDoesNotHaveDeleteButton()
+ {
+ AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
+ AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+
+ assertDeleteButtonVisibility(0, false);
+ }
+
+ [Test]
+ public void TestCurrentItemHasDeleteButtonIfNotSingle()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
- assertDeleteButtonVisibility(0, false);
+ assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, true);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index cf62c73ad4..6070b1456f 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -79,6 +80,16 @@ namespace osu.Game.Tests.Visual.Navigation
[Resolved]
private OsuGameBase gameBase { get; set; }
+ [Test]
+ public void TestCursorHidesWhenIdle()
+ {
+ AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
+ AddUntilStep("wait until idle", () => Game.IsIdle.Value);
+ AddUntilStep("menu cursor hidden", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+ AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
+ AddUntilStep("menu cursor shown", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
+ }
+
[Test]
public void TestNullRulesetHandled()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
new file mode 100644
index 0000000000..bf01d3b0ac
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
@@ -0,0 +1,257 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Comments;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneCommentActions : OsuManualInputManagerTestScene
+ {
+ private Container content = null!;
+ protected override Container Content => content;
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ [Cached(typeof(IDialogOverlay))]
+ private readonly DialogOverlay dialogOverlay = new DialogOverlay();
+
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private CommentsContainer commentsContainer = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ base.Content.AddRange(new Drawable[]
+ {
+ content = new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ dialogOverlay
+ });
+ }
+
+ [SetUpSteps]
+ public void SetUp()
+ {
+ Schedule(() =>
+ {
+ API.Login("test", "test");
+ Child = commentsContainer = new CommentsContainer();
+ });
+ }
+
+ [Test]
+ public void TestNonOwnCommentCantBeDeleted()
+ {
+ addTestComments();
+
+ AddUntilStep("First comment has button", () =>
+ {
+ var comments = this.ChildrenOfType();
+ var ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
+ return ourComment != null && ourComment.ChildrenOfType().Any(x => x.Text == "Delete");
+ });
+
+ AddAssert("Second doesn't", () =>
+ {
+ var comments = this.ChildrenOfType();
+ var ourComment = comments.Single(x => x.Comment.Id == 2);
+ return ourComment.ChildrenOfType().All(x => x.Text != "Delete");
+ });
+ }
+
+ private readonly ManualResetEventSlim deletionPerformed = new ManualResetEventSlim();
+
+ [Test]
+ public void TestDeletion()
+ {
+ DrawableComment? ourComment = null;
+
+ addTestComments();
+ AddUntilStep("Comment exists", () =>
+ {
+ var comments = this.ChildrenOfType();
+ ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
+ return ourComment != null;
+ });
+ AddStep("It has delete button", () =>
+ {
+ var btn = ourComment.ChildrenOfType().Single(x => x.Text == "Delete");
+ InputManager.MoveMouseTo(btn);
+ });
+ AddStep("Click delete button", () =>
+ {
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("Setup request handling", () =>
+ {
+ deletionPerformed.Reset();
+
+ dummyAPI.HandleRequest = request =>
+ {
+ if (!(request is CommentDeleteRequest req))
+ return false;
+
+ if (req.CommentId != 1)
+ return false;
+
+ CommentBundle cb = new CommentBundle
+ {
+ Comments = new List
+ {
+ new Comment
+ {
+ Id = 2,
+ Message = "This is a comment by another user",
+ UserId = API.LocalUser.Value.Id + 1,
+ CreatedAt = DateTimeOffset.Now,
+ User = new APIUser
+ {
+ Id = API.LocalUser.Value.Id + 1,
+ Username = "Another user"
+ }
+ },
+ },
+ IncludedComments = new List(),
+ PinnedComments = new List(),
+ };
+
+ Task.Run(() =>
+ {
+ deletionPerformed.Wait(10000);
+ req.TriggerSuccess(cb);
+ });
+
+ return true;
+ };
+ });
+ AddStep("Confirm dialog", () => InputManager.Key(Key.Number1));
+
+ AddAssert("Loading spinner shown", () => commentsContainer.ChildrenOfType().Any(d => d.IsPresent));
+
+ AddStep("Complete request", () => deletionPerformed.Set());
+
+ AddUntilStep("Comment is deleted locally", () => this.ChildrenOfType().Single(x => x.Comment.Id == 1).WasDeleted);
+ }
+
+ [Test]
+ public void TestDeletionFail()
+ {
+ DrawableComment? ourComment = null;
+ bool delete = false;
+
+ addTestComments();
+ AddUntilStep("Comment exists", () =>
+ {
+ var comments = this.ChildrenOfType();
+ ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
+ return ourComment != null;
+ });
+ AddStep("It has delete button", () =>
+ {
+ var btn = ourComment.ChildrenOfType().Single(x => x.Text == "Delete");
+ InputManager.MoveMouseTo(btn);
+ });
+ AddStep("Click delete button", () =>
+ {
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("Setup request handling", () =>
+ {
+ dummyAPI.HandleRequest = request =>
+ {
+ if (request is not CommentDeleteRequest req)
+ return false;
+
+ req.TriggerFailure(new Exception());
+ delete = true;
+ return false;
+ };
+ });
+ AddStep("Confirm dialog", () => InputManager.Key(Key.Number1));
+ AddUntilStep("Deletion requested", () => delete);
+ AddUntilStep("Comment is available", () =>
+ {
+ return !this.ChildrenOfType().Single(x => x.Comment.Id == 1).WasDeleted;
+ });
+ AddAssert("Loading spinner hidden", () =>
+ {
+ return ourComment.ChildrenOfType().All(d => !d.IsPresent);
+ });
+ AddAssert("Actions available", () =>
+ {
+ return ourComment.ChildrenOfType().Single(x => x.Name == @"Actions buttons").IsPresent;
+ });
+ }
+
+ private void addTestComments()
+ {
+ AddStep("set up response", () =>
+ {
+ CommentBundle cb = new CommentBundle
+ {
+ Comments = new List
+ {
+ new Comment
+ {
+ Id = 1,
+ Message = "This is our comment",
+ UserId = API.LocalUser.Value.Id,
+ CreatedAt = DateTimeOffset.Now,
+ User = API.LocalUser.Value,
+ },
+ new Comment
+ {
+ Id = 2,
+ Message = "This is a comment by another user",
+ UserId = API.LocalUser.Value.Id + 1,
+ CreatedAt = DateTimeOffset.Now,
+ User = new APIUser
+ {
+ Id = API.LocalUser.Value.Id + 1,
+ Username = "Another user"
+ }
+ },
+ },
+ IncludedComments = new List(),
+ PinnedComments = new List(),
+ };
+ setUpCommentsResponse(cb);
+ });
+
+ AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
+ }
+
+ private void setUpCommentsResponse(CommentBundle commentBundle)
+ {
+ dummyAPI.HandleRequest = request =>
+ {
+ if (!(request is GetCommentsRequest getCommentsRequest))
+ return false;
+
+ getCommentsRequest.TriggerSuccess(commentBundle);
+ return true;
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index 39432ee059..863b352618 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -189,6 +189,16 @@ Line after image";
});
}
+ [Test]
+ public void TestFlag()
+ {
+ AddStep("Add flag", () =>
+ {
+ markdownContainer.CurrentPath = @"https://dev.ppy.sh";
+ markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
+ });
+ }
+
private class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
index 558bff2f3c..27cd74bb1f 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
@@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Online
Locale = "en",
Subtitle = "Article styling criteria",
Markdown =
- "# Formatting\n\n*For the writing standards, see: [Article style criteria/Writing](../Writing)*\n\n*Notice: This article uses [RFC 2119](https://tools.ietf.org/html/rfc2119 \"IETF Tools\") to describe requirement levels.*\n\n## Locales\n\nListed below are the properly-supported locales for the wiki:\n\n| File Name | Locale Name | Native Script |\n| :-- | :-- | :-- |\n| `en.md` | English | English |\n| `ar.md` | Arabic | اَلْعَرَبِيَّةُ |\n| `be.md` | Belarusian | Беларуская мова |\n| `bg.md` | Bulgarian | Български |\n| `cs.md` | Czech | Česky |\n| `da.md` | Danish | Dansk |\n| `de.md` | German | Deutsch |\n| `gr.md` | Greek | Ελληνικά |\n| `es.md` | Spanish | Español |\n| `fi.md` | Finnish | Suomi |\n| `fr.md` | French | Français |\n| `hu.md` | Hungarian | Magyar |\n| `id.md` | Indonesian | Bahasa Indonesia |\n| `it.md` | Italian | Italiano |\n| `ja.md` | Japanese | 日本語 |\n| `ko.md` | Korean | 한국어 |\n| `nl.md` | Dutch | Nederlands |\n| `no.md` | Norwegian | Norsk |\n| `pl.md` | Polish | Polski |\n| `pt.md` | Portuguese | Português |\n| `pt-br.md` | Brazilian Portuguese | Português (Brasil) |\n| `ro.md` | Romanian | Română |\n| `ru.md` | Russian | Русский |\n| `sk.md` | Slovak | Slovenčina |\n| `sv.md` | Swedish | Svenska |\n| `th.md` | Thai | ไทย |\n| `tr.md` | Turkish | Türkçe |\n| `uk.md` | Ukrainian | Українська мова |\n| `vi.md` | Vietnamese | Tiếng Việt |\n| `zh.md` | Chinese (Simplified) | 简体中文 |\n| `zh-tw.md` | Traditional Chinese (Taiwan) | 繁體中文(台灣) |\n\n*Note: The website will give readers their selected language's version of an article. If it is not available, the English version will be given.*\n\n### Content parity\n\nTranslations are subject to strict content parity with their English article, in the sense that they must have the same message, regardless of grammar and syntax. Any changes to the translations' meanings must be accompanied by equivalent changes to the English article.\n\nThere are some cases where the content is allowed to differ:\n\n- Articles originally written in a language other than English (in this case, English should act as the translation)\n- Explanations of English words that are common terms in the osu! community\n- External links\n- Tags\n- Subcommunity-specific explanations\n\n## Front matter\n\nFront matter must be placed at the very top of the file. It is written in [YAML](https://en.wikipedia.org/wiki/YAML#Example \"YAML Wikipedia article\") and describes additional information about the article. This must be surrounded by three hyphens (`---`) on the lines above and below it, and an empty line must follow it before the title heading.\n\n### Articles that need help\n\n*Note: Avoid translating English articles with this tag. In addition to this, this tag should be added when the translation needs its own clean up.*\n\nThe `needs_cleanup` tag may be added to articles that need rewriting or formatting help. It is also acceptable to open an issue on GitHub for this purpose. This tag must be written as shown below:\n\n```yaml\nneeds_cleanup: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be done to remove the tag.\n\n### Outdated articles\n\n*Note: Avoid translating English articles with this tag. If the English article has this tag, the translation must also have this tag.*\n\nTranslated articles that are outdated must use the `outdated` tag when the English variant is updated. English articles may also become outdated when the content they contain is misleading or no longer relevant. This tag must be written as shown below:\n\n```yaml\noutdated: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be updated to remove the tag.\n\n### Tagging articles\n\nTags help the website's search engine query articles better. Tags should be written in the same language as the article and include the original list of tags. Tags should use lowercase letters where applicable.\n\nFor example, an article called \"Beatmap discussion\" may include the following tags:\n\n```yaml\ntags:\n - beatmap discussions\n - modding V2\n - MV2\n```\n\n### Translations without reviews\n\n*Note: Wiki maintainers will determine and apply this mark prior to merging.*\n\nSometimes, translations are added to the wiki without review from other native speakers of the language. In this case, the `no_native_review` mark is added to let future translators know that it may need to be checked again. This tag must be written as shown below:\n\n```yaml\nno_native_review: true\n```\n\n## Article naming\n\n*See also: [Folder names](#folder-names) and [Titles](#titles)*\n\nArticle titles should be singular and use sentence case. See [Wikipedia's naming conventions article](https://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(plurals) \"Wikipedia\") for more details.\n\nArticle titles should match the folder name it is in (spaces may replace underscores (`_`) where appropriate). If the folder name changes, the article title should be changed to match it and vice versa.\n\n---\n\nContest and tournament articles are an exception. The folder name must use abbreviations, acronyms, or initialisms. The article's title must be the full name of the contest or tournament.\n\n## Folder and file structure\n\n### Folder names\n\n*See also: [Article naming](#article-naming)*\n\nFolder names must be in English and use sentence case.\n\nFolder names must only use these characters:\n\n- uppercase and lowercase letters\n- numbers\n- underscores (`_`)\n- hyphens (`-`)\n- exclamation marks (`!`)\n\n### Article file names\n\nThe file name of an article can be found in the `File Name` column of the [locales section](#locales). The location of a translated article must be placed in the same folder as the English article.\n\n### Index articles\n\nAn index article must be created if the folder is intended to only hold other articles. Index articles must contain a list of articles that are inside its own folder. They may also contain other information, such as a lead paragraph or descriptions of the linked articles.\n\n### Disambiguation articles\n\n[Disambiguation](/wiki/Disambiguation) articles must be placed in the `/wiki/Disambiguation` folder. The main page must be updated to include the disambiguation article. Refer to [Disambiguation/Mod](/wiki/Disambiguation/Mod) as an example.\n\nRedirects must be updated to have the ambiguous keyword(s) redirect to the disambiguation article.\n\nArticles linked from a disambiguation article must have a [For other uses](#for-other-uses) hatnote.\n\n## HTML\n\nHTML must not be used, with exception for [comments](#comments). The structure of the article must be redone if HTML is used.\n\n### Comments\n\nHTML comments should be used for marking to-dos, but may also be used to annotate text. They should be on their own line, but can be placed inline in a paragraph. If placed inline, the start of the comment must not have a space.\n\nBad example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\nGood example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\n## Editing\n\n### End of line sequence\n\n*Caution: Uploading Markdown files using `CRLF` (carriage return and line feed) via GitHub will result in those files using `CRLF`. To prevent this, set the line ending to `LF` (line feed) before uploading.*\n\nMarkdown files must be checked in using the `LF` end of line sequence.\n\n### Escaping\n\nMarkdown syntax should be escaped as needed. However, article titles are parsed as plain text and so must not be escaped.\n\n### Paragraphs\n\nEach paragraph must be followed by one empty line.\n\n### Line breaks\n\nLine breaks must use a backslash (`\\`).\n\nLine breaks must be used sparingly.\n\n## Hatnote\n\n*Not to be confused with [Notice](#notice).*\n\nHatnotes are short notes placed at the top of an article or section to help readers navigate to related articles or inform them about related topics.\n\nHatnotes must be italicised and be placed immediately after the heading. If multiple hatnotes are used, they must be on the same paragraph separated with a line break.\n\n### Main page\n\n*Main page* hatnotes direct the reader to the main article of a topic. When this hatnote is used, it implies that the section it is on is a summary of what the linked page is about. This hatnote should have only one link. These must be formatted as follows:\n\n```markdown\n*Main page: {article}*\n\n*Main pages: {article} and {article}*\n```\n\n### See also\n\n*See also* hatnotes suggest to readers other points of interest from a given article or section. These must be formatted as follows:\n\n```markdown\n*See also: {article}*\n\n*See also: {article} and {article}*\n```\n\n### For see\n\n*For see* hatnotes are similar to *see also* hatnotes, but are generally more descriptive and direct. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*For {description}, see: {article}`*\n\n*For {description}, see: {article} and {article}`*\n```\n\n### Not to be confused with\n\n*Not to be confused with* hatnotes help distinguish ambiguous or misunderstood article titles or sections. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*Not to be confused with {article}.*\n\n*Not to be confused with {article} or {article}.*\n```\n\n### For other uses\n\n*For other uses* hatnotes are similar to *not to be confused with* hatnotes, but links directly to the [disambiguation article](#disambiguation-articles). This hatnote must only link to the disambiguation article. These must be formatted as follows:\n\n```markdown\n*For other uses, see {disambiguation article}.*\n```\n\n## Notice\n\n*Not to be confused with [Hatnote](#hatnote).*\n\nA notice should be placed where appropriate in a section, but must start off the paragraph and use italics. Notices may contain bolding where appropriate, but should be kept to a minimum. Notices must be written as complete sentences. Thus, unlike most [hatnotes](#hatnotes), must use a full stop (`.`) or an exclamation mark (`!`) if appropriate. Anything within the same paragraph of a notice must also be italicised. These must be formatted as follows:\n\n```markdown\n*Note: {note}.*\n\n*Notice: {notice}.*\n\n*Caution: {caution}.*\n\n*Warning: {warning}.*\n```\n\n- `Note` should be used for factual or trivial details.\n- `Notice` should be used for reminders or to draw attention to something that the reader should be made aware of.\n- `Caution` should be used to warn the reader to avoid unintended consequences.\n- `Warning` should be used to warn the reader that action may be taken against them.\n\n## Emphasising\n\n### Bold\n\nBold must use double asterisks (`**`).\n\nLead paragraphs may bold the first occurrence of the article's title.\n\n### Italics\n\nItalics must use single asterisks (`*`).\n\nNames of work or video games should be italicised. osu!—the game—is exempt from this.\n\nThe first occurrence of an abbreviation, acronym, or initialism may be italicised.\n\nItalics may also be used to provide emphasis or help with readability.\n\n## Headings\n\nAll headings must use sentence case.\n\nHeadings must use the [ATX (hash) style](https://github.github.com/gfm/#atx-headings \"GitHub\") and must have an empty line before and after the heading. The title heading is an exception when it is on the first line. If this is the case, there only needs to be an empty line after the title heading.\n\nHeadings must not exceed a heading level of 5 and must not be used to style or format text.\n\n### Titles\n\n*See also: [Article naming](#article-naming)*\n\n*Caution: Titles are parsed as plain text; they must not be escaped.*\n\nThe first heading in all articles must be a level 1 heading, being the article's title. All headings afterwards must be [section headings](#sections). Titles must not contain formatting, links, or images.\n\nThe title heading must be on the first line, unless [front matter](#front-matter) is being used. If that is the case, the title heading must go after it and have an empty line before the title heading.\n\n### Sections\n\nSection headings must use levels 2 to 5. The section heading proceeding the [title heading](#titles) must be a level 2 heading. Unlike titles, section headings may have small image icons.\n\nSection headings must not skip a heading level (i.e. do not go from a level 2 heading to a level 4 heading) and must not contain formatting or links.\n\n*Notice: On the website, heading levels 4 and 5 will not appear in the table of contents. They cannot be linked to directly either.*\n\n## Lists\n\nLists should not go over 4 levels of indentation and should not have an empty line in between each item.\n\nFor nested lists, bullets or numbers must align with the item content of their parent lists.\n\nThe following example was done incorrectly (take note of the spacing before the bullet):\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\nThe following example was done correctly:\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\n### Bulleted\n\nBulleted lists must use a hyphen (`-`). These must then be followed by one space. (Example shown below.)\n\n```markdown\n- osu!\n - Hit circle\n - Combo number\n - Approach circle\n - Slider\n - Hit circles\n - Slider body\n - Slider ticks\n - Spinner\n- osu!taiko\n```\n\n### Numbered\n\nThe numbers in a numbered list must be incremented to represent their step.\n\n```markdown\n1. Download the osu! installer.\n2. Run the installer.\n 1. To change the installation location, click the text underneath the progression bar.\n 2. The installer will prompt for a new location, choose the installation folder.\n3. osu! will start up once installation is complete.\n4. Sign in.\n```\n\n### Mixed\n\nCombining both bulleted and numbered lists should be done sparingly.\n\n```markdown\n1. Download a skin from the forums.\n2. Load the skin file into osu!.\n - If the file is a `.zip`, unzip it and move the contents into the `Skins/` folder (found in your osu! installation folder).\n - If the file is a `.osk`, open it on your desktop or drag-and-drop it into the game client.\n3. Open osu!, if it is not opened, and select the skin in the options.\n - This may have been completed if you opened the `.osk` file or drag-and-dropped it into the game client.\n```\n\n## Code\n\nThe markup for code is a grave mark (`` ` ``). To put grave marks in code, use double grave marks instead. If the grave mark is at the start or end, pad it with one space. (Example shown below.)\n\n```markdown\n`` ` ``\n`` `Space` ``\n```\n\n### Keyboard keys\n\n*Notice: When denoting the letter itself, and not the keyboard key, use quotation marks instead.*\n\nWhen representing keyboard keys, use capital letters for single characters and title case for modifiers. Use the plus symbol (`+`) (without code) to represent key combinations. (Example shown below.)\n\n```markdown\npippi is spelt with a lowercase \"p\" like peppy.\n\nPress `Ctrl` + `O` to open the open dialog.\n```\n\nWhen representing a space or the spacebar, use `` `Space` ``.\n\n### Button and menu text\n\nWhen copying the text from a menu or button, the letter casing should be copied as it appears. (Example shown below.)\n\n```markdown\nThe `osu!direct` button is visible in the main menu on the right side, if you have an active osu!supporter tag.\n```\n\n### Folder and directory names\n\nWhen copying the name of a folder or directory, the letter casing should be copied as it appears, but prefer lowercased paths when possible. Directory paths must not be absolute (i.e. do not start the directory name from the drive letter or from the root folder). (Example shown below.)\n\n```markdown\nosu! is installed in the `AppData/Local` folder by default, unless specified otherwise during installation.\n```\n\n### Keywords and commands\n\nWhen copying a keyword or command, the letter casing should be copied as it appears or how someone normally would type it. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nAs of now, the `Name` and `Author` commands in the skin configuration file (`skin.ini`) do nothing.\n```\n\n### File names\n\nWhen copying the name of a file, the letter casing should be copied as it appears. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nTo play osu!, double click the `osu!.exe` icon.\n```\n\n### File extensions\n\n*Notice: File formats (not to be confused with file extensions) must be written in capital letters without the prefixed fullstop (`.`).*\n\nFile extensions must be prefixed with a fullstop (`.`) and be followed by the file extension in lowercase letters. (Example shown below.)\n\n```markdown\nThe JPG (or JPEG) file format has the `.jpg` (or `.jpeg`) extension.\n```\n\n### Chat channels\n\nWhen copying the name of a chat channel, start it with a hash (`#`), followed by the channel name in lowercase letters. (Example shown below.)\n\n```markdown\n`#lobby` is where you can advertise your multi room.\n```\n\n## Preformatted text (code blocks)\n\n*Notice: Syntax highlighting for preformatted text is not implemented on the website yet.*\n\nPreformatted text (also known as code blocks) must be fenced using three grave marks. They should set the language identifier for syntax highlighting.\n\n## Links\n\nThere are two types of links: inline and reference. Inline has two styles.\n\nThe following is an example of both inline styles:\n\n```markdown\n[Game Modifiers](/wiki/Game_Modifiers)\n\n\n```\n\nThe following is an example of the reference style:\n\n```markdown\n[Game Modifiers][game mods link]\n\n[game mods link]: /wiki/Game_Modifiers\n```\n\n---\n\nLinks must use the inline style if they are only referenced once. The inline angle brackets style should be avoided. References to reference links must be placed at the bottom of the article.\n\n### Internal links\n\n*Note: Internal links refer to links that stay inside the `https://osu.ppy.sh/` domain.*\n\n#### Wiki links\n\nAll links that point to an wiki article should start with `/wiki/` followed by the path to get to the article you are targeting. Relative links may also be used. Some examples include the following:\n\n```markdown\n[FAQ](/wiki/FAQ)\n[pippi](/wiki/Mascots#-pippi)\n[Beatmaps](../)\n[Pattern](./Pattern)\n```\n\nWiki links must not use redirects and must not have a trailing forward slash (`/`).\n\nBad examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/ASC)\n[Developers](/wiki/Developers/)\n[Developers](/wiki/Developers/#game-client-developers)\n```\n\nGood examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/Article_styling_criteria)\n[Developers](/wiki/Developers)\n[Developers](/wiki/Developers#game-client-developers)\n```\n\n##### Sub-article links\n\nWiki links that point to a sub-article should include the parent article's folder name in its link text. See the following example:\n\n```markdown\n*See also: [Beatmap Editor/Design](/wiki/Beatmap_Editor/Design)*\n```\n\n##### Section links\n\n*Notice: On the website, heading levels 4 and 5 are not given the id attribute. This means that they can not be linked to directly.*\n\nWiki links that point to a section of an article may use the section sign symbol (`§`). See the following example:\n\n```markdown\n*For timing rules, see: [Ranking Criteria § Timing](/wiki/Ranking_Criteria#timing)*\n```\n\n#### Other osu! links\n\nThe URL from the address bar of your web browser should be copied as it is when linking to other osu! web pages. The `https://osu.ppy.sh` part of the URL must be kept.\n\n##### User profiles\n\nAll usernames must be linked on first occurrence. Other occurrences are optional, but must be consistent throughout the entire article for all usernames. If it is difficult to determine the user's id, it may be skipped over.\n\nWhen linking to a user profile, the user's id number must be used. Use the new website (`https://osu.ppy.sh/users/{username})`) to get the user's id.\n\nThe link text of the user link should be the user's current name.\n\n##### Difficulties\n\nWhenever linking to a single difficulty, use this format as the link text:\n\n```\n{artist} - {title} ({creator}) [{difficuty_name}]\n```\n\nThe link must actually link to that difficulty. Beatmap difficulty URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}#{mode}/{BeatmapID}\n```\n\nThe difficulty name may be left outside of the link text, but doing so must be consistent throughout the entire article.\n\n##### Beatmaps\n\nWhenever linking to a beatmap, use this format as the link text:\n\n```\n{artist} - {title} ({creator})\n```\n\nAll beatmap URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}\n```\n\n### External links\n\n*Notice: External links refers to links that go outside the `https://osu.ppy.sh/` domain.*\n\nThe `https` protocol must be used, unless the site does not support it. External links must be a clean and direct link to a reputable source. The link text should be the title of the page it is linking to. The URL from the address bar of your web browser should be copied as it is when linking to other external pages.\n\nThere are no visual differences between external and osu! web links. Due to this, the website name should be included in the title text. See the following example:\n\n```markdown\n*For more information about music theory, see: [Music theory](https://en.wikipedia.org/wiki/Music_theory \"Wikipedia\")*\n```\n\n## Images\n\nThere are two types of image links: inline and reference. Examples:\n\n**Inline style:**\n\n```markdown\n![](/wiki/shared/flag/AU.gif)\n```\n\n**Reference style:**\n\n```markdown\n![][flag_AU]\n\n[flag_AU]: /wiki/shared/flag/AU.gif\n```\n\nImages should use the inline linking style. References to reference links must be placed at the bottom of the article.\n\nImages must be placed in a folder named `img`, located in the article's folder. Images that are used in multiple articles should be stored in the `/wiki/shared/` folder.\n\n### Image caching\n\nImages on the website are cached for up to 60 days. The cached image is matched with the image link's URL.\n\nWhen updating an image, either change the image's name or append a query string to the URL. In both cases, all translations linking to the updated image should also be updated.\n\n### Formats and quality\n\nImages should use the JPG format at quality 8 (80 or 80%, depending on the program). If the image contains transparency or has text that must be readable, use the PNG format instead. If the image contains an animation, the GIF format can be used; however, this should be used sparingly as these may take longer to load or can be bigger then the [max file size](#file-size).\n\n### File size\n\nImages must be under 1 megabyte, otherwise they will fail to load. Downscaling and using JPG at 80% is almost always under the size limit.\n\nAll images should be optimised as much as possible. Use [jpeg-archive](https://github.com/danielgtaylor/jpeg-archive \"GitHub\") to compress JPEG images. For consistency, use the following command for jpeg-archive:\n\n```sh\njpeg-recompress -am smallfry
- private void testLocalCursor()
+ [Test]
+ public void TestLocalCursor()
{
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
- AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
AddStep("Move out", moveOut);
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
}
@@ -125,47 +126,98 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether overriding a user cursor (green) with another user cursor (blue)
/// results in the correct visibility and states for the cursors.
///
- private void testUserCursorOverride()
+ [Test]
+ public void TestUserCursorOverride()
{
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
- AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
- AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
+ AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor));
+ AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor));
}
///
/// -- Yellow-Purple Box Boundary --
/// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time.
///
- private void testMultipleLocalCursors()
+ [Test]
+ public void TestMultipleLocalCursors()
{
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
- AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
- AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
+ AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
}
///
/// -- Yellow-Blue Box Boundary --
/// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue).
///
- private void testUserOverrideWithLocal()
+ [Test]
+ public void TestUserOverrideWithLocal()
{
- AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
- AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
- AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
+ AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10, 0)));
+ AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
+ AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
+ AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
+ }
+
+ ///
+ /// Ensures non-mouse input hides global cursor on a "local cursor" area (which doesn't hide global cursor).
+ ///
+ [Test]
+ public void TestKeyboardLocalCursor([Values] bool clickToShow)
+ {
+ AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
+ AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3].ScreenSpaceDrawQuad.Centre + new Vector2(10, 0)));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.Alpha == 1);
+
+ AddStep("Press key", () => InputManager.Key(Key.A));
+ AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddUntilStep("Check global cursor alpha is 0", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+
+ if (clickToShow)
+ AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
+ else
+ AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
+
+ AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddUntilStep("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
+ }
+
+ ///
+ /// Ensures mouse input after non-mouse input doesn't show global cursor on a "user cursor" area (which hides global cursor).
+ ///
+ [Test]
+ public void TestKeyboardUserCursor([Values] bool clickToShow)
+ {
+ AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
+ AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
+ AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check global cursor alpha is 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+
+ AddStep("Press key", () => InputManager.Key(Key.A));
+ AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+
+ if (clickToShow)
+ AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
+ else
+ AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
+
+ AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
}
///
@@ -191,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public bool SmoothTransition;
- public CursorContainer MenuCursor { get; }
+ public CursorContainer Cursor { get; }
public bool ProvidingUserCursor { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
@@ -218,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
Text = providesUserCursor ? "User cursor" : "Local cursor"
},
- MenuCursor = new TestCursorContainer
+ Cursor = new TestCursorContainer
{
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
index 50e506f82b..9fb0905a4f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
@@ -5,12 +5,14 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
@@ -20,11 +22,17 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private SettingsToolboxGroup group;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = group = new SettingsToolboxGroup("example")
{
+ Scale = new Vector2(3),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Children = new Drawable[]
{
new RoundedButton
diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs
index db48c36c99..d3b40a3526 100644
--- a/osu.Game.Tournament/JsonPointConverter.cs
+++ b/osu.Game.Tournament/JsonPointConverter.cs
@@ -6,6 +6,7 @@
using System;
using System.Diagnostics;
using System.Drawing;
+using System.Globalization;
using Newtonsoft.Json;
namespace osu.Game.Tournament
@@ -31,7 +32,9 @@ namespace osu.Game.Tournament
Debug.Assert(str != null);
- return new PointConverter().ConvertFromString(str) as Point? ?? new Point();
+ // Null check suppression is required due to .NET standard expecting a non-null context.
+ // Seems to work fine at a runtime level (and the parameter is nullable in .NET 6+).
+ return new PointConverter().ConvertFromString(null!, CultureInfo.InvariantCulture, str) as Point? ?? new Point();
}
var point = new Point();
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 1bc929604d..348661e2a3 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
- req.Success += res =>
+ req.Success += res => Schedule(() =>
{
Model.Beatmap = new TournamentBeatmap(res);
updatePanel();
- };
+ });
- req.Failure += _ =>
+ req.Failure += _ => Schedule(() =>
{
Model.Beatmap = null;
updatePanel();
- };
+ });
API.Queue(req);
}, true);
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 292caa4397..bcb1d7f961 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -141,18 +141,9 @@ namespace osu.Game.Beatmaps
// Handle collections using permissive difficulty name to track difficulties.
foreach (var originalBeatmap in original.Beatmaps)
{
- var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName);
-
- if (updatedBeatmap == null)
- continue;
-
- var collections = realm.All().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash));
-
- foreach (var c in collections)
- {
- c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash);
- c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash);
- }
+ updated.Beatmaps
+ .FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName)?
+ .TransferCollectionReferences(realm, originalBeatmap.MD5Hash);
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 32b7f0b29b..6f9df1ba7f 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -8,6 +8,7 @@ using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
@@ -213,6 +214,23 @@ namespace osu.Game.Beatmaps
return fileHashX == fileHashY;
}
+ ///
+ /// When updating a beatmap, its hashes will change. Collections currently track beatmaps by hash, so they need to be updated.
+ /// This method will handle updating
+ ///
+ /// A realm instance in an active write transaction.
+ /// The previous MD5 hash of the beatmap before update.
+ public void TransferCollectionReferences(Realm realm, string previousMD5Hash)
+ {
+ var collections = realm.All().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(previousMD5Hash));
+
+ foreach (var c in collections)
+ {
+ c.BeatmapMD5Hashes.Remove(previousMD5Hash);
+ c.BeatmapMD5Hashes.Add(MD5Hash);
+ }
+ }
+
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 2c6edb64f8..befc56d244 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -311,6 +311,8 @@ namespace osu.Game.Beatmaps
if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo);
+ string oldMd5Hash = beatmapInfo.MD5Hash;
+
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
@@ -327,6 +329,8 @@ namespace osu.Game.Beatmaps
setInfo.CopyChangesToRealm(liveBeatmapSet);
+ beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
+
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
});
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 56a432aec4..0a09e6e7e6 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -9,11 +9,8 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
- public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable
+ public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint
{
- ///
- /// The time at which the control point takes effect.
- ///
[JsonIgnore]
public double Time { get; set; }
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 4be6b5eede..422e306450 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// The time to find the control point at.
/// The control point to use when is before any control points.
/// The active control point at , or a fallback if none found.
- protected T BinarySearchWithFallback(IReadOnlyList list, double time, T fallback)
- where T : ControlPoint
+ public static T BinarySearchWithFallback(IReadOnlyList list, double time, T fallback)
+ where T : class, IControlPoint
{
return BinarySearch(list, time) ?? fallback;
}
@@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The list to search.
/// The time to find the control point at.
- /// The active control point at .
- protected virtual T BinarySearch(IReadOnlyList list, double time)
- where T : ControlPoint
+ /// The active control point at . Will return null if there are no control points, or if the time is before the first control point.
+ public static T BinarySearch(IReadOnlyList list, double time)
+ where T : class, IControlPoint
{
if (list == null)
throw new ArgumentNullException(nameof(list));
diff --git a/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
new file mode 100644
index 0000000000..091e99e029
--- /dev/null
+++ b/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
@@ -0,0 +1,13 @@
+// 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.Beatmaps.ControlPoints
+{
+ public interface IControlPoint
+ {
+ ///
+ /// The time at which the control point takes effect.
+ ///
+ double Time { get; }
+ }
+}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 683bc19ce6..5f0a2a0824 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
+ case LegacyEventType.Sprite:
+ // Generally, the background is the first thing defined in a beatmap file.
+ // In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
+ // Allow the first sprite (by file order) to act as the background in such cases.
+ if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
+ beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
+ break;
+
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break;
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index a39766abe1..0f0e72b0ac 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
///
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
///
- void PrepareTrackForPreview(bool looping);
+ void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0);
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 0a51c843cd..393c4ba892 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
- public void PrepareTrackForPreview(bool looping)
+ public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
Track.Looping = looping;
Track.RestartPoint = Metadata.PreviewTime;
@@ -125,6 +125,8 @@ namespace osu.Game.Beatmaps
Track.RestartPoint = 0.4f * Track.Length;
}
+
+ Track.RestartPoint += offsetFromPreviewPoint;
}
///
diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs
index 877c90a534..6cba8fe819 100644
--- a/osu.Game/Database/ModelDownloader.cs
+++ b/osu.Game/Database/ModelDownloader.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Database
public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null);
- public void DownloadAsUpdate(TModel originalModel) => Download(originalModel, false, originalModel);
+ public void DownloadAsUpdate(TModel originalModel, bool minimiseDownloadSize) => Download(originalModel, minimiseDownloadSize, originalModel);
protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel)
{
diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs
index f1bc0bfe0e..0286815569 100644
--- a/osu.Game/Database/RealmArchiveModelImporter.cs
+++ b/osu.Game/Database/RealmArchiveModelImporter.cs
@@ -294,15 +294,38 @@ namespace osu.Game.Database
// Log output here will be missing a valid hash in non-batch imports.
LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}...");
+ List files = new List();
+
+ if (archive != null)
+ {
+ // Import files to the disk store.
+ // We intentionally delay adding to realm to avoid blocking on a write during disk operations.
+ foreach (var filenames in getShortenedFilenames(archive))
+ {
+ using (Stream s = archive.GetStream(filenames.original))
+ files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
+ }
+ }
+
+ using (var transaction = realm.BeginWrite())
+ {
+ // Add all files to realm in one go.
+ // This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails.
+ foreach (var file in files)
+ {
+ if (!file.File.IsManaged)
+ realm.Add(file.File, true);
+ }
+
+ transaction.Commit();
+ }
+
+ item.Files.AddRange(files);
+ item.Hash = ComputeHash(item);
+
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
using (var transaction = realm.BeginWrite())
{
- if (archive != null)
- // TODO: look into rollback of file additions (or delayed commit).
- item.Files.AddRange(createFileInfos(archive, Files, realm));
-
- item.Hash = ComputeHash(item);
-
// TODO: we may want to run this outside of the transaction.
Populate(item, archive, realm, cancellationToken);
@@ -425,16 +448,6 @@ namespace osu.Game.Database
{
var fileInfos = new List();
- // import files to manager
- foreach (var filenames in getShortenedFilenames(reader))
- {
- using (Stream s = reader.GetStream(filenames.original))
- {
- var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened);
- fileInfos.Add(item);
- }
- }
-
return fileInfos;
}
diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs
index ecb152c45c..036b15ea17 100644
--- a/osu.Game/Database/RealmFileStore.cs
+++ b/osu.Game/Database/RealmFileStore.cs
@@ -40,8 +40,8 @@ namespace osu.Game.Database
///
/// The file data stream.
/// The realm instance to add to. Should already be in a transaction.
- ///
- public RealmFile Add(Stream data, Realm realm)
+ /// Whether the should immediately be added to the underlying realm. If false is provided here, the instance must be manually added.
+ public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
{
string hash = data.ComputeSHA2Hash();
@@ -52,7 +52,7 @@ namespace osu.Game.Database
if (!checkFileExistsAndMatchesHash(file))
copyToStore(file, data);
- if (!file.IsManaged)
+ if (addToRealm && !file.IsManaged)
realm.Add(file);
return file;
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
index 62fefe201d..3855ed6d4e 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs
@@ -5,6 +5,7 @@
using Markdig;
using Markdig.Extensions.AutoLinks;
+using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.EmphasisExtras;
using Markdig.Extensions.Footnotes;
using Markdig.Extensions.Tables;
@@ -32,6 +33,12 @@ namespace osu.Game.Graphics.Containers.Markdown
///
protected virtual bool Autolinks => false;
+ ///
+ /// Allows this markdown container to parse custom containers (used for flags and infoboxes).
+ ///
+ ///
+ protected virtual bool CustomContainers => false;
+
public OsuMarkdownContainer()
{
LineSpacing = 21;
@@ -107,6 +114,9 @@ namespace osu.Game.Graphics.Containers.Markdown
if (Autolinks)
pipeline = pipeline.UseAutoLinks();
+ if (CustomContainers)
+ pipeline.UseCustomContainers();
+
return pipeline.Build();
}
}
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
index db8abfb269..9d7b47281f 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs
@@ -3,6 +3,9 @@
#nullable disable
+using System;
+using System.Linq;
+using Markdig.Extensions.CustomContainers;
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -11,6 +14,9 @@ using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
+using osu.Game.Users;
+using osu.Game.Users.Drawables;
+using osuTK;
namespace osu.Game.Graphics.Containers.Markdown
{
@@ -33,6 +39,31 @@ namespace osu.Game.Graphics.Containers.Markdown
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
+ protected override void AddCustomComponent(CustomContainerInline inline)
+ {
+ if (!(inline.FirstChild is LiteralInline literal))
+ {
+ base.AddCustomComponent(inline);
+ return;
+ }
+
+ string[] attributes = literal.Content.ToString().Trim(' ', '{', '}').Split();
+ string flagAttribute = attributes.SingleOrDefault(a => a.StartsWith(@"flag", StringComparison.Ordinal));
+
+ if (flagAttribute == null)
+ {
+ base.AddCustomComponent(inline);
+ return;
+ }
+
+ string flag = flagAttribute.Split('=').Last().Trim('"');
+
+ if (!Enum.TryParse(flag, out var countryCode))
+ countryCode = CountryCode.Unknown;
+
+ AddDrawable(new DrawableFlag(countryCode) { Size = new Vector2(20, 15) });
+ }
+
private class OsuMarkdownInlineCode : Container
{
[Resolved]
diff --git a/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs b/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
index 6613e18cbe..f5429723be 100644
--- a/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
+++ b/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
@@ -13,7 +13,7 @@ using osu.Game.Configuration;
namespace osu.Game.Graphics.Cursor
{
///
- /// A container which provides the main .
+ /// A container which provides the main .
/// Also handles cases where a more localised cursor is provided by another component (via ).
///
public class GlobalCursorDisplay : Container, IProvideCursor
@@ -23,7 +23,9 @@ namespace osu.Game.Graphics.Cursor
///
internal bool ShowCursor = true;
- public CursorContainer MenuCursor { get; }
+ CursorContainer IProvideCursor.Cursor => MenuCursor;
+
+ public MenuCursorContainer MenuCursor { get; }
public bool ProvidingUserCursor => true;
@@ -42,8 +44,8 @@ namespace osu.Game.Graphics.Cursor
{
AddRangeInternal(new Drawable[]
{
- MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } },
- Content = new Container { RelativeSizeAxes = Axes.Both }
+ Content = new Container { RelativeSizeAxes = Axes.Both },
+ MenuCursor = new MenuCursorContainer { State = { Value = Visibility.Hidden } }
});
}
@@ -64,7 +66,7 @@ namespace osu.Game.Graphics.Cursor
if (!hasValidInput || !ShowCursor)
{
- currentOverrideProvider?.MenuCursor?.Hide();
+ currentOverrideProvider?.Cursor?.Hide();
currentOverrideProvider = null;
return;
}
@@ -83,8 +85,8 @@ namespace osu.Game.Graphics.Cursor
if (currentOverrideProvider == newOverrideProvider)
return;
- currentOverrideProvider?.MenuCursor?.Hide();
- newOverrideProvider.MenuCursor?.Show();
+ currentOverrideProvider?.Cursor?.Hide();
+ newOverrideProvider.Cursor?.Show();
currentOverrideProvider = newOverrideProvider;
}
diff --git a/osu.Game/Graphics/Cursor/IProvideCursor.cs b/osu.Game/Graphics/Cursor/IProvideCursor.cs
index f7f7b75bc8..9f01e5da6d 100644
--- a/osu.Game/Graphics/Cursor/IProvideCursor.cs
+++ b/osu.Game/Graphics/Cursor/IProvideCursor.cs
@@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
/// The cursor provided by this .
/// May be null if no cursor should be visible.
///
- CursorContainer MenuCursor { get; }
+ CursorContainer Cursor { get; }
///
- /// Whether should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
+ /// Whether should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
///
bool ProvidingUserCursor { get; }
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
similarity index 69%
rename from osu.Game/Graphics/Cursor/MenuCursor.cs
rename to osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index 862a10208c..af542989ff 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -1,10 +1,7 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -21,24 +18,43 @@ using osuTK;
namespace osu.Game.Graphics.Cursor
{
- public class MenuCursor : CursorContainer
+ public class MenuCursorContainer : CursorContainer
{
private readonly IBindable screenshotCursorVisibility = new Bindable(true);
public override bool IsPresent => screenshotCursorVisibility.Value && base.IsPresent;
+ private bool hideCursorOnNonMouseInput;
+
+ public bool HideCursorOnNonMouseInput
+ {
+ get => hideCursorOnNonMouseInput;
+ set
+ {
+ if (hideCursorOnNonMouseInput == value)
+ return;
+
+ hideCursorOnNonMouseInput = value;
+ updateState();
+ }
+ }
+
protected override Drawable CreateCursor() => activeCursor = new Cursor();
- private Cursor activeCursor;
+ private Cursor activeCursor = null!;
- private Bindable cursorRotate;
private DragRotationState dragRotationState;
private Vector2 positionMouseDown;
-
- private Sample tapSample;
private Vector2 lastMovePosition;
- [BackgroundDependencyLoader(true)]
- private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
+ private Bindable cursorRotate = null!;
+ private Sample tapSample = null!;
+
+ private MouseInputDetector mouseInputDetector = null!;
+
+ private bool visible;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config, ScreenshotManager? screenshotManager, AudioManager audio)
{
cursorRotate = config.GetBindable(OsuSetting.CursorRotation);
@@ -46,6 +62,45 @@ namespace osu.Game.Graphics.Cursor
screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility);
tapSample = audio.Samples.Get(@"UI/cursor-tap");
+
+ Add(mouseInputDetector = new MouseInputDetector());
+ }
+
+ [Resolved]
+ private OsuGame? game { get; set; }
+
+ private readonly IBindable lastInputWasMouse = new BindableBool();
+ private readonly IBindable isIdle = new BindableBool();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ lastInputWasMouse.BindTo(mouseInputDetector.LastInputWasMouseSource);
+ lastInputWasMouse.BindValueChanged(_ => updateState(), true);
+
+ if (game != null)
+ {
+ isIdle.BindTo(game.IsIdle);
+ isIdle.BindValueChanged(_ => updateState());
+ }
+ }
+
+ protected override void UpdateState(ValueChangedEvent state) => updateState();
+
+ private void updateState()
+ {
+ bool combinedVisibility = State.Value == Visibility.Visible && (lastInputWasMouse.Value || !hideCursorOnNonMouseInput) && !isIdle.Value;
+
+ if (visible == combinedVisibility)
+ return;
+
+ visible = combinedVisibility;
+
+ if (visible)
+ PopIn();
+ else
+ PopOut();
}
protected override void Update()
@@ -163,11 +218,11 @@ namespace osu.Game.Graphics.Cursor
public class Cursor : Container
{
- private Container cursorContainer;
- private Bindable cursorScale;
+ private Container cursorContainer = null!;
+ private Bindable cursorScale = null!;
private const float base_scale = 0.15f;
- public Sprite AdditiveLayer;
+ public Sprite AdditiveLayer = null!;
public Cursor()
{
@@ -204,6 +259,40 @@ namespace osu.Game.Graphics.Cursor
}
}
+ private class MouseInputDetector : Component
+ {
+ ///
+ /// Whether the last input applied to the game is sourced from mouse.
+ ///
+ public IBindable LastInputWasMouseSource => lastInputWasMouseSource;
+
+ private readonly Bindable lastInputWasMouseSource = new Bindable();
+
+ public MouseInputDetector()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected override bool Handle(UIEvent e)
+ {
+ switch (e)
+ {
+ case MouseDownEvent:
+ case MouseMoveEvent:
+ lastInputWasMouseSource.Value = true;
+ return false;
+
+ case KeyDownEvent keyDown when !keyDown.Repeat:
+ case JoystickPressEvent:
+ case MidiDownEvent:
+ lastInputWasMouseSource.Value = false;
+ return false;
+ }
+
+ return false;
+ }
+ }
+
private enum DragRotationState
{
NotDragging,
diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
index 7f86a060ad..e51dbeed14 100644
--- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
+++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
-using osu.Game.Localisation;
using osu.Framework.Platform;
using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
@@ -94,15 +93,7 @@ namespace osu.Game.Graphics.UserInterface
private void copyUrl()
{
host.GetClipboard()?.SetText(Link);
- onScreenDisplay?.Display(new CopyUrlToast(ToastStrings.UrlCopied));
- }
-
- private class CopyUrlToast : Toast
- {
- public CopyUrlToast(LocalisableString value)
- : base(UserInterfaceStrings.GeneralHeader, value, "")
- {
- }
+ onScreenDisplay?.Display(new CopyUrlToast());
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
index 6358317e9d..f0ff76b35d 100644
--- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Graphics.UserInterface
[Description("button")]
Button,
+ [Description("button-sidebar")]
+ ButtonSidebar,
+
[Description("toolbar")]
Toolbar,
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index bbd8f8ecea..8772c1e2d9 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -26,24 +26,24 @@ namespace osu.Game.Graphics.UserInterface
{
set
{
- if (labelText != null)
- labelText.Text = value;
+ if (LabelTextFlowContainer != null)
+ LabelTextFlowContainer.Text = value;
}
}
public MarginPadding LabelPadding
{
- get => labelText?.Padding ?? new MarginPadding();
+ get => LabelTextFlowContainer?.Padding ?? new MarginPadding();
set
{
- if (labelText != null)
- labelText.Padding = value;
+ if (LabelTextFlowContainer != null)
+ LabelTextFlowContainer.Padding = value;
}
}
protected readonly Nub Nub;
- private readonly OsuTextFlowContainer labelText;
+ protected readonly OsuTextFlowContainer LabelTextFlowContainer;
private Sample sampleChecked;
private Sample sampleUnchecked;
@@ -56,7 +56,7 @@ namespace osu.Game.Graphics.UserInterface
Children = new Drawable[]
{
- labelText = new OsuTextFlowContainer(ApplyLabelParameters)
+ LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters)
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@@ -70,19 +70,19 @@ namespace osu.Game.Graphics.UserInterface
Nub.Anchor = Anchor.CentreRight;
Nub.Origin = Anchor.CentreRight;
Nub.Margin = new MarginPadding { Right = nub_padding };
- labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
+ LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
else
{
Nub.Anchor = Anchor.CentreLeft;
Nub.Origin = Anchor.CentreLeft;
Nub.Margin = new MarginPadding { Left = nub_padding };
- labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
+ LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
Nub.Current.BindTo(Current);
- Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
+ Current.DisabledChanged += disabled => LabelTextFlowContainer.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
}
///
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 2a8b41fd20..9acb0c7f94 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -44,6 +44,8 @@ namespace osu.Game.Graphics.UserInterface
public virtual LocalisableString TooltipText { get; private set; }
+ public bool PlaySamplesOnAdjust { get; set; } = true;
+
///
/// Whether to format the tooltip as a percentage or the actual value.
///
@@ -187,6 +189,9 @@ namespace osu.Game.Graphics.UserInterface
private void playSample(T value)
{
+ if (!PlaySamplesOnAdjust)
+ return;
+
if (Clock == null || Clock.CurrentTime - lastSampleTime <= 30)
return;
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
index 42e1073baf..0e348108aa 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
+ protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
+
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs
new file mode 100644
index 0000000000..7aaf12ca34
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs
@@ -0,0 +1,38 @@
+// 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.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ internal class OsuDirectorySelectorHiddenToggle : OsuCheckbox
+ {
+ public OsuDirectorySelectorHiddenToggle()
+ {
+ RelativeSizeAxes = Axes.None;
+ AutoSizeAxes = Axes.None;
+ Size = new Vector2(100, 50);
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.CentreLeft;
+ LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
+ LabelTextFlowContainer.Origin = Anchor.CentreLeft;
+ LabelText = @"Show hidden";
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours)
+ {
+ if (overlayColourProvider != null)
+ return;
+
+ Nub.AccentColour = colours.GreySeaFoamLighter;
+ Nub.GlowingAccentColour = Color4.White;
+ Nub.GlowColour = Color4.White;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
index 3e8b7dc209..70af68d595 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
@@ -33,6 +33,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
+ protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
+
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
index 14a041b459..4f079ab435 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
@@ -55,13 +55,13 @@ namespace osu.Game.Input.Bindings
{
// The first fire of this is a bit redundant as this is being called in base.LoadComplete,
// but this is safest in case the subscription is restored after a context recycle.
- reloadMappings(sender.AsQueryable());
+ ReloadMappings(sender.AsQueryable());
});
base.LoadComplete();
}
- protected override void ReloadMappings() => reloadMappings(queryRealmKeyBindings(realm.Realm));
+ protected sealed override void ReloadMappings() => ReloadMappings(queryRealmKeyBindings(realm.Realm));
private IQueryable queryRealmKeyBindings(Realm realm)
{
@@ -70,7 +70,7 @@ namespace osu.Game.Input.Bindings
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
}
- private void reloadMappings(IQueryable realmKeyBindings)
+ protected virtual void ReloadMappings(IQueryable realmKeyBindings)
{
var defaults = DefaultKeyBindings.ToList();
diff --git a/osu.Game/Localisation/PopupDialogStrings.cs b/osu.Game/Localisation/PopupDialogStrings.cs
new file mode 100644
index 0000000000..b2e9673cbe
--- /dev/null
+++ b/osu.Game/Localisation/PopupDialogStrings.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.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class PopupDialogStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.PopupDialog";
+
+ ///
+ /// "Are you sure you want to update this beatmap?"
+ ///
+ public static LocalisableString UpdateLocallyModifiedText => new TranslatableString(getKey(@"update_locally_modified_text"), @"Are you sure you want to update this beatmap?");
+
+ ///
+ /// "This will discard all local changes you have on that beatmap."
+ ///
+ public static LocalisableString UpdateLocallyModifiedDescription => new TranslatableString(getKey(@"update_locally_modified_description"), @"This will discard all local changes you have on that beatmap.");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/CommentDeleteRequest.cs b/osu.Game/Online/API/Requests/CommentDeleteRequest.cs
new file mode 100644
index 0000000000..b150a6d5fc
--- /dev/null
+++ b/osu.Game/Online/API/Requests/CommentDeleteRequest.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class CommentDeleteRequest : APIRequest
+ {
+ public readonly long CommentId;
+
+ public CommentDeleteRequest(long id)
+ {
+ CommentId = id;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+ req.Method = HttpMethod.Delete;
+ return req;
+ }
+
+ protected override string Target => $@"comments/{CommentId}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/Comment.cs b/osu.Game/Online/API/Requests/Responses/Comment.cs
index 500c0566e6..907632186c 100644
--- a/osu.Game/Online/API/Requests/Responses/Comment.cs
+++ b/osu.Game/Online/API/Requests/Responses/Comment.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 Newtonsoft.Json;
using System;
@@ -16,18 +14,18 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"parent_id")]
public long? ParentId { get; set; }
- public Comment ParentComment { get; set; }
+ public Comment? ParentComment { get; set; }
[JsonProperty(@"user_id")]
public long? UserId { get; set; }
- public APIUser User { get; set; }
+ public APIUser? User { get; set; }
[JsonProperty(@"message")]
- public string Message { get; set; }
+ public string Message { get; set; } = null!;
[JsonProperty(@"message_html")]
- public string MessageHtml { get; set; }
+ public string? MessageHtml { get; set; }
[JsonProperty(@"replies_count")]
public int RepliesCount { get; set; }
@@ -36,13 +34,13 @@ namespace osu.Game.Online.API.Requests.Responses
public int VotesCount { get; set; }
[JsonProperty(@"commenatble_type")]
- public string CommentableType { get; set; }
+ public string CommentableType { get; set; } = null!;
[JsonProperty(@"commentable_id")]
public int CommentableId { get; set; }
[JsonProperty(@"legacy_name")]
- public string LegacyName { get; set; }
+ public string? LegacyName { get; set; }
[JsonProperty(@"created_at")]
public DateTimeOffset CreatedAt { get; set; }
@@ -62,7 +60,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"pinned")]
public bool Pinned { get; set; }
- public APIUser EditedUser { get; set; }
+ public APIUser? EditedUser { get; set; }
public bool IsTopLevel => !ParentId.HasValue;
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
index 2c9f250028..4469d50acb 100644
--- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -114,6 +114,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("has_replay")]
public bool HasReplay { get; set; }
+ // These properties are calculated or not relevant to any external usage.
public bool ShouldSerializeID() => false;
public bool ShouldSerializeUser() => false;
public bool ShouldSerializeBeatmap() => false;
@@ -122,6 +123,18 @@ namespace osu.Game.Online.API.Requests.Responses
public bool ShouldSerializeOnlineID() => false;
public bool ShouldSerializeHasReplay() => false;
+ // These fields only need to be serialised if they hold values.
+ // Generally this is required because this model may be used by server-side components, but
+ // we don't want to bother sending these fields in score submission requests, for instance.
+ public bool ShouldSerializeEndedAt() => EndedAt != default;
+ public bool ShouldSerializeStartedAt() => StartedAt != default;
+ public bool ShouldSerializeLegacyScoreId() => LegacyScoreId != null;
+ public bool ShouldSerializeLegacyTotalScore() => LegacyTotalScore != null;
+ public bool ShouldSerializeMods() => Mods.Length > 0;
+ public bool ShouldSerializeUserID() => UserID > 0;
+ public bool ShouldSerializeBeatmapID() => BeatmapID > 0;
+ public bool ShouldSerializeBuildID() => BuildID != null;
+
#endregion
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 6f597e5b10..d5e0c7a970 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Online.Rooms
[CanBeNull]
public MultiplayerScoresAround ScoresAround { get; set; }
- public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
+ public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
{
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
if (ruleset == null)
@@ -90,6 +90,8 @@ namespace osu.Game.Online.Rooms
Position = Position,
};
+ scoreManager.PopulateMaximumStatistics(scoreInfo);
+
return scoreInfo;
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 939d3a63ed..2bdcb57f2a 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -70,6 +70,7 @@ namespace osu.Game
/// The full osu! experience. Builds on top of to add menus and binding logic
/// for initial components that are generally retrieved via DI.
///
+ [Cached(typeof(OsuGame))]
public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler
{
///
@@ -136,6 +137,11 @@ namespace osu.Game
private IdleTracker idleTracker;
+ ///
+ /// Whether the user is currently in an idle state.
+ ///
+ public IBindable IsIdle => idleTracker.IsIdle;
+
///
/// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen.
///
@@ -266,8 +272,6 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
- dependencies.CacheAs(this);
-
SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
@@ -563,6 +567,15 @@ namespace osu.Game
// This should be able to be performed from song select, but that is disabled for now
// due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
+ //
+ // As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select.
+ // This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the
+ // song select leaderboard).
+ IEnumerable validScreens =
+ Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
+ ? new[] { typeof(SongSelect) }
+ : Array.Empty();
+
PerformFromScreen(screen =>
{
Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
@@ -580,7 +593,7 @@ namespace osu.Game
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break;
}
- });
+ }, validScreens: validScreens);
}
public override Task Import(params ImportTask[] imports)
@@ -1320,6 +1333,8 @@ namespace osu.Game
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
API.Activity.BindTo(newOsuScreen.Activity);
+ GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput;
+
if (newOsuScreen.HideOverlaysOnEnter)
CloseAllOverlays();
else
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 478f154d58..7d9ed7bf3e 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -62,6 +62,7 @@ namespace osu.Game
/// Unlike , this class will not load any kind of UI, allowing it to be used
/// for provide dependencies to test cases without interfering with them.
///
+ [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
@@ -253,7 +254,6 @@ namespace osu.Game
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
dependencies.Cache(largeStore);
- dependencies.CacheAs(this);
dependencies.CacheAs(LocalConfig);
InitialiseFonts();
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 296320ec1b..0675d99a25 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.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.Containers;
using osu.Framework.Graphics;
using osu.Game.Graphics;
@@ -22,7 +20,13 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
using osu.Framework.Localisation;
+using osu.Framework.Platform;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Comments.Buttons;
+using osu.Game.Overlays.Dialog;
+using osu.Game.Overlays.OSD;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
@@ -31,7 +35,7 @@ namespace osu.Game.Overlays.Comments
{
private const int avatar_size = 40;
- public Action RepliesRequested;
+ public Action RepliesRequested = null!;
public readonly Comment Comment;
@@ -45,13 +49,35 @@ namespace osu.Game.Overlays.Comments
private int currentPage;
- private FillFlowContainer childCommentsVisibilityContainer;
- private FillFlowContainer childCommentsContainer;
- private LoadRepliesButton loadRepliesButton;
- private ShowMoreRepliesButton showMoreButton;
- private ShowRepliesButton showRepliesButton;
- private ChevronButton chevronButton;
- private DeletedCommentsCounter deletedCommentsCounter;
+ ///
+ /// Local field for tracking comment state. Initialized from Comment.IsDeleted, may change when deleting was requested by user.
+ ///
+ public bool WasDeleted { get; protected set; }
+
+ private FillFlowContainer childCommentsVisibilityContainer = null!;
+ private FillFlowContainer childCommentsContainer = null!;
+ private LoadRepliesButton loadRepliesButton = null!;
+ private ShowMoreRepliesButton showMoreButton = null!;
+ private ShowRepliesButton showRepliesButton = null!;
+ private ChevronButton chevronButton = null!;
+ private LinkFlowContainer actionsContainer = null!;
+ private LoadingSpinner actionsLoading = null!;
+ private DeletedCommentsCounter deletedCommentsCounter = null!;
+ private OsuSpriteText deletedLabel = null!;
+ private GridContainer content = null!;
+ private VotePill votePill = null!;
+
+ [Resolved(canBeNull: true)]
+ private IDialogOverlay? dialogOverlay { get; set; }
+
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
+ [Resolved]
+ private GameHost host { get; set; } = null!;
+
+ [Resolved(canBeNull: true)]
+ private OnScreenDisplay? onScreenDisplay { get; set; }
public DrawableComment(Comment comment)
{
@@ -64,8 +90,6 @@ namespace osu.Game.Overlays.Comments
LinkFlowContainer username;
FillFlowContainer info;
CommentMarkdownContainer message;
- GridContainer content;
- VotePill votePill;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -148,9 +172,9 @@ namespace osu.Game.Overlays.Comments
},
Comment.Pinned ? new PinnedCommentNotice() : Empty(),
new ParentUsername(Comment),
- new OsuSpriteText
+ deletedLabel = new OsuSpriteText
{
- Alpha = Comment.IsDeleted ? 1 : 0,
+ Alpha = 0f,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Text = CommentsStrings.Deleted
}
@@ -163,16 +187,36 @@ namespace osu.Game.Overlays.Comments
DocumentMargin = new MarginPadding(0),
DocumentPadding = new MarginPadding(0),
},
- info = new FillFlowContainer
+ new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
- new DrawableDate(Comment.CreatedAt, 12, false)
+ info = new FillFlowContainer
{
- Colour = colourProvider.Foreground1
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Children = new Drawable[]
+ {
+ new DrawableDate(Comment.CreatedAt, 12, false)
+ {
+ Colour = colourProvider.Foreground1
+ }
+ }
+ },
+ actionsContainer = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold))
+ {
+ Name = @"Actions buttons",
+ AutoSizeAxes = Axes.Both,
+ },
+ actionsLoading = new LoadingSpinner
+ {
+ Size = new Vector2(12f),
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft
}
}
},
@@ -246,9 +290,9 @@ namespace osu.Game.Overlays.Comments
if (Comment.UserId.HasValue)
username.AddUserLink(Comment.User);
else
- username.AddText(Comment.LegacyName);
+ username.AddText(Comment.LegacyName!);
- if (Comment.EditedAt.HasValue)
+ if (Comment.EditedAt.HasValue && Comment.EditedUser != null)
{
var font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
var colour = colourProvider.Foreground1;
@@ -282,10 +326,16 @@ namespace osu.Game.Overlays.Comments
if (Comment.HasMessage)
message.Text = Comment.Message;
- if (Comment.IsDeleted)
+ WasDeleted = Comment.IsDeleted;
+ if (WasDeleted)
+ makeDeleted();
+
+ actionsContainer.AddLink("Copy link", copyUrl);
+ actionsContainer.AddArbitraryDrawable(new Container { Width = 10 });
+
+ if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
{
- content.FadeColour(OsuColour.Gray(0.5f));
- votePill.Hide();
+ actionsContainer.AddLink("Delete", deleteComment);
}
if (Comment.IsTopLevel)
@@ -317,11 +367,63 @@ namespace osu.Game.Overlays.Comments
};
}
+ ///
+ /// Transforms some comment's components to show it as deleted. Invoked both from loading and deleting.
+ ///
+ private void makeDeleted()
+ {
+ deletedLabel.Show();
+ content.FadeColour(OsuColour.Gray(0.5f));
+ votePill.Hide();
+ actionsContainer.Expire();
+ }
+
+ ///
+ /// Invokes comment deletion with confirmation.
+ ///
+ private void deleteComment()
+ {
+ if (dialogOverlay == null)
+ deleteCommentRequest();
+ else
+ dialogOverlay.Push(new ConfirmDialog("Do you really want to delete your comment?", deleteCommentRequest));
+ }
+
+ ///
+ /// Invokes comment deletion directly.
+ ///
+ private void deleteCommentRequest()
+ {
+ actionsContainer.Hide();
+ actionsLoading.Show();
+ var request = new CommentDeleteRequest(Comment.Id);
+ request.Success += _ => Schedule(() =>
+ {
+ actionsLoading.Hide();
+ makeDeleted();
+ WasDeleted = true;
+ if (!ShowDeleted.Value)
+ Hide();
+ });
+ request.Failure += _ => Schedule(() =>
+ {
+ actionsLoading.Hide();
+ actionsContainer.Show();
+ });
+ api.Queue(request);
+ }
+
+ private void copyUrl()
+ {
+ host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
+ onScreenDisplay?.Display(new CopyUrlToast());
+ }
+
protected override void LoadComplete()
{
ShowDeleted.BindValueChanged(show =>
{
- if (Comment.IsDeleted)
+ if (WasDeleted)
this.FadeTo(show.NewValue ? 1 : 0);
}, true);
childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true);
@@ -425,7 +527,7 @@ namespace osu.Game.Overlays.Comments
{
public LocalisableString TooltipText => getParentMessage();
- private readonly Comment parentComment;
+ private readonly Comment? parentComment;
public ParentUsername(Comment comment)
{
@@ -445,7 +547,7 @@ namespace osu.Game.Overlays.Comments
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
- Text = parentComment?.User?.Username ?? parentComment?.LegacyName
+ Text = parentComment?.User?.Username ?? parentComment?.LegacyName!
}
};
}
diff --git a/osu.Game/Overlays/OSD/CopyUrlToast.cs b/osu.Game/Overlays/OSD/CopyUrlToast.cs
new file mode 100644
index 0000000000..ea835a1c5e
--- /dev/null
+++ b/osu.Game/Overlays/OSD/CopyUrlToast.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.OSD
+{
+ public class CopyUrlToast : Toast
+ {
+ public CopyUrlToast()
+ : base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "")
+ {
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
index cea8fdd733..8f188f04d9 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Audio
@@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
Children = new Drawable[]
{
- new SettingsSlider
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.MasterVolume,
Current = audio.Volume,
@@ -35,14 +36,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
- new SettingsSlider
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.EffectVolume,
Current = audio.VolumeSample,
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
- new SettingsSlider
+
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.MusicVolume,
Current = audio.VolumeTrack,
@@ -51,5 +53,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
},
};
}
+
+ private class VolumeAdjustSlider : SettingsSlider
+ {
+ protected override Drawable CreateControl()
+ {
+ var sliderBar = (OsuSliderBar)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 c64a3101b7..59b56522a4 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.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 System.Collections.Generic;
using System.Drawing;
using System.Linq;
using osu.Framework;
@@ -29,37 +28,41 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader;
- private FillFlowContainer> scalingSettings;
+ private FillFlowContainer> scalingSettings = null!;
private readonly Bindable currentDisplay = new Bindable();
private readonly IBindableList windowModes = new BindableList();
- private Bindable scalingMode;
- private Bindable sizeFullscreen;
+ private Bindable scalingMode = null!;
+ private Bindable sizeFullscreen = null!;
private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) });
private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable);
[Resolved]
- private OsuGameBase game { get; set; }
+ private OsuGameBase game { get; set; } = null!;
[Resolved]
- private GameHost host { get; set; }
+ private GameHost host { get; set; } = null!;
- private SettingsDropdown resolutionDropdown;
- private SettingsDropdown displayDropdown;
- private SettingsDropdown windowModeDropdown;
+ private IWindow? window;
- private Bindable scalingPositionX;
- private Bindable scalingPositionY;
- private Bindable scalingSizeX;
- private Bindable scalingSizeY;
+ private SettingsDropdown resolutionDropdown = null!;
+ private SettingsDropdown displayDropdown = null!;
+ private SettingsDropdown windowModeDropdown = null!;
+
+ private Bindable scalingPositionX = null!;
+ private Bindable scalingPositionY = null!;
+ private Bindable scalingSizeX = null!;
+ private Bindable scalingSizeY = null!;
private const int transition_duration = 400;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, GameHost host)
{
+ window = host.Window;
+
scalingMode = osuConfig.GetBindable(OsuSetting.Scaling);
sizeFullscreen = config.GetBindable(FrameworkSetting.SizeFullscreen);
scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX);
@@ -67,10 +70,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX);
scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY);
- if (host.Window != null)
+ if (window != null)
{
- currentDisplay.BindTo(host.Window.CurrentDisplayBindable);
- windowModes.BindTo(host.Window.SupportedWindowModes);
+ currentDisplay.BindTo(window.CurrentDisplayBindable);
+ windowModes.BindTo(window.SupportedWindowModes);
+ window.DisplaysChanged += onDisplaysChanged;
}
if (host.Renderer is IWindowsRenderer windowsRenderer)
@@ -87,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
displayDropdown = new DisplaySettingsDropdown
{
LabelText = GraphicsSettingsStrings.Display,
- Items = host.Window?.Displays,
+ Items = window?.Displays,
Current = currentDisplay,
},
resolutionDropdown = new ResolutionSettingsDropdown
@@ -202,19 +206,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
// initial update bypasses transforms
updateScalingModeVisibility();
- void updateDisplayModeDropdowns()
- {
- if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
- resolutionDropdown.Show();
- else
- resolutionDropdown.Hide();
-
- if (displayDropdown.Items.Count() > 1)
- displayDropdown.Show();
- else
- displayDropdown.Hide();
- }
-
void updateScalingModeVisibility()
{
if (scalingMode.Value == ScalingMode.Off)
@@ -225,6 +216,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
}
+ private void onDisplaysChanged(IEnumerable displays)
+ {
+ Scheduler.AddOnce(d =>
+ {
+ displayDropdown.Items = d;
+ updateDisplayModeDropdowns();
+ }, displays);
+ }
+
+ private void updateDisplayModeDropdowns()
+ {
+ if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
+ resolutionDropdown.Show();
+ else
+ resolutionDropdown.Hide();
+
+ if (displayDropdown.Items.Count() > 1)
+ displayDropdown.Show();
+ else
+ displayDropdown.Hide();
+ }
+
private void updateScreenModeWarning()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
@@ -280,7 +293,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
};
}
- private Drawable preview;
+ private Drawable? preview;
private void showPreview()
{
@@ -291,6 +304,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
preview.Expire();
}
+ protected override void Dispose(bool isDisposing)
+ {
+ if (window != null)
+ window.DisplaysChanged -= onDisplaysChanged;
+
+ base.Dispose(isDisposing);
+ }
+
private class ScalingPreview : ScalingContainer
{
public ScalingPreview()
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
index 9ff47578e9..c91a6a48d4 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
@@ -327,6 +327,50 @@ namespace osu.Game.Overlays.Settings.Sections.Input
finalise();
}
+ protected override bool OnTabletAuxiliaryButtonPress(TabletAuxiliaryButtonPressEvent e)
+ {
+ if (!HasFocus)
+ return false;
+
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ finalise();
+
+ return true;
+ }
+
+ protected override void OnTabletAuxiliaryButtonRelease(TabletAuxiliaryButtonReleaseEvent e)
+ {
+ if (!HasFocus)
+ {
+ base.OnTabletAuxiliaryButtonRelease(e);
+ return;
+ }
+
+ finalise();
+ }
+
+ protected override bool OnTabletPenButtonPress(TabletPenButtonPressEvent e)
+ {
+ if (!HasFocus)
+ return false;
+
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ finalise();
+
+ return true;
+ }
+
+ protected override void OnTabletPenButtonRelease(TabletPenButtonReleaseEvent e)
+ {
+ if (!HasFocus)
+ {
+ base.OnTabletPenButtonRelease(e);
+ return;
+ }
+
+ finalise();
+ }
+
private void clear()
{
if (bindTarget == null)
@@ -387,14 +431,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (bindTarget != null) bindTarget.IsBinding = true;
}
- private void updateStoreFromButton(KeyButton button)
- {
- realm.Run(r =>
- {
- var binding = r.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID);
- r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
- });
- }
+ private void updateStoreFromButton(KeyButton button) =>
+ realm.WriteAsync(r => r.Find(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString);
private void updateIsDefaultValue()
{
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index c6a4cbbcaa..2c4832c68a 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -16,6 +16,11 @@ namespace osu.Game.Overlays.Settings
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; }
+ protected SidebarButton()
+ : base(HoverSampleSet.ButtonSidebar)
+ {
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs
index 548d75c9a9..6dd9e2a56d 100644
--- a/osu.Game/Overlays/SettingsToolboxGroup.cs
+++ b/osu.Game/Overlays/SettingsToolboxGroup.cs
@@ -1,8 +1,7 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
@@ -23,25 +22,38 @@ namespace osu.Game.Overlays
{
public class SettingsToolboxGroup : Container, IExpandable
{
+ private readonly string title;
public const int CONTAINER_WIDTH = 270;
private const float transition_duration = 250;
- private const int border_thickness = 2;
private const int header_height = 30;
private const int corner_radius = 5;
- private const float fade_duration = 800;
- private const float inactive_alpha = 0.5f;
-
private readonly Cached headerTextVisibilityCache = new Cached();
- private readonly FillFlowContainer content;
+ protected override Container Content => content;
+
+ private readonly FillFlowContainer content = new FillFlowContainer
+ {
+ Name = @"Content",
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10, Top = 5, Bottom = 10 },
+ Spacing = new Vector2(0, 15),
+ };
public BindableBool Expanded { get; } = new BindableBool(true);
- private readonly OsuSpriteText headerText;
+ private OsuSpriteText headerText = null!;
- private readonly Container headerContent;
+ private Container headerContent = null!;
+
+ private Box background = null!;
+
+ private IconButton expandButton = null!;
///
/// Create a new instance.
@@ -49,20 +61,25 @@ namespace osu.Game.Overlays
/// The title to be displayed in the header of this group.
public SettingsToolboxGroup(string title)
{
+ this.title = title;
+
AutoSizeAxes = Axes.Y;
Width = CONTAINER_WIDTH;
Masking = true;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider)
+ {
CornerRadius = corner_radius;
- BorderColour = Color4.Black;
- BorderThickness = border_thickness;
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- Alpha = 0.5f,
+ Alpha = 0.1f,
+ Colour = colourProvider?.Background4 ?? Color4.Black,
},
new FillFlowContainer
{
@@ -88,7 +105,7 @@ namespace osu.Game.Overlays
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 },
},
- new IconButton
+ expandButton = new IconButton
{
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
@@ -99,19 +116,7 @@ namespace osu.Game.Overlays
},
}
},
- content = new FillFlowContainer
- {
- Name = @"Content",
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.X,
- AutoSizeDuration = transition_duration,
- AutoSizeEasing = Easing.OutQuint,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding(15),
- Spacing = new Vector2(0, 15),
- }
+ content
}
},
};
@@ -175,9 +180,10 @@ namespace osu.Game.Overlays
private void updateFadeState()
{
- this.FadeTo(IsHovered ? 1 : inactive_alpha, fade_duration, Easing.OutQuint);
- }
+ const float fade_duration = 500;
- protected override Container Content => content;
+ background.FadeTo(IsHovered ? 1 : 0.1f, fade_duration, Easing.OutQuint);
+ expandButton.FadeTo(IsHovered ? 1 : 0, fade_duration, Easing.OutQuint);
+ }
}
}
diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
index 4f1083a75c..15c455416c 100644
--- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
+++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Linq;
+using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -16,6 +17,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
public class WikiMarkdownContainer : OsuMarkdownContainer
{
protected override bool Footnotes => true;
+ protected override bool CustomContainers => true;
public string CurrentPath
{
@@ -26,6 +28,11 @@ namespace osu.Game.Overlays.Wiki.Markdown
{
switch (markdownObject)
{
+ case CustomContainer:
+ // infoboxes are parsed into CustomContainer objects, but we don't have support for infoboxes yet.
+ // todo: add support for infobox.
+ break;
+
case YamlFrontMatterBlock yamlFrontMatterBlock:
container.Add(new WikiNoticeContainer(yamlFrontMatterBlock));
break;
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index 4726211666..d23d8ad438 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -8,6 +8,8 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
@@ -52,20 +54,32 @@ namespace osu.Game.Rulesets.Edit
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
- AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
+ AddInternal(new Container
{
- Padding = new MarginPadding(10),
- Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
- Child = new EditorToolboxGroup("snapping")
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Children = new Drawable[]
{
- Child = distanceSpacingSlider = new ExpandableSlider>
+ new Box
{
- Current = { BindTarget = DistanceSpacingMultiplier },
- KeyboardStep = adjust_step,
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
+ {
+ Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
+ Child = new EditorToolboxGroup("snapping")
+ {
+ Child = distanceSpacingSlider = new ExpandableSlider>
+ {
+ Current = { BindTarget = DistanceSpacingMultiplier },
+ KeyboardStep = adjust_step,
+ }
+ }
}
}
});
@@ -91,7 +105,7 @@ namespace osu.Game.Rulesets.Edit
}
}
- public bool OnPressed(KeyBindingPressEvent e)
+ public virtual bool OnPressed(KeyBindingPressEvent e)
{
switch (e.Action)
{
@@ -103,7 +117,7 @@ namespace osu.Game.Rulesets.Edit
return false;
}
- public void OnReleased(KeyBindingReleaseEvent e)
+ public virtual void OnReleased(KeyBindingReleaseEvent e)
{
}
@@ -134,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
{
- return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
index d3371d3543..26dd5dfa55 100644
--- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
+++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
@@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Edit
{
RelativeSizeAxes = Axes.Y;
- FillFlow.Spacing = new Vector2(10);
+ FillFlow.Spacing = new Vector2(5);
+ Padding = new MarginPadding { Vertical = 5 };
}
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 37c66037eb..3bed835854 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -12,10 +12,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
+using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -80,7 +82,7 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
Config = Dependencies.Get().GetConfigFor(Ruleset);
@@ -102,7 +104,7 @@ namespace osu.Game.Rulesets.Edit
InternalChildren = new Drawable[]
{
- new Container
+ PlayfieldContentContainer = new Container
{
Name = "Content",
RelativeSizeAxes = Axes.Both,
@@ -116,25 +118,37 @@ namespace osu.Game.Rulesets.Edit
.WithChild(BlueprintContainer = CreateBlueprintContainer())
}
},
- new ExpandingToolboxContainer(90, 200)
+ new Container
{
- Padding = new MarginPadding(10),
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new EditorToolboxGroup("toolbox (1-9)")
+ new Box
{
- Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
},
- new EditorToolboxGroup("toggles (Q~P)")
+ new ExpandingToolboxContainer(60, 200)
{
- Child = togglesCollection = new FillFlowContainer
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 5),
- },
- }
+ new EditorToolboxGroup("toolbox (1-9)")
+ {
+ Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
+ },
+ new EditorToolboxGroup("toggles (Q~P)")
+ {
+ Child = togglesCollection = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5),
+ },
+ }
+ }
+ },
}
},
};
@@ -152,6 +166,15 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
}
+ ///
+ /// Houses all content relevant to the playfield.
+ ///
+ ///
+ /// Generally implementations should not be adding to this directly.
+ /// Use or instead.
+ ///
+ protected Container PlayfieldContentContainer { get; private set; }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index 69937a0fba..d58a901154 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -12,7 +11,6 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices;
@@ -20,6 +18,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
@@ -84,8 +83,6 @@ namespace osu.Game.Rulesets.Mods
flashlight.Combo.BindTo(Combo);
drawableRuleset.KeyBindingInputManager.Add(flashlight);
-
- flashlight.Breaks = drawableRuleset.Beatmap.Breaks;
}
protected abstract Flashlight CreateFlashlight();
@@ -100,8 +97,6 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false;
- public List Breaks = new List();
-
private readonly float defaultFlashlightSize;
private readonly float sizeMultiplier;
private readonly bool comboBasedSize;
@@ -119,37 +114,36 @@ namespace osu.Game.Rulesets.Mods
shader = shaderManager.Load("PositionAndColour", FragmentShader);
}
+ [Resolved]
+ private Player? player { get; set; }
+
+ private readonly IBindable isBreakTime = new BindableBool();
+
protected override void LoadComplete()
{
base.LoadComplete();
- Combo.ValueChanged += OnComboChange;
+ Combo.ValueChanged += _ => UpdateFlashlightSize(GetSize());
- using (BeginAbsoluteSequence(0))
+ if (player != null)
{
- foreach (var breakPeriod in Breaks)
- {
- if (!breakPeriod.HasEffect)
- continue;
-
- if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
-
- this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
- this.Delay(breakPeriod.EndTime - FLASHLIGHT_FADE_DURATION).FadeInFromZero(FLASHLIGHT_FADE_DURATION);
- }
+ isBreakTime.BindTo(player.IsBreakTime);
+ isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
}
}
- protected abstract void OnComboChange(ValueChangedEvent e);
+ protected abstract void UpdateFlashlightSize(float size);
protected abstract string FragmentShader { get; }
- protected float GetSizeFor(int combo)
+ protected float GetSize()
{
float size = defaultFlashlightSize * sizeMultiplier;
- if (comboBasedSize)
- size *= GetComboScaleFor(combo);
+ if (isBreakTime.Value)
+ size *= 2.5f;
+ else if (comboBasedSize)
+ size *= GetComboScaleFor(Combo.Value);
return size;
}
diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
index dec81d9bbd..c2709db747 100644
--- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs
+++ b/osu.Game/Rulesets/Objects/BarLineGenerator.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 System.Collections.Generic;
using System.Linq;
@@ -40,14 +38,21 @@ namespace osu.Game.Rulesets.Objects
for (int i = 0; i < timingPoints.Count; i++)
{
TimingControlPoint currentTimingPoint = timingPoints[i];
+ EffectControlPoint currentEffectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTimingPoint.Time);
int currentBeat = 0;
- // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
- double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
+ // Stop on the next timing point, or if there is no next timing point stop slightly past the last object
+ double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
+ double startTime = currentTimingPoint.Time;
double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
- for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
+ if (currentEffectPoint.OmitFirstBarLine)
+ {
+ startTime += barLength;
+ }
+
+ for (double t = startTime; Precision.AlmostBigger(endTime, t); t += barLength, currentBeat++)
{
double roundedTime = Math.Round(t, MidpointRounding.AwayFromZero);
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index d20e0616e5..0f79e58201 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Newtonsoft.Json;
@@ -198,6 +199,21 @@ namespace osu.Game.Rulesets.Objects
///
[NotNull]
protected virtual HitWindows CreateHitWindows() => new HitWindows();
+
+ public IList CreateSlidingSamples()
+ {
+ var slidingSamples = new List();
+
+ var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
+ if (normalSample != null)
+ slidingSamples.Add(normalSample.With("sliderslide"));
+
+ var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
+ if (whistleSample != null)
+ slidingSamples.Add(whistleSample.With("sliderwhistle"));
+
+ return slidingSamples;
+ }
}
public static class HitObjectExtensions
diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index 1e80bd165b..279de2f940 100644
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -11,12 +11,12 @@ namespace osu.Game.Rulesets.Timing
///
/// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
///
- public class MultiplierControlPoint : IComparable
+ public class MultiplierControlPoint : IComparable, IControlPoint
{
///
/// The time in milliseconds at which this starts.
///
- public double StartTime;
+ public double Time { get; set; }
///
/// The aggregate multiplier which this provides.
@@ -54,13 +54,13 @@ namespace osu.Game.Rulesets.Timing
///
/// Creates a .
///
- /// The start time of this .
- public MultiplierControlPoint(double startTime)
+ /// The start time of this .
+ public MultiplierControlPoint(double time)
{
- StartTime = startTime;
+ Time = time;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
- public int CompareTo(MultiplierControlPoint other) => StartTime.CompareTo(other?.StartTime);
+ public int CompareTo(MultiplierControlPoint other) => Time.CompareTo(other?.Time);
}
}
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 73acb1759f..dd3a950264 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -384,7 +384,7 @@ namespace osu.Game.Rulesets.UI
// only show the cursor when within the playfield, by default.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
- CursorContainer IProvideCursor.MenuCursor => Playfield.Cursor;
+ CursorContainer IProvideCursor.Cursor => Playfield.Cursor;
public override GameplayCursorContainer Cursor => Playfield.Cursor;
@@ -499,6 +499,7 @@ namespace osu.Game.Rulesets.UI
///
/// The cursor being displayed by the . May be null if no cursor is provided.
///
+ [CanBeNull]
public abstract GameplayCursorContainer Cursor { get; }
///
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 2ec72d8fe3..e59e45722a 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -202,16 +202,14 @@ namespace osu.Game.Rulesets.UI
///
/// The cursor currently being used by this . May be null if no cursor is provided.
///
+ [CanBeNull]
public GameplayCursorContainer Cursor { get; private set; }
///
/// Provide a cursor which is to be used for gameplay.
///
- ///
- /// The default provided cursor is invisible when inside the bounds of the .
- ///
/// The cursor, or null to show the menu cursor.
- protected virtual GameplayCursorContainer CreateCursor() => new InvisibleCursorContainer();
+ protected virtual GameplayCursorContainer CreateCursor() => null;
///
/// Registers a as a nested .
@@ -522,14 +520,5 @@ namespace osu.Game.Rulesets.UI
}
#endregion
-
- public class InvisibleCursorContainer : GameplayCursorContainer
- {
- protected override Drawable CreateCursor() => new InvisibleCursor();
-
- private class InvisibleCursor : Drawable
- {
- }
- }
}
}
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 1a97153f2f..64ac021204 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -230,9 +230,9 @@ namespace osu.Game.Rulesets.UI
{
}
- protected override void ReloadMappings()
+ protected override void ReloadMappings(IQueryable realmKeyBindings)
{
- base.ReloadMappings();
+ base.ReloadMappings(realmKeyBindings);
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
index 0bd8aa64c9..c957a84eb1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return -PositionAt(startTime, endTime, timeRange, scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
=> (float)((time - currentTime) / timeRange * scrollLength);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
index d2fb9e3531..f78509f919 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
@@ -53,8 +53,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The current time.
/// The amount of visible time.
/// The absolute spatial length through .
+ /// The time to be used for control point lookups (ie. the parent's start time for nested hit objects).
/// The absolute spatial position.
- float PositionAt(double time, double currentTime, double timeRange, float scrollLength);
+ float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null);
///
/// Computes the time which brings a point to a provided spatial position given the current time.
@@ -63,7 +64,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The current time.
/// The amount of visible time.
/// The absolute spatial length through .
- /// The time at which == .
+ /// The time at which == .
double TimeAt(float position, double currentTime, double timeRange, float scrollLength);
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
index d41117bce8..54079c7895 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
@@ -4,22 +4,20 @@
#nullable disable
using System;
+using System.Linq;
using osu.Framework.Lists;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
{
public class OverlappingScrollAlgorithm : IScrollAlgorithm
{
- private readonly MultiplierControlPoint searchPoint;
-
private readonly SortedList controlPoints;
public OverlappingScrollAlgorithm(SortedList controlPoints)
{
this.controlPoints = controlPoints;
-
- searchPoint = new MultiplierControlPoint();
}
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
@@ -37,8 +35,8 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return -PositionAt(startTime, endTime, timeRange, scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength);
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
+ => (float)((time - currentTime) / timeRange * controlPointAt(originTime ?? time).Multiplier * scrollLength);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
{
@@ -52,7 +50,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
for (; i < controlPoints.Count; i++)
{
float lastPos = pos;
- pos = PositionAt(controlPoints[i].StartTime, currentTime, timeRange, scrollLength);
+ pos = PositionAt(controlPoints[i].Time, currentTime, timeRange, scrollLength);
if (pos > position)
{
@@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
i = Math.Clamp(i, 0, controlPoints.Count - 1);
- return controlPoints[i].StartTime + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
+ return controlPoints[i].Time + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
}
public void Reset()
@@ -78,19 +76,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The .
private MultiplierControlPoint controlPointAt(double time)
{
- if (controlPoints.Count == 0)
- return new MultiplierControlPoint(double.NegativeInfinity);
-
- if (time < controlPoints[0].StartTime)
- return controlPoints[0];
-
- searchPoint.StartTime = time;
- int index = controlPoints.BinarySearch(searchPoint);
-
- if (index < 0)
- index = ~index - 1;
-
- return controlPoints[index];
+ return ControlPointInfo.BinarySearch(controlPoints, time)
+ // The standard binary search will fail if there's no control points, or if the time is before the first.
+ // For this method, we want to use the first control point in the latter case.
+ ?? controlPoints.FirstOrDefault()
+ ?? new MultiplierControlPoint(double.NegativeInfinity);
}
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
index bfddc22573..774beb20c7 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return (float)(objectLength * scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
{
double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange);
return (float)(timelineLength * scrollLength);
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
if (controlPoints.Count == 0)
return;
- positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0]));
+ positionMappings.Add(new PositionMapping(controlPoints[0].Time, controlPoints[0]));
for (int i = 0; i < controlPoints.Count - 1; i++)
{
@@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
var next = controlPoints[i + 1];
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
- float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier);
+ float length = (float)((next.Time - current.Time) / timeRange * current.Multiplier);
- positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length));
+ positionMappings.Add(new PositionMapping(next.Time, next, positionMappings[^1].Position + length));
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 825aba5bc2..68469d083c 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -158,9 +158,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Trim unwanted sequences of timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
- .Where(s => s.StartTime <= lastObjectTime)
+ .Where(s => s.Time <= lastObjectTime)
// Collapse sections with the same start time
- .GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
+ .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time);
ControlPoints.AddRange(timingChanges);
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 37da157cc1..424fc7c44c 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -93,9 +93,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
/// Given a time, return the position along the scrolling axis within this at time .
///
- public float PositionAtTime(double time, double currentTime)
+ public float PositionAtTime(double time, double currentTime, double? originTime = null)
{
- float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
+ float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength, originTime);
return axisInverted ? -scrollPosition : scrollPosition;
}
@@ -236,8 +236,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
}
- private void updateLayoutRecursive(DrawableHitObject hitObject)
+ private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
{
+ parentHitObjectStartTime ??= hitObject.HitObject.StartTime;
+
if (hitObject.HitObject is IHasDuration e)
{
float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime);
@@ -249,17 +251,17 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in hitObject.NestedHitObjects)
{
- updateLayoutRecursive(obj);
+ updateLayoutRecursive(obj, parentHitObjectStartTime);
// Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
- updatePosition(obj, hitObject.HitObject.StartTime);
+ updatePosition(obj, hitObject.HitObject.StartTime, parentHitObjectStartTime);
setComputedLifetimeStart(obj.Entry);
}
}
- private void updatePosition(DrawableHitObject hitObject, double currentTime)
+ private void updatePosition(DrawableHitObject hitObject, double currentTime, double? parentHitObjectStartTime = null)
{
- float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
+ float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime, parentHitObjectStartTime);
if (scrollingAxis == Direction.Horizontal)
hitObject.X = position;
diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs
index 82e94dc862..071bb9fdcb 100644
--- a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs
+++ b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs
@@ -8,13 +8,12 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -30,9 +29,9 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
public readonly RadioButton Button;
private Color4 defaultBackgroundColour;
- private Color4 defaultBubbleColour;
+ private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
- private Color4 selectedBubbleColour;
+ private Color4 selectedIconColour;
private Drawable icon;
@@ -50,20 +49,13 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
- defaultBackgroundColour = colours.Gray3;
- defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
- selectedBackgroundColour = colours.BlueDark;
- selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
+ defaultBackgroundColour = colourProvider.Background3;
+ selectedBackgroundColour = colourProvider.Background1;
- Content.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 2,
- Offset = new Vector2(0, 1),
- Colour = Color4.Black.Opacity(0.5f)
- };
+ defaultIconColour = defaultBackgroundColour.Darken(0.5f);
+ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
@@ -98,7 +90,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
return;
BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
- icon.Colour = Button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
+ icon.Colour = Button.Selected.Value ? selectedIconColour : defaultIconColour;
}
protected override SpriteText CreateText() => new OsuSpriteText
diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
index 55302833c1..1fb5c0285d 100644
--- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
+++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
@@ -6,12 +6,11 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -20,9 +19,9 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
internal class DrawableTernaryButton : OsuButton
{
private Color4 defaultBackgroundColour;
- private Color4 defaultBubbleColour;
+ private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
- private Color4 selectedBubbleColour;
+ private Color4 selectedIconColour;
private Drawable icon;
@@ -38,20 +37,13 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
- defaultBackgroundColour = colours.Gray3;
- defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
- selectedBackgroundColour = colours.BlueDark;
- selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
+ defaultBackgroundColour = colourProvider.Background3;
+ selectedBackgroundColour = colourProvider.Background1;
- Content.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 2,
- Offset = new Vector2(0, 1),
- Colour = Color4.Black.Opacity(0.5f)
- };
+ defaultIconColour = defaultBackgroundColour.Darken(0.5f);
+ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
@@ -85,17 +77,17 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
- icon.Colour = selectedBubbleColour.Darken(0.5f);
+ icon.Colour = selectedIconColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
- icon.Colour = defaultBubbleColour;
+ icon.Colour = defaultIconColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
- icon.Colour = selectedBubbleColour;
+ icon.Colour = selectedIconColour;
BackgroundColour = selectedBackgroundColour;
break;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index 40403e08ad..19ea2162a3 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -123,16 +123,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
},
new Drawable[]
- {
- new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
- {
- Padding = new MarginPadding { Horizontal = 15 },
- Text = "beat snap",
- RelativeSizeAxes = Axes.X,
- TextAnchor = Anchor.TopCentre
- },
- },
- new Drawable[]
{
new Container
{
@@ -173,6 +163,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
},
+ new Drawable[]
+ {
+ new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
+ {
+ Padding = new MarginPadding { Horizontal = 15, Vertical = 8 },
+ Text = "beat snap",
+ RelativeSizeAxes = Axes.X,
+ TextAnchor = Anchor.TopCentre,
+ },
+ },
},
RowDimensions = new[]
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 8b38d9c612..43ad270c16 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -3,7 +3,6 @@
#nullable disable
-using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
@@ -13,7 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -61,25 +59,31 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
case NotifyCollectionChangedAction.Add:
foreach (object o in args.NewItems)
- SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
+ {
+ if (blueprintMap.TryGetValue((T)o, out var blueprint))
+ blueprint.Select();
+ }
break;
case NotifyCollectionChangedAction.Remove:
foreach (object o in args.OldItems)
- SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
+ {
+ if (blueprintMap.TryGetValue((T)o, out var blueprint))
+ blueprint.Deselect();
+ }
break;
}
};
SelectionHandler = CreateSelectionHandler();
- SelectionHandler.DeselectAll = deselectAll;
+ SelectionHandler.DeselectAll = DeselectAll;
SelectionHandler.SelectedItems.BindTo(SelectedItems);
AddRangeInternal(new[]
{
- DragBox = CreateDragBox(selectBlueprintsFromDragRectangle),
+ DragBox = CreateDragBox(),
SelectionHandler,
SelectionBlueprints = CreateSelectionBlueprintContainer(),
SelectionHandler.CreateProxy(),
@@ -101,12 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[CanBeNull]
protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null;
- protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect);
-
- ///
- /// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to.
- ///
- protected virtual bool AllowDeselectionDuringDrag => true;
+ protected virtual DragBox CreateDragBox() => new DragBox();
protected override bool OnMouseDown(MouseDownEvent e)
{
@@ -142,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (endClickSelection(e) || ClickedBlueprint != null)
return true;
- deselectAll();
+ DeselectAll();
return true;
}
@@ -171,11 +170,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
finishSelectionMovement();
}
+ private MouseButtonEvent lastDragEvent;
+
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button == MouseButton.Right)
return false;
+ lastDragEvent = e;
+
if (movementBlueprints != null)
{
isDraggingBlueprint = true;
@@ -183,30 +186,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true;
}
- if (DragBox.HandleDrag(e))
- {
- DragBox.Show();
- return true;
- }
-
- return false;
+ DragBox.HandleDrag(e);
+ DragBox.Show();
+ return true;
}
protected override void OnDrag(DragEvent e)
{
- if (e.Button == MouseButton.Right)
- return;
-
- if (DragBox.State == Visibility.Visible)
- DragBox.HandleDrag(e);
+ lastDragEvent = e;
moveCurrentSelection(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
- if (e.Button == MouseButton.Right)
- return;
+ lastDragEvent = null;
if (isDraggingBlueprint)
{
@@ -214,8 +208,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
changeHandler?.EndChange();
}
- if (DragBox.State == Visibility.Visible)
- DragBox.Hide();
+ DragBox.Hide();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (lastDragEvent != null && DragBox.State == Visibility.Visible)
+ {
+ lastDragEvent.Target = this;
+ DragBox.HandleDrag(lastDragEvent);
+ UpdateSelectionFromDragBox();
+ }
}
///
@@ -233,7 +238,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!SelectionHandler.SelectedBlueprints.Any())
return false;
- deselectAll();
+ DeselectAll();
return true;
}
@@ -380,44 +385,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
///
- /// Select all masks in a given rectangle selection area.
+ /// Select all blueprints in a selection area specified by .
///
- /// The rectangle to perform a selection on in screen-space coordinates.
- private void selectBlueprintsFromDragRectangle(RectangleF rect)
+ protected virtual void UpdateSelectionFromDragBox()
{
+ var quad = DragBox.Box.ScreenSpaceDrawQuad;
+
foreach (var blueprint in SelectionBlueprints)
{
- // only run when utmost necessary to avoid unnecessary rect computations.
- bool isValidForSelection() => blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint);
-
switch (blueprint.State)
{
- case SelectionState.NotSelected:
- if (isValidForSelection())
- blueprint.Select();
+ case SelectionState.Selected:
+ // Selection is preserved even after blueprint becomes dead.
+ if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint))
+ blueprint.Deselect();
break;
- case SelectionState.Selected:
- if (AllowDeselectionDuringDrag && !isValidForSelection())
- blueprint.Deselect();
+ case SelectionState.NotSelected:
+ if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint))
+ blueprint.Select();
break;
}
}
}
///
- /// Selects all s.
+ /// Select all currently-present items.
///
- protected virtual void SelectAll()
- {
- // Scheduled to allow the change in lifetime to take place.
- Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select()));
- }
+ protected abstract void SelectAll();
///
- /// Deselects all selected s.
+ /// Deselect all selected items.
///
- private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
+ protected void DeselectAll() => SelectedItems.Clear();
protected virtual void OnBlueprintSelected(SelectionBlueprint blueprint)
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 4c37d200bc..ec07da43a0 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -12,7 +12,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
@@ -37,7 +36,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
private PlacementBlueprint currentPlacement;
- private InputManager inputManager;
///
/// Positional input must be received outside the container's bounds,
@@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
- inputManager = GetContainingInputManager();
-
Beatmap.HitObjectAdded += hitObjectAdded;
// updates to selected are handled for us by SelectionHandler.
@@ -220,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementPosition()
{
- var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
+ var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position);
// if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
index 838562719d..905d47533a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
@@ -8,7 +8,6 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
@@ -21,18 +20,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
public class DragBox : CompositeDrawable, IStateful
{
- protected readonly Action PerformSelection;
-
- protected Drawable Box;
+ public Drawable Box { get; private set; }
///
/// Creates a new .
///
- /// A delegate that performs drag selection.
- public DragBox(Action performSelection)
+ public DragBox()
{
- PerformSelection = performSelection;
-
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
@@ -46,30 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual Drawable CreateBox() => new BoxWithBorders();
- private RectangleF? dragRectangle;
-
///
/// Handle a forwarded mouse event.
///
/// The mouse event.
- /// Whether the event should be handled and blocking.
- public virtual bool HandleDrag(MouseButtonEvent e)
+ public virtual void HandleDrag(MouseButtonEvent e)
{
- var dragPosition = e.ScreenSpaceMousePosition;
- var dragStartPosition = e.ScreenSpaceMouseDownPosition;
-
- var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
-
- // We use AABBFloat instead of RectangleF since it handles negative sizes for us
- var rec = dragQuad.AABBFloat;
- dragRectangle = rec;
-
- var topLeft = ToLocalSpace(rec.TopLeft);
- var bottomRight = ToLocalSpace(rec.BottomRight);
-
- Box.Position = topLeft;
- Box.Size = bottomRight - topLeft;
- return true;
+ Box.Position = Vector2.ComponentMin(e.MouseDownPosition, e.MousePosition);
+ Box.Size = Vector2.ComponentMax(e.MouseDownPosition, e.MousePosition) - Box.Position;
}
private Visibility state;
@@ -87,19 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
- protected override void Update()
- {
- base.Update();
-
- if (dragRectangle != null)
- PerformSelection?.Invoke(dragRectangle.Value);
- }
-
- public override void Hide()
- {
- State = Visibility.Hidden;
- dragRectangle = null;
- }
+ public override void Hide() => State = Visibility.Hidden;
public override void Show() => State = Visibility.Visible;
diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs
index 6a4fe27f04..7423b368b4 100644
--- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs
@@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -27,6 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private HitObjectUsageEventBuffer usageEventBuffer;
+ protected InputManager InputManager { get; private set; }
+
protected EditorBlueprintContainer(HitObjectComposer composer)
{
Composer = composer;
@@ -42,6 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
+ InputManager = GetContainingInputManager();
+
Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
@@ -66,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints)
=> blueprints.OrderBy(b => b.Item.StartTime);
- protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning;
-
protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result)
{
if (!base.ApplySnapResult(blueprints, result))
@@ -133,8 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void SelectAll()
{
Composer.Playfield.KeepAllAlive();
-
- base.SelectAll();
+ SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray());
}
protected override void OnBlueprintSelected(SelectionBlueprint blueprint)
diff --git a/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs
new file mode 100644
index 0000000000..58bfaf56ff
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs
@@ -0,0 +1,64 @@
+// 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.Input.Events;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Screens.Edit.Compose.Components
+{
+ ///
+ /// A that scrolls along with the scrolling playfield.
+ ///
+ public class ScrollingDragBox : DragBox
+ {
+ public double MinTime { get; private set; }
+
+ public double MaxTime { get; private set; }
+
+ private double? startTime;
+
+ private readonly ScrollingPlayfield playfield;
+
+ public ScrollingDragBox(Playfield playfield)
+ {
+ this.playfield = playfield as ScrollingPlayfield ?? throw new ArgumentException("Playfield must be of type {nameof(ScrollingPlayfield)} to use this class.", nameof(playfield));
+ }
+
+ public override void HandleDrag(MouseButtonEvent e)
+ {
+ base.HandleDrag(e);
+
+ startTime ??= playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMouseDownPosition);
+ double endTime = playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMousePosition);
+
+ MinTime = Math.Min(startTime.Value, endTime);
+ MaxTime = Math.Max(startTime.Value, endTime);
+
+ var startPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(startTime.Value));
+ var endPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(endTime));
+
+ switch (playfield.ScrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ case ScrollingDirection.Down:
+ Box.Y = Math.Min(startPos.Y, endPos.Y);
+ Box.Height = Math.Max(startPos.Y, endPos.Y) - Box.Y;
+ break;
+
+ case ScrollingDirection.Left:
+ case ScrollingDirection.Right:
+ Box.X = Math.Min(startPos.X, endPos.X);
+ Box.Width = Math.Max(startPos.X, endPos.X) - Box.X;
+ break;
+ }
+ }
+
+ public override void Hide()
+ {
+ base.Hide();
+ startTime = null;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 8419d3b380..269c19f846 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -305,7 +305,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected void DeleteSelected()
{
- DeleteItems(selectedBlueprints.Select(b => b.Item));
+ DeleteItems(SelectedItems.ToArray());
}
#endregion
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 721f0c4e3b..a73ada76f5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -304,10 +304,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
///
public double VisibleRange => editorClock.TrackLength / Zoom;
- public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
- new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
+ public double TimeAtPosition(float x)
+ {
+ return x / Content.DrawWidth * editorClock.TrackLength;
+ }
- private double getTimeFromPosition(Vector2 localPosition) =>
- (localPosition.X / Content.DrawWidth) * editorClock.TrackLength;
+ public float PositionAtTime(double time)
+ {
+ return (float)(time / editorClock.TrackLength * Content.DrawWidth);
+ }
+
+ public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
+ {
+ double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
+ return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
index c2415ce978..58d378154a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
@@ -78,16 +78,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
LabelText = "Waveform",
Current = { Value = true },
},
- controlPointsCheckbox = new OsuCheckbox
- {
- LabelText = "Control Points",
- Current = { Value = true },
- },
ticksCheckbox = new OsuCheckbox
{
LabelText = "Ticks",
Current = { Value = true },
- }
+ },
+ controlPointsCheckbox = new OsuCheckbox
+ {
+ LabelText = "BPM",
+ Current = { Value = true },
+ },
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 590f92d281..b79c2675c8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -3,7 +3,6 @@
#nullable disable
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -13,7 +12,6 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
@@ -31,10 +29,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(CanBeNull = true)]
private Timeline timeline { get; set; }
- private DragEvent lastDragEvent;
private Bindable placement;
private SelectionBlueprint placementBlueprint;
+ private bool hitObjectDragged;
+
///
/// Positional input must be received outside the container's bounds,
/// in order to handle timeline blueprints which are stacked offscreen.
@@ -65,7 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete()
{
base.LoadComplete();
- DragBox.Alpha = 0;
placement = Beatmap.PlacementObject.GetBoundCopy();
placement.ValueChanged += placementChanged;
@@ -93,24 +91,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
- protected override void OnDrag(DragEvent e)
+ protected override bool OnDragStart(DragStartEvent e)
{
- handleScrollViaDrag(e);
+ if (!base.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition))
+ return false;
- base.OnDrag(e);
- }
-
- protected override void OnDragEnd(DragEndEvent e)
- {
- base.OnDragEnd(e);
- lastDragEvent = null;
+ return base.OnDragStart(e);
}
protected override void Update()
{
- // trigger every frame so drags continue to update selection while playback is scrolling the timeline.
- if (lastDragEvent != null)
- OnDrag(lastDragEvent);
+ if (IsDragged || hitObjectDragged)
+ handleScrollViaDrag();
if (Composer != null && timeline != null)
{
@@ -165,30 +157,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
return new TimelineHitObjectBlueprint(item)
{
- OnDragHandled = handleScrollViaDrag,
+ OnDragHandled = e => hitObjectDragged = e != null,
};
}
- protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect);
+ protected sealed override DragBox CreateDragBox() => new TimelineDragBox();
- private void handleScrollViaDrag(DragEvent e)
+ protected override void UpdateSelectionFromDragBox()
{
- lastDragEvent = e;
+ var dragBox = (TimelineDragBox)DragBox;
+ double minTime = dragBox.MinTime;
+ double maxTime = dragBox.MaxTime;
- if (lastDragEvent == null)
- return;
+ SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject));
- if (timeline != null)
+ foreach (var hitObject in Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected))
{
- var timelineQuad = timeline.ScreenSpaceDrawQuad;
- float mouseX = e.ScreenSpaceMousePosition.X;
-
- // scroll if in a drag and dragging outside visible extents
- if (mouseX > timelineQuad.TopRight.X)
- timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
- else if (mouseX < timelineQuad.TopLeft.X)
- timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
+ Composer.Playfield.SetKeepAlive(hitObject, true);
+ SelectedItems.Add(hitObject);
}
+
+ bool shouldBeSelected(HitObject hitObject)
+ {
+ double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2;
+ return minTime <= midTime && midTime <= maxTime;
+ }
+ }
+
+ private void handleScrollViaDrag()
+ {
+ if (timeline == null) return;
+
+ var timelineQuad = timeline.ScreenSpaceDrawQuad;
+ float mouseX = InputManager.CurrentState.Mouse.Position.X;
+
+ // scroll if in a drag and dragging outside visible extents
+ if (mouseX > timelineQuad.TopRight.X)
+ timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
+ else if (mouseX < timelineQuad.TopLeft.X)
+ timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
}
private class SelectableAreaBackground : CompositeDrawable
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs
index c026c169d6..65d9293b7e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs
@@ -6,76 +6,44 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
-using osu.Framework.Utils;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineDragBox : DragBox
{
- // the following values hold the start and end X positions of the drag box in the timeline's local space,
- // but with zoom unapplied in order to be able to compensate for positional changes
- // while the timeline is being zoomed in/out.
- private float? selectionStart;
- private float selectionEnd;
+ public double MinTime { get; private set; }
+
+ public double MaxTime { get; private set; }
+
+ private double? startTime;
[Resolved]
private Timeline timeline { get; set; }
- public TimelineDragBox(Action performSelect)
- : base(performSelect)
- {
- }
-
protected override Drawable CreateBox() => new Box
{
RelativeSizeAxes = Axes.Y,
Alpha = 0.3f
};
- public override bool HandleDrag(MouseButtonEvent e)
+ public override void HandleDrag(MouseButtonEvent e)
{
- // The dragbox should only be active if the mouseDownPosition.Y is within this drawable's bounds.
- float localY = ToLocalSpace(e.ScreenSpaceMouseDownPosition).Y;
- if (DrawRectangle.Top > localY || DrawRectangle.Bottom < localY)
- return false;
+ startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X);
+ double endTime = timeline.TimeAtPosition(e.MousePosition.X);
- selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom;
+ MinTime = Math.Min(startTime.Value, endTime);
+ MaxTime = Math.Max(startTime.Value, endTime);
- // only calculate end when a transition is not in progress to avoid bouncing.
- if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom))
- selectionEnd = e.MousePosition.X / timeline.CurrentZoom;
-
- updateDragBoxPosition();
- return true;
- }
-
- private void updateDragBoxPosition()
- {
- if (selectionStart == null)
- return;
-
- float rescaledStart = selectionStart.Value * timeline.CurrentZoom;
- float rescaledEnd = selectionEnd * timeline.CurrentZoom;
-
- Box.X = Math.Min(rescaledStart, rescaledEnd);
- Box.Width = Math.Abs(rescaledStart - rescaledEnd);
-
- var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat;
-
- // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment.
- boxScreenRect.Y -= boxScreenRect.Height;
- boxScreenRect.Height *= 2;
-
- PerformSelection?.Invoke(boxScreenRect);
+ Box.X = timeline.PositionAtTime(MinTime);
+ Box.Width = timeline.PositionAtTime(MaxTime) - Box.X;
}
public override void Hide()
{
base.Hide();
- selectionStart = null;
+ startTime = null;
}
}
}
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 16c0064e80..839535b99f 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -352,6 +352,8 @@ namespace osu.Game.Screens.Edit
var updates = batchPendingUpdates.ToArray();
batchPendingUpdates.Clear();
+ foreach (var h in deletes) SelectedHitObjects.Remove(h);
+
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs
index 7d8657a3df..a5739a41b1 100644
--- a/osu.Game/Screens/IOsuScreen.cs
+++ b/osu.Game/Screens/IOsuScreen.cs
@@ -41,6 +41,11 @@ namespace osu.Game.Screens
///
bool HideOverlaysOnEnter { get; }
+ ///
+ /// Whether the menu cursor should be hidden when non-mouse input is received.
+ ///
+ bool HideMenuCursorOnNonMouseInput { get; }
+
///
/// Whether overlays should be able to be opened when this screen is current.
///
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index 409c7d6c8d..dcead4a3a8 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -278,11 +278,11 @@ namespace osu.Game.Screens.Menu
if (!UsingThemedIntro)
{
- initialBeatmap?.PrepareTrackForPreview(false);
+ initialBeatmap?.PrepareTrackForPreview(false, -2600);
drawableTrack.VolumeTo(0);
drawableTrack.Restart();
- drawableTrack.VolumeTo(1, 2200, Easing.InCubic);
+ drawableTrack.VolumeTo(1, 2600, Easing.InCubic);
}
else
{
diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs
index 9e56a3a0b7..5ae2158172 100644
--- a/osu.Game/Screens/Menu/IntroWelcome.cs
+++ b/osu.Game/Screens/Menu/IntroWelcome.cs
@@ -78,13 +78,17 @@ namespace osu.Game.Screens.Menu
if (reverbChannel != null)
intro.LogoVisualisation.AddAmplitudeSource(reverbChannel);
- Scheduler.AddDelayed(() =>
- {
+ if (!UsingThemedIntro)
StartTrack();
- // this classic intro loops forever.
+ Scheduler.AddDelayed(() =>
+ {
if (UsingThemedIntro)
+ {
+ StartTrack();
+ // this classic intro loops forever.
Track.Looping = true;
+ }
const float fade_in_time = 200;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
index 39740e650f..ba6b482729 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
@@ -78,9 +78,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
return;
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
+ bool isValidItem = isItemOwner && !Item.Expired;
- AllowDeletion = isItemOwner && !Item.Expired && Item.ID != multiplayerClient.Room.Settings.PlaylistItemId;
- AllowEditing = isItemOwner && !Item.Expired;
+ AllowDeletion = isValidItem
+ && (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check.
+ || multiplayerClient.Room.Playlist.Count(i => !i.Expired) > 1);
+
+ AllowEditing = isValidItem;
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
index 41633c34ce..27193d3cb6 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
@@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// An optional pivot around which the scores were retrieved.
private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() =>
{
- var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
+ var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index 0678a90f71..6be13bbda3 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -40,11 +40,10 @@ namespace osu.Game.Screens
public virtual bool AllowExternalScreenChange => false;
- ///
- /// Whether all overlays should be hidden when this screen is entered or resumed.
- ///
public virtual bool HideOverlaysOnEnter => false;
+ public virtual bool HideMenuCursorOnNonMouseInput => false;
+
///
/// The initial overlay activation mode to use when this screen is entered for the first time.
///
diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs
new file mode 100644
index 0000000000..4179d41646
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/ComboCounter.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 System;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public abstract class ComboCounter : RollingCounter, ISkinnableDrawable
+ {
+ public bool UsesFixedAnchor { get; set; }
+
+ protected ComboCounter()
+ {
+ Current.Value = DisplayedCount = 0;
+ }
+
+ protected override double GetProportionalDuration(int currentValue, int newValue)
+ {
+ return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
index 1f14811169..0c9c363280 100644
--- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
@@ -1,29 +1,17 @@
// 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.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
- public class DefaultComboCounter : RollingCounter, ISkinnableDrawable
+ public class DefaultComboCounter : ComboCounter
{
- public bool UsesFixedAnchor { get; set; }
-
- public DefaultComboCounter()
- {
- Current.Value = DisplayedCount = 0;
- }
-
[BackgroundDependencyLoader]
private void load(OsuColour colours, ScoreProcessor scoreProcessor)
{
@@ -31,17 +19,12 @@ namespace osu.Game.Screens.Play.HUD
Current.BindTo(scoreProcessor.Combo);
}
+ protected override OsuSpriteText CreateSpriteText()
+ => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
+
protected override LocalisableString FormatCount(int count)
{
return $@"{count}x";
}
-
- protected override double GetProportionalDuration(int currentValue, int newValue)
- {
- return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
- }
-
- protected override OsuSpriteText CreateSpriteText()
- => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
}
}
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
index 747f4d4a8a..d6b9c62369 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -44,8 +45,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
public Bindable LabelStyle { get; } = new Bindable(LabelStyles.Icons);
private SpriteIcon arrow;
- private Drawable labelEarly;
- private Drawable labelLate;
+ private UprightAspectMaintainingContainer labelEarly;
+ private UprightAspectMaintainingContainer labelLate;
private Container colourBarsEarly;
private Container colourBarsLate;
@@ -122,6 +123,20 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
RelativeSizeAxes = Axes.Y,
Width = judgement_line_width,
},
+ labelEarly = new UprightAspectMaintainingContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Y = -10,
+ },
+ labelLate = new UprightAspectMaintainingContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ Y = 10,
+ },
}
},
arrowContainer = new Container
@@ -261,57 +276,39 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
const float icon_size = 14;
- labelEarly?.Expire();
- labelEarly = null;
-
- labelLate?.Expire();
- labelLate = null;
-
switch (style)
{
case LabelStyles.None:
break;
case LabelStyles.Icons:
- labelEarly = new SpriteIcon
+ labelEarly.Child = new SpriteIcon
{
- Y = -10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.ShippingFast,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.Centre,
};
- labelLate = new SpriteIcon
+ labelLate.Child = new SpriteIcon
{
- Y = 10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.Bicycle,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.Centre,
};
break;
case LabelStyles.Text:
- labelEarly = new OsuSpriteText
+ labelEarly.Child = new OsuSpriteText
{
- Y = -10,
Text = "Early",
Font = OsuFont.Default.With(size: 10),
Height = 12,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.Centre,
};
- labelLate = new OsuSpriteText
+ labelLate.Child = new OsuSpriteText
{
- Y = 10,
Text = "Late",
Font = OsuFont.Default.With(size: 10),
Height = 12,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.Centre,
};
break;
@@ -320,26 +317,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
throw new ArgumentOutOfRangeException(nameof(style), style, null);
}
- if (labelEarly != null)
- {
- colourBars.Add(labelEarly);
- labelEarly.FadeInFromZero(500);
- }
-
- if (labelLate != null)
- {
- colourBars.Add(labelLate);
- labelLate.FadeInFromZero(500);
- }
- }
-
- protected override void Update()
- {
- base.Update();
-
- // undo any layout rotation to display icons in the correct orientation
- if (labelEarly != null) labelEarly.Rotation = -Rotation;
- if (labelLate != null) labelLate.Rotation = -Rotation;
+ labelEarly.FadeInFromZero(500);
+ labelLate.FadeInFromZero(500);
}
private void createColourBars((HitResult result, double length)[] windows)
diff --git a/osu.Game/Screens/Play/HUD/LongestComboCounter.cs b/osu.Game/Screens/Play/HUD/LongestComboCounter.cs
new file mode 100644
index 0000000000..0e7af69af2
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/LongestComboCounter.cs
@@ -0,0 +1,83 @@
+// 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.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class LongestComboCounter : ComboCounter
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, ScoreProcessor scoreProcessor)
+ {
+ Colour = colours.YellowLighter;
+ Current.BindTo(scoreProcessor.HighestCombo);
+ }
+
+ protected override IHasText CreateText() => new TextComponent();
+
+ private class TextComponent : CompositeDrawable, IHasText
+ {
+ public LocalisableString Text
+ {
+ get => text.Text;
+ set => text.Text = $"{value}x";
+ }
+
+ private readonly OsuSpriteText text;
+
+ public TextComponent()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChild = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(2),
+ Children = new Drawable[]
+ {
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Font = OsuFont.Numeric.With(size: 20)
+ },
+ new FillFlowContainer
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Font = OsuFont.Numeric.With(size: 8),
+ Text = @"longest",
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Font = OsuFont.Numeric.With(size: 8),
+ Text = @"combo",
+ Padding = new MarginPadding { Bottom = 3f }
+ }
+ }
+ }
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 3fbb051c3b..7833c2d7fa 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -39,6 +39,10 @@ namespace osu.Game.Screens.Play
///
public float BottomScoringElementsHeight { get; private set; }
+ // HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
+ // Without blocking input, this would also allow them to be interacted with in such a state.
+ public override bool PropagatePositionalInputSubTree => ShowHud.Value;
+
public readonly KeyCounterDisplay KeyCounter;
public readonly ModDisplay ModDisplay;
public readonly HoldForMenuButton HoldToQuit;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 7721d5b912..7048f83c09 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -66,6 +66,8 @@ namespace osu.Game.Screens.Play
public override bool HideOverlaysOnEnter => true;
+ public override bool HideMenuCursorOnNonMouseInput => true;
+
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
// We are managing our own adjustments (see OnEntering/OnExiting).
@@ -94,6 +96,11 @@ namespace osu.Game.Screens.Play
public int RestartCount;
+ ///
+ /// Whether the is currently visible.
+ ///
+ public IBindable ShowingOverlayComponents = new Bindable();
+
[Resolved]
private ScoreManager scoreManager { get; set; }
@@ -1015,6 +1022,8 @@ namespace osu.Game.Screens.Play
});
HUDOverlay.IsPlaying.BindTo(localUserPlaying);
+ ShowingOverlayComponents.BindTo(HUDOverlay.ShowHud);
+
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index e32d3d90be..4ff5083107 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -64,6 +64,8 @@ namespace osu.Game.Screens.Play
protected Task? DisposalTask { get; private set; }
+ private OsuScrollContainer settingsScroll = null!;
+
private bool backgroundBrightnessReduction;
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
@@ -71,6 +73,9 @@ namespace osu.Game.Screens.Play
private AudioFilter lowPassFilter = null!;
private AudioFilter highPassFilter = null!;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
protected bool BackgroundBrightnessReduction
{
set
@@ -165,30 +170,30 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- new OsuScrollContainer
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
- Padding = new MarginPadding { Vertical = padding },
- Masking = false,
- Child = PlayerSettings = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Padding = new MarginPadding { Horizontal = padding },
- Children = new PlayerSettingsGroup[]
- {
- VisualSettings = new VisualSettings(),
- AudioSettings = new AudioSettings(),
- new InputSettings()
- }
- },
- },
- idleTracker = new IdleTracker(750),
}),
+ settingsScroll = new OsuScrollContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
+ Padding = new MarginPadding { Vertical = padding },
+ Masking = false,
+ Child = PlayerSettings = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Padding = new MarginPadding { Horizontal = padding },
+ Children = new PlayerSettingsGroup[]
+ {
+ VisualSettings = new VisualSettings(),
+ AudioSettings = new AudioSettings(),
+ new InputSettings()
+ }
+ },
+ },
+ idleTracker = new IdleTracker(750),
lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass)
};
@@ -224,6 +229,9 @@ namespace osu.Game.Screens.Play
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
+ // Start off-screen.
+ settingsScroll.MoveToX(settingsScroll.DrawWidth);
+
content.ScaleTo(0.7f);
contentIn();
@@ -313,6 +321,16 @@ namespace osu.Game.Screens.Play
content.StopTracking();
}
+ protected override void LogoSuspending(OsuLogo logo)
+ {
+ base.LogoSuspending(logo);
+ content.StopTracking();
+
+ logo
+ .FadeOut(CONTENT_OUT_DURATION / 2, Easing.OutQuint)
+ .ScaleTo(logo.Scale * 0.8f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
+ }
+
#endregion
protected override void Update()
@@ -391,6 +409,10 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
+
+ settingsScroll.FadeInFromZero(500, Easing.Out)
+ .MoveToX(0, 500, Easing.OutQuint);
+
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
@@ -404,6 +426,10 @@ namespace osu.Game.Screens.Play
content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
+
+ settingsScroll.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint)
+ .MoveToX(settingsScroll.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
+
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION);
}
@@ -432,7 +458,7 @@ namespace osu.Game.Screens.Play
ContentOut();
- TransformSequence pushSequence = this.Delay(CONTENT_OUT_DURATION);
+ TransformSequence pushSequence = this.Delay(0);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
@@ -441,6 +467,7 @@ namespace osu.Game.Screens.Play
const double epilepsy_display_length = 3000;
pushSequence
+ .Delay(CONTENT_OUT_DURATION)
.Schedule(() => epilepsyWarning.State.Value = Visibility.Visible)
.TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint)
.Delay(epilepsy_display_length)
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index d56b9c23c8..345bd5a134 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -86,16 +86,13 @@ namespace osu.Game.Screens.Play
// Generally a timeout would not happen here as APIAccess will timeout first.
if (!tcs.Task.Wait(60000))
- handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
+ req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
return true;
void handleTokenFailure(Exception exception)
{
- // This method may be invoked multiple times due to the Task.Wait call above.
- // We only really care about the first error.
- if (!tcs.TrySetResult(false))
- return;
+ tcs.SetResult(false);
if (HandleTokenRetrievalFailure(exception))
{
diff --git a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
index 3c4ed4734b..1bb607bcf3 100644
--- a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
+++ b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
@@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -30,9 +32,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private IAPIProvider api { get; set; } = null!;
- [Resolved(canBeNull: true)]
+ [Resolved]
private LoginOverlay? loginOverlay { get; set; }
+ [Resolved]
+ private IDialogOverlay? dialogOverlay { get; set; }
+
public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo)
{
this.beatmapSetInfo = beatmapSetInfo;
@@ -43,11 +48,15 @@ namespace osu.Game.Screens.Select.Carousel
Origin = Anchor.CentreLeft;
}
+ private Bindable preferNoVideo = null!;
+
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
const float icon_size = 14;
+ preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo);
+
Content.Anchor = Anchor.CentreLeft;
Content.Origin = Anchor.CentreLeft;
@@ -96,17 +105,34 @@ namespace osu.Game.Screens.Select.Carousel
},
});
- Action = () =>
- {
- if (!api.IsLoggedIn)
- {
- loginOverlay?.Show();
- return;
- }
+ Action = updateBeatmap;
+ }
- beatmapDownloader.DownloadAsUpdate(beatmapSetInfo);
- attachExistingDownload();
- };
+ private bool updateConfirmed;
+
+ private void updateBeatmap()
+ {
+ if (!api.IsLoggedIn)
+ {
+ loginOverlay?.Show();
+ return;
+ }
+
+ if (dialogOverlay != null && beatmapSetInfo.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed)
+ {
+ dialogOverlay.Push(new UpdateLocalConfirmationDialog(() =>
+ {
+ updateConfirmed = true;
+ updateBeatmap();
+ }));
+
+ return;
+ }
+
+ updateConfirmed = false;
+
+ beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value);
+ attachExistingDownload();
}
protected override void LoadComplete()
diff --git a/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs b/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs
new file mode 100644
index 0000000000..f5267e905e
--- /dev/null
+++ b/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs
@@ -0,0 +1,21 @@
+// 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.Graphics.Sprites;
+using osu.Game.Overlays.Dialog;
+using osu.Game.Localisation;
+
+namespace osu.Game.Screens.Select.Carousel
+{
+ public class UpdateLocalConfirmationDialog : DeleteConfirmationDialog
+ {
+ public UpdateLocalConfirmationDialog(Action onConfirm)
+ {
+ HeaderText = PopupDialogStrings.UpdateLocallyModifiedText;
+ BodyText = PopupDialogStrings.UpdateLocallyModifiedDescription;
+ Icon = FontAwesome.Solid.ExclamationTriangle;
+ DeleteAction = onConfirm;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Utility/LatencyArea.cs b/osu.Game/Screens/Utility/LatencyArea.cs
index c8e0bf93a2..b7d45ba642 100644
--- a/osu.Game/Screens/Utility/LatencyArea.cs
+++ b/osu.Game/Screens/Utility/LatencyArea.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Screens.Utility
public readonly Bindable VisualMode = new Bindable();
- public CursorContainer? MenuCursor { get; private set; }
+ public CursorContainer? Cursor { get; private set; }
public bool ProvidingUserCursor => IsActiveArea.Value;
@@ -91,7 +91,7 @@ namespace osu.Game.Screens.Utility
{
RelativeSizeAxes = Axes.Both,
},
- MenuCursor = new LatencyCursorContainer
+ Cursor = new LatencyCursorContainer
{
RelativeSizeAxes = Axes.Both,
},
@@ -105,7 +105,7 @@ namespace osu.Game.Screens.Utility
{
RelativeSizeAxes = Axes.Both,
},
- MenuCursor = new LatencyCursorContainer
+ Cursor = new LatencyCursorContainer
{
RelativeSizeAxes = Axes.Both,
},
@@ -119,7 +119,7 @@ namespace osu.Game.Screens.Utility
{
RelativeSizeAxes = Axes.Both,
},
- MenuCursor = new LatencyCursorContainer
+ Cursor = new LatencyCursorContainer
{
RelativeSizeAxes = Axes.Both,
},
diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
index abc7b61036..f0caef9fa1 100644
--- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
@@ -67,9 +67,12 @@ namespace osu.Game.Skinning
return sampleInfo is StoryboardSampleInfo || beatmapHitsounds.Value;
}
+ private readonly ISkin skin;
+
public BeatmapSkinProvidingContainer(ISkin skin)
: base(skin)
{
+ this.skin = skin;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -84,11 +87,21 @@ namespace osu.Game.Skinning
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(SkinManager skins)
{
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
+
+ // If the beatmap skin looks to have skinnable resources, add the default classic skin as a fallback opportunity.
+ if (skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources)
+ {
+ SetSources(new[]
+ {
+ skin,
+ skins.DefaultClassicSkin
+ });
+ }
}
}
}
diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs
index 04f1286dc7..b80275a1e8 100644
--- a/osu.Game/Skinning/DefaultLegacySkin.cs
+++ b/osu.Game/Skinning/DefaultLegacySkin.cs
@@ -46,6 +46,8 @@ namespace osu.Game.Skinning
new Color4(242, 24, 57, 255)
};
+ Configuration.ConfigDictionary[nameof(SkinConfiguration.LegacySetting.AllowSliderBallTint)] = @"true";
+
Configuration.LegacyVersion = 2.7m;
}
}
diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
index 5a1ef34151..2937b62eec 100644
--- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
+++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
@@ -117,6 +117,11 @@ namespace osu.Game.Skinning.Editor
return false;
}
+ protected override void SelectAll()
+ {
+ SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray());
+ }
+
///
/// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints).
///
diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
index 980dee8601..469657c03c 100644
--- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
+++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
@@ -85,10 +85,6 @@ namespace osu.Game.Skinning.Editor
{
public Action? RequestPlacement;
- protected override bool ShouldBeConsideredForInput(Drawable child) => false;
-
- public override bool PropagateNonPositionalInputSubTree => false;
-
private readonly Drawable component;
private readonly CompositeDrawable? dependencySource;
@@ -177,6 +173,10 @@ namespace osu.Game.Skinning.Editor
public class DependencyBorrowingContainer : Container
{
+ protected override bool ShouldBeConsideredForInput(Drawable child) => false;
+
+ public override bool PropagateNonPositionalInputSubTree => false;
+
private readonly CompositeDrawable? donor;
public DependencyBorrowingContainer(CompositeDrawable? donor)
diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs
deleted file mode 100644
index 586882d790..0000000000
--- a/osu.Game/Skinning/HUDSkinComponents.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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.Skinning
-{
- public enum HUDSkinComponents
- {
- ComboCounter,
- ScoreCounter,
- AccuracyCounter,
- HealthDisplay,
- SongProgress,
- BarHitErrorMeter,
- ColourHitErrorMeter,
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs b/osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
similarity index 81%
rename from osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs
rename to osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
index 152ed5c3d9..2bcdd5b5a1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs
+++ b/osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
@@ -7,15 +7,15 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
-namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+namespace osu.Game.Skinning
{
- internal class KiaiFlashingDrawable : BeatSyncedContainer
+ public class LegacyKiaiFlashingDrawable : BeatSyncedContainer
{
private readonly Drawable flashingDrawable;
- private const float flash_opacity = 0.3f;
+ private const float flash_opacity = 0.55f;
- public KiaiFlashingDrawable(Func creationFunc)
+ public LegacyKiaiFlashingDrawable(Func creationFunc)
{
AutoSizeAxes = Axes.Both;
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
flashingDrawable
.FadeTo(flash_opacity)
.Then()
- .FadeOut(timingPoint.BeatLength * 0.75f);
+ .FadeOut(Math.Max(80, timingPoint.BeatLength - 80), Easing.OutSine);
}
}
}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 646746a0f3..eaca0de11a 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -389,18 +389,17 @@ namespace osu.Game.Skinning
if (particle != null)
return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, particle);
- else
- return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
+
+ return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
}
return null;
case SkinnableSprite.SpriteComponent sprite:
return this.GetAnimation(sprite.LookupName, false, false);
-
- default:
- throw new UnsupportedSkinComponentException(component);
}
+
+ return null;
}
private Texture? getParticleTexture(HitResult result)
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index 2de1564a5c..367e5bae01 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -13,6 +13,11 @@ namespace osu.Game.Skinning
///
public abstract class LegacySkinTransformer : SkinTransformer
{
+ ///
+ /// Whether the skin being transformed is able to provide legacy resources for the ruleset.
+ ///
+ public virtual bool IsProvidingLegacyResources => this.HasFont(LegacyFont.Combo);
+
protected LegacySkinTransformer(ISkin skin)
: base(skin)
{
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index 6ad5d64e4b..7267ebd92d 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Skinning
Ruleset = ruleset;
Beatmap = beatmap;
- InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin is LegacySkin ? GetRulesetTransformedSkin(beatmapSkin) : beatmapSkin)
+ InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin))
{
Child = Content = new Container
{
diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs
index 0b1159f8fd..4e5d96ccb8 100644
--- a/osu.Game/Skinning/SkinConfiguration.cs
+++ b/osu.Game/Skinning/SkinConfiguration.cs
@@ -38,7 +38,8 @@ namespace osu.Game.Skinning
HitCirclePrefix,
HitCircleOverlap,
AnimationFramerate,
- LayeredHitSounds
+ LayeredHitSounds,
+ AllowSliderBallTint,
}
public static List DefaultComboColours { get; } = new List
diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs
index 4d0da9597b..d1a1edcd03 100644
--- a/osu.Game/Storyboards/CommandTimeline.cs
+++ b/osu.Game/Storyboards/CommandTimeline.cs
@@ -27,7 +27,10 @@ namespace osu.Game.Storyboards
public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue)
{
if (endTime < startTime)
- return;
+ {
+ (startTime, endTime) = (endTime, startTime);
+ (startValue, endValue) = (endValue, startValue);
+ }
commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue });
diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs
index e47d19fba6..3ca83a4781 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -17,6 +17,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
@@ -42,6 +43,8 @@ namespace osu.Game.Tests.Visual
protected override bool CreateNestedActionContainer => false;
+ protected override bool DisplayCursorForManualInput => false;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -119,6 +122,8 @@ namespace osu.Game.Tests.Visual
public RealmAccess Realm => Dependencies.Get();
+ public new GlobalCursorDisplay GlobalCursorDisplay => base.GlobalCursorDisplay;
+
public new BackButton BackButton => base.BackButton;
public new BeatmapManager BeatmapManager => base.BeatmapManager;
diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
index 9082ca9c58..e56c546bac 100644
--- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
@@ -36,21 +36,31 @@ namespace osu.Game.Tests.Visual
///
protected virtual bool CreateNestedActionContainer => true;
+ ///
+ /// Whether a menu cursor controlled by the manual input manager should be displayed.
+ /// True by default, but is disabled for s as they provide their own global cursor.
+ ///
+ protected virtual bool DisplayCursorForManualInput => true;
+
protected OsuManualInputManagerTestScene()
{
- GlobalCursorDisplay cursorDisplay;
+ var mainContent = content = new Container { RelativeSizeAxes = Axes.Both };
- CompositeDrawable mainContent = cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both };
-
- cursorDisplay.Child = content = new OsuTooltipContainer(cursorDisplay.MenuCursor)
+ if (DisplayCursorForManualInput)
{
- RelativeSizeAxes = Axes.Both
- };
+ var cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both };
+
+ cursorDisplay.Add(new OsuTooltipContainer(cursorDisplay.MenuCursor)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = mainContent
+ });
+
+ mainContent = cursorDisplay;
+ }
if (CreateNestedActionContainer)
- {
mainContent = new GlobalActionContainer(null).WithChild(mainContent);
- }
base.Content.AddRange(new Drawable[]
{
diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
index cf7fe6e45d..1817a704b9 100644
--- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs
+++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
@@ -99,8 +99,8 @@ namespace osu.Game.Tests.Visual
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
=> implementation.GetLength(startTime, endTime, timeRange, scrollLength);
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => implementation.PositionAt(time, currentTime, timeRange, scrollLength);
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
+ => implementation.PositionAt(time, currentTime, timeRange, scrollLength, originTime);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
=> implementation.TimeAt(position, currentTime, timeRange, scrollLength);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index efdb6c6995..22474c0592 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,7 +18,7 @@
-
+
@@ -35,8 +35,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 47872e4ff7..cf70b65578 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -82,7 +82,7 @@
-
+