diff --git a/README.md b/README.md
index f3f025fa10..eb2fe6d0eb 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
-For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
+We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence
diff --git a/osu.Android.props b/osu.Android.props
index 927d66d93f..3ede0b85da 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index ea5f54a775..cd8894753f 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private void load()
{
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
- RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
index cae19e9468..180cb98ed7 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index e59a0a0431..6efb415880 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
index 57c06e1cd1..83db9f665b 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
index ce06b841aa..3afb8c3d89 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
index 9e38913be7..c537897439 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 1c52c092ec..f77dab56c8 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopCentre;
Size = new Vector2(BASE_SIZE);
+
if (difficulty != null)
Scale = calculateScale(difficulty);
@@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
+
body.Scale = scaleFromDirection;
- caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
+ // Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
+ caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
+ hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@@ -414,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
private void clearPlate(DroppedObjectAnimation animation)
{
- var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
+ var caughtObjects = caughtObjectContainer.Children.ToArray();
caughtObjectContainer.Clear(false);
+ // Use the already returned PoolableDrawables for new objects
+ var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
+
droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects)
@@ -426,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{
- var droppedObject = getDroppedObject(caughtObject);
-
caughtObjectContainer.Remove(caughtObject, false);
+ var droppedObject = getDroppedObject(caughtObject);
+
droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
@@ -452,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
break;
}
+ // Define lifetime start for dropped objects to be disposed correctly when rewinding replay
+ d.LifetimeStart = Clock.CurrentTime;
d.Expire();
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
index bec0a6a1d3..309393b664 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.5;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
index a302f95966..f4b9cf3b88 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
index 014954dd60..8d48e3acde 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.5;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
index 4cc712060c..748725af9f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModNightcore : ModNightcore
{
- public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png
new file mode 100644
index 0000000000..258162c486
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 907422858e..c84a6ab70f 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
@@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable background;
+ private readonly Bindable ripples = new Bindable();
+
public TestSceneGameplayCursor()
{
var ruleset = new OsuRuleset();
@@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
});
});
+ AddToggleStep("ripples", v => ripples.Value = v);
+
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
config.SetValue(OsuSetting.AutoCursorSize, true);
@@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("test cursor container", () => loadContent(false));
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
+ rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
+ }
+
[TestCase(1, 1)]
[TestCase(5, 1)]
[TestCase(10, 1)]
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs
index b2e4e07526..bb424eb587 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs
@@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
@@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private OsuConfigManager config { get; set; } = null!;
- private TestActionKeyCounter leftKeyCounter = null!;
+ private DefaultKeyCounter leftKeyCounter = null!;
- private TestActionKeyCounter rightKeyCounter = null!;
+ private DefaultKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!;
@@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre,
Children = new Drawable[]
{
- leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton)
+ leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Depth = float.MinValue,
X = -100,
},
- rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
+ rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
@@ -598,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertKeyCounter(int left, int right)
{
- AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left));
- AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right));
+ AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left));
+ AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right));
}
private void releaseAllTouches()
@@ -615,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action));
private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action));
- public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler
+ public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler
{
public OsuAction Action { get; }
- public TestActionKeyCounter(OsuAction action)
+ public TestActionKeyCounterTrigger(OsuAction action)
: base(action.ToString())
{
Action = action;
@@ -629,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
if (e.Action == Action)
{
- IsLit = true;
- Increment();
+ Activate();
}
return false;
@@ -638,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public void OnReleased(KeyBindingReleaseEvent e)
{
- if (e.Action == Action) IsLit = false;
+ if (e.Action == Action)
+ Deactivate();
}
}
diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
index b8ad61e6dd..2056a50eda 100644
--- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SetDefault(OsuRulesetSetting.SnakingInSliders, true);
SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
+ SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
@@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SnakingInSliders,
SnakingOutSliders,
ShowCursorTrail,
+ ShowCursorRipples,
PlayfieldBorderStyle,
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
index 3e161089cd..d6409279a4 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive
- || (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
+ || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject)
: base(hitObject)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
index 371dfe6a1a..1de6b9ce55 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
index 700a3f44bc..5569df8d95 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
index 4769e7660b..bf65a6c9d3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
index b7838ebaa7..661cc948c5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 8fdf3821fa..52fdfea95f 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor,
CursorTrail,
CursorParticles,
+ CursorRipple,
SliderScorePoint,
ReverseArrow,
HitCircleText,
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 620540b8ef..f049aa088f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
+ case OsuSkinComponents.CursorRipple:
+ if (GetTexture("cursor-ripple") != null)
+ {
+ var ripple = this.GetAnimation("cursor-ripple", false, false);
+
+ // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
+ // If anyone complains about these not being applied, this can be uncommented.
+ //
+ // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
+ // so we might be okay.
+ //
+ // if (ripple != null)
+ // {
+ // ripple.Scale = new Vector2(0.5f);
+ // ripple.Alpha = 0.2f;
+ // }
+
+ return ripple;
+ }
+
+ return null;
+
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs
new file mode 100644
index 0000000000..076d97d06a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs
@@ -0,0 +1,105 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Osu.Configuration;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.UI.Cursor
+{
+ public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler
+ {
+ private readonly Bindable showRipples = new Bindable(true);
+
+ private readonly DrawablePool ripplePool = new DrawablePool(20);
+
+ public CursorRippleVisualiser()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ public Vector2 CursorScale { get; set; } = Vector2.One;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuRulesetConfigManager? rulesetConfig)
+ {
+ rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (showRipples.Value)
+ {
+ AddInternal(ripplePool.Get(r =>
+ {
+ r.Position = e.MousePosition;
+ r.Scale = CursorScale;
+ }));
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ private partial class CursorRipple : PoolableDrawable
+ {
+ private Drawable ripple = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
+ {
+ Blending = BlendingParameters.Additive,
+ };
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ ClearTransforms(true);
+
+ ripple.ScaleTo(0.1f)
+ .ScaleTo(1, 700, Easing.Out);
+
+ this
+ .FadeOutFromOne(700)
+ .Expire(true);
+ }
+ }
+
+ public partial class DefaultCursorRipple : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new RingPiece(3)
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
+ Alpha = 0.1f,
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 5d7648b073..bf1ff872dd 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Bindable userCursorScale;
private Bindable autoCursorScale;
+ private readonly CursorRippleVisualiser rippleVisualiser;
+
public OsuCursorContainer()
{
InternalChild = fadeContainer = new Container
@@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Children = new[]
{
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
+ rippleVisualiser = new CursorRippleVisualiser(),
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
}
};
@@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
+ rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale;
}, true);
diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
index 64c4e7eef6..0e410dbf57 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
@@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = RulesetSettingsStrings.CursorTrail,
Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
},
+ new SettingsCheckbox
+ {
+ LabelText = RulesetSettingsStrings.CursorRipples,
+ Current = config.GetBindable(OsuRulesetSetting.ShowCursorRipples)
+ },
new SettingsEnumDropdown
{
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index 0a1f5380b5..8b1a4f688c 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.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.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
- switch (e.Button)
- {
- case MouseButton.Left:
- HitObject.Type = HitType.Centre;
- EndPlacement(true);
- return true;
+ if (e.Button != MouseButton.Left)
+ return false;
- case MouseButton.Right:
- HitObject.Type = HitType.Rim;
- EndPlacement(true);
- return true;
- }
-
- return false;
+ EndPlacement(true);
+ return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index d0361b1c8d..cdeaafde10 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
- drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
+ drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
index 84aa5e6bba..f442435d9c 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
index 89581c57bd..e517439ba4 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
index 68d6305fbf..9ef6fe8649 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
index 7cb14635ff..ad5da3d601 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index a08877e2dd..64d406a308 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
- public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
+ public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
@@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
- float ratio = DrawHeight / 480;
+ // Width is used because it defines how many notes fit on the playfield.
+ // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
+ float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
@@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
- LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
+ LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 42732d90e4..3587783104 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
- private const float default_aspect = 16f / 9f;
- public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true);
+ public const float MAXIMUM_ASPECT = 16f / 9f;
+ public const float MINIMUM_ASPECT = 5f / 4f;
+
+ public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true);
protected override void Update()
{
@@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
- if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
- height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
+ if (LockPlayfieldAspectRange.Value)
+ {
+ float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
+ if (currentAspect > MAXIMUM_ASPECT)
+ height *= currentAspect / MAXIMUM_ASPECT;
+ else if (currentAspect < MINIMUM_ASPECT)
+ height *= currentAspect / MINIMUM_ASPECT;
+ }
+
+ // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
+ height = Math.Min(height, 1f / 3f);
Height = height;
- // Position the taiko playfield exactly one playfield from the top of the screen.
+ // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
+ // Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
RelativePositionAxes = Axes.Y;
Y = height;
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 85d304da9c..d898650b66 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.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.Diagnostics;
using System.IO;
using System.Linq;
using NUnit.Framework;
@@ -161,6 +160,51 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeVideoWithLowercaseExtension()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+ var metadata = beatmap.Metadata;
+
+ Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
+ }
+ }
+
+ [Test]
+ public void TestDecodeVideoWithUppercaseExtension()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+ var metadata = beatmap.Metadata;
+
+ Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
+ }
+ }
+
+ [Test]
+ public void TestDecodeImageSpecifiedAsVideo()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+ var metadata = beatmap.Metadata;
+
+ Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
+ }
+ }
+
[Test]
public void TestDecodeBeatmapTimingPoints()
{
@@ -320,6 +364,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
var comboColors = decoder.Decode(stream).ComboColours;
+ Debug.Assert(comboColors != null);
+
Color4[] expectedColors =
{
new Color4(142, 199, 255, 255),
@@ -330,7 +376,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
- Assert.AreEqual(expectedColors.Length, comboColors?.Count);
+ Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++)
Assert.AreEqual(expectedColors[i], comboColors[i]);
}
@@ -415,14 +461,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData);
- Assert.AreEqual(new Vector2(192, 168), positionData.Position);
+ Assert.AreEqual(new Vector2(192, 168), positionData!.Position);
Assert.AreEqual(956, hitObjects[0].StartTime);
Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));
positionData = hitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData);
- Assert.AreEqual(new Vector2(304, 56), positionData.Position);
+ Assert.AreEqual(new Vector2(304, 56), positionData!.Position);
Assert.AreEqual(1285, hitObjects[1].StartTime);
Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP));
}
@@ -578,8 +624,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestFallbackDecoderForCorruptedHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -596,8 +642,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestFallbackDecoderForMissingHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("missing-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -614,8 +660,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeFileWithEmptyLinesAtStart()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -632,8 +678,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeFileWithEmptyLinesAndNoHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -650,8 +696,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeFileWithContentImmediatelyAfterHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -678,7 +724,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestAllowFallbackDecoderOverwrite()
{
- Decoder decoder = null;
+ Decoder decoder = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream))
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index 281ea4e4ff..34ff8bfd84 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.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.Linq;
using NUnit.Framework;
using osuTK;
@@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(storyboard.HasDrawable);
Assert.AreEqual(6, storyboard.Layers.Count());
- StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3);
+ StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.IsNotNull(background);
Assert.AreEqual(16, background.Elements.Count);
Assert.IsTrue(background.VisibleWhenFailing);
Assert.IsTrue(background.VisibleWhenPassing);
Assert.AreEqual("Background", background.Name);
- StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2);
+ StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2);
Assert.IsNotNull(fail);
Assert.AreEqual(0, fail.Elements.Count);
Assert.IsTrue(fail.VisibleWhenFailing);
Assert.IsFalse(fail.VisibleWhenPassing);
Assert.AreEqual("Fail", fail.Name);
- StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1);
+ StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1);
Assert.IsNotNull(pass);
Assert.AreEqual(0, pass.Elements.Count);
Assert.IsFalse(pass.VisibleWhenFailing);
Assert.IsTrue(pass.VisibleWhenPassing);
Assert.AreEqual("Pass", pass.Name);
- StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0);
+ StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0);
Assert.IsNotNull(foreground);
Assert.AreEqual(151, foreground.Elements.Count);
Assert.IsTrue(foreground.VisibleWhenFailing);
Assert.IsTrue(foreground.VisibleWhenPassing);
Assert.AreEqual("Foreground", foreground.Name);
- StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue);
+ StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue);
Assert.IsNotNull(overlay);
Assert.IsEmpty(overlay.Elements);
Assert.IsTrue(overlay.VisibleWhenFailing);
@@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sprite = background.Elements.ElementAt(0) as StoryboardSprite;
Assert.NotNull(sprite);
- Assert.IsTrue(sprite.HasCommands);
+ Assert.IsTrue(sprite!.HasCommands);
Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition);
Assert.IsTrue(sprite.IsDrawable);
Assert.AreEqual(Anchor.Centre, sprite.Origin);
@@ -97,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestLoopWithoutExplicitFadeOut()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
+ Assert.AreEqual(1, background.Elements.Count);
+
+ Assert.AreEqual(2000, background.Elements[0].StartTime);
+ Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
+
+ Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
+ Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
+ }
+ }
+
[Test]
public void TestCorrectAnimationStartTime()
{
@@ -171,6 +190,55 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeVideoWithLowercaseExtension()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+
+ Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path);
+ }
+ }
+
+ [Test]
+ public void TestDecodeVideoWithUppercaseExtension()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+
+ Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path);
+ }
+ }
+
+ [Test]
+ public void TestDecodeImageSpecifiedAsVideo()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.Zero);
+ }
+ }
+
[Test]
public void TestDecodeOutOfRangeLoopAnimationType()
{
diff --git a/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb
new file mode 100644
index 0000000000..7afaa445df
--- /dev/null
+++ b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb
@@ -0,0 +1,6 @@
+[Events]
+//Storyboard Layer 0 (Background)
+Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
+ F,0,2000,,0,1
+ L,2000,10
+ F,18,0,1000,1,0
diff --git a/osu.Game.Tests/Resources/image-specified-as-video.osb b/osu.Game.Tests/Resources/image-specified-as-video.osb
new file mode 100644
index 0000000000..9cea7dd4e7
--- /dev/null
+++ b/osu.Game.Tests/Resources/image-specified-as-video.osb
@@ -0,0 +1,4 @@
+osu file format v14
+
+[Events]
+Video,0,"BG.jpg",0,0
diff --git a/osu.Game.Tests/Resources/video-with-lowercase-extension.osb b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb
new file mode 100644
index 0000000000..eec09722ed
--- /dev/null
+++ b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb
@@ -0,0 +1,5 @@
+osu file format v14
+
+[Events]
+0,0,"BG.jpg",0,0
+Video,0,"Video.avi",0,0
diff --git a/osu.Game.Tests/Resources/video-with-uppercase-extension.osb b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb
new file mode 100644
index 0000000000..3834a547f2
--- /dev/null
+++ b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb
@@ -0,0 +1,5 @@
+osu file format v14
+
+[Events]
+0,0,"BG.jpg",0,0
+Video,0,"Video.AVI",0,0
diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index f8248e88bb..6639b6dd68 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -164,7 +164,7 @@ namespace osu.Game.Tests.Rulesets
this.parentManager = parentManager;
}
- public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name);
+ public override byte[] GetRawData(string fileName) => parentManager.GetRawData(fileName);
public bool IsDisposed { get; private set; }
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index f1533a32b9..585a3f95e7 100644
--- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.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 NUnit.Framework;
@@ -51,9 +49,11 @@ namespace osu.Game.Tests.Testing
[Test]
public void TestRetrieveShader()
{
- AddAssert("ruleset shaders retrieved", () =>
- Dependencies.Get().LoadRaw(@"sh_TestVertex.vs") != null &&
- Dependencies.Get().LoadRaw(@"sh_TestFragment.fs") != null);
+ AddStep("ruleset shaders retrieved without error", () =>
+ {
+ Dependencies.Get().GetRawData(@"sh_TestVertex.vs");
+ Dependencies.Get().GetRawData(@"sh_TestFragment.fs");
+ });
}
[Test]
@@ -76,12 +76,12 @@ namespace osu.Game.Tests.Testing
}
public override IResourceStore CreateResourceStore() => new NamespacedResourceStore(TestResources.GetStore(), @"Resources");
- public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager();
+ public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TestRulesetConfigManager();
public override IEnumerable GetModsFor(ModType type) => Array.Empty();
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null;
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => null!;
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
}
private class TestRulesetConfigManager : IRulesetConfigManager
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index fbdaad1cd8..8f4250799e 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsDrawable => true;
public double StartTime => double.MinValue;
public double EndTime => double.MaxValue;
+ public double EndTimeForDisplay => double.MaxValue;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs
new file mode 100644
index 0000000000..7f9a69833c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions;
+using osu.Game.Database;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene
+ {
+ public override void SetUpSteps()
+ {
+ CreateInitialBeatmap = () =>
+ {
+ var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
+ return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First());
+ };
+
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestLocallyModifyingOnlineBeatmap()
+ {
+ AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
+
+ AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
+ SaveEditor();
+
+ ReloadEditorToSameBeatmap();
+ AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 5442b3bfef..f3f942b74b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay
var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
- AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2));
seekTo(referenceBeatmap.Breaks[0].StartTime);
- AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
+ AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
- AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 1dffeed01b..751aeb4e13 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
- AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15);
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
- AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
AddAssert("no results triggered", () => Player.Results.Count == 0);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index b918c5e64a..ae46dda750 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
- private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
+ private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
[BackgroundDependencyLoader]
private void load()
@@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty());
// Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
index 890ac21b40..22f7111f68 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
@@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -17,43 +19,63 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public TestSceneKeyCounter()
{
- KeyCounterKeyboard testCounter;
-
- KeyCounterDisplay kc = new KeyCounterDisplay
+ KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Children = new KeyCounter[]
- {
- testCounter = new KeyCounterKeyboard(Key.X),
- new KeyCounterKeyboard(Key.X),
- new KeyCounterMouse(MouseButton.Left),
- new KeyCounterMouse(MouseButton.Right),
- },
+ Position = new Vector2(0, 72.7f)
};
+ KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Position = new Vector2(0, -72.7f)
+ };
+
+ defaultDisplay.AddRange(new InputTrigger[]
+ {
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterMouseTrigger(MouseButton.Left),
+ new KeyCounterMouseTrigger(MouseButton.Right),
+ });
+
+ argonDisplay.AddRange(new InputTrigger[]
+ {
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterMouseTrigger(MouseButton.Left),
+ new KeyCounterMouseTrigger(MouseButton.Right),
+ });
+
+ var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First();
+
AddStep("Add random", () =>
{
Key key = (Key)((int)Key.A + RNG.Next(26));
- kc.Add(new KeyCounterKeyboard(key));
+ defaultDisplay.Add(new KeyCounterKeyboardTrigger(key));
+ argonDisplay.Add(new KeyCounterKeyboardTrigger(key));
});
- Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key;
+ Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key;
- void addPressKeyStep()
+ addPressKeyStep();
+ AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
+ addPressKeyStep();
+ AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
+ AddStep("Disable counting", () =>
{
- AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
- }
+ argonDisplay.IsCounting.Value = false;
+ defaultDisplay.IsCounting.Value = false;
+ });
+ addPressKeyStep();
+ AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
- addPressKeyStep();
- AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1);
- addPressKeyStep();
- AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
- AddStep("Disable counting", () => testCounter.IsCounting = false);
- addPressKeyStep();
- AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2);
+ Add(defaultDisplay);
+ Add(argonDisplay);
- Add(kc);
+ void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 2ea27c2fef..dbd1ce1f6e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private SessionStatics sessionStatics { get; set; }
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
[Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay;
@@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay
saveVolumes();
setFullVolume();
+ AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => resetPlayer(false));
@@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay
restoreVolumes();
}
+ [Test]
+ public void TestEpilepsyWarningWithDisabledStoryboard()
+ {
+ saveVolumes();
+ setFullVolume();
+
+ AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false));
+ AddStep("change epilepsy warning", () => epilepsyWarning = true);
+ AddStep("load dummy beatmap", () => resetPlayer(false));
+
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
+
+ AddUntilStep("epilepsy warning absent", () => getWarning() == null);
+
+ restoreVolumes();
+ }
+
[Test]
public void TestEpilepsyWarningEarlyExit()
{
saveVolumes();
setFullVolume();
+ AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false));
@@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick());
}
- private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault();
+ private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
index 0469df1de3..d16f51f36e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
@@ -164,6 +164,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
}
+ [Test]
+ public void TestRevertNestedObjects()
+ {
+ ManualClock clock = null;
+
+ var beatmap = new Beatmap();
+ beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 });
+
+ createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
+
+ AddStep("skip to middle of object", () => clock.CurrentTime = (beatmap.HitObjects[0].StartTime + beatmap.HitObjects[0].GetEndTime()) / 2);
+ AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
+
+ AddStep("skip to before end of object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() - 1);
+ AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
+
+ DrawableHitObject drawableHitObject = null;
+ HashSet revertedHitObjects = new HashSet();
+
+ AddStep("retrieve drawable hit object", () => drawableHitObject = playfield.ChildrenOfType().Single());
+ AddStep("set up revert tracking", () =>
+ {
+ revertedHitObjects.Clear();
+ drawableHitObject.OnRevertResult += (ho, _) => revertedHitObjects.Add(ho.HitObject);
+ });
+ AddStep("skip back to object start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime);
+ AddAssert("3 reverts fired", () => revertedHitObjects, () => Has.Count.EqualTo(3));
+ AddAssert("no objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
+ }
+
[Test]
public void TestApplyHitResultOnKilled()
{
@@ -258,6 +288,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RegisterPool(poolSize);
RegisterPool(poolSize);
+ RegisterPool(poolSize);
+ RegisterPool(poolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@@ -388,6 +420,120 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
+ private class TestHitObjectWithNested : TestHitObject
+ {
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ base.CreateNestedHitObjects(cancellationToken);
+
+ for (int i = 0; i < 3; ++i)
+ AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
+ }
+ }
+
+ private class NestedHitObject : ConvertHitObject
+ {
+ }
+
+ private partial class DrawableTestHitObjectWithNested : DrawableHitObject
+ {
+ private Container nestedContainer;
+
+ public DrawableTestHitObjectWithNested()
+ : base(null)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.Red
+ },
+ nestedContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ Size = new Vector2(200, 50);
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+ nestedContainer.Add(hitObject);
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ nestedContainer.Clear(false);
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ base.CheckForResult(userTriggered, timeOffset);
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+
+ private partial class DrawableNestedHitObject : DrawableHitObject
+ {
+ public DrawableNestedHitObject()
+ : this(null)
+ {
+ }
+
+ public DrawableNestedHitObject(NestedHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(15);
+ Colour = Colour4.White;
+ RelativePositionAxes = Axes.Both;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ });
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ X = (float)((HitObject.StartTime - ParentHitObject!.HitObject.StartTime) / (ParentHitObject.HitObject.GetEndTime() - ParentHitObject.HitObject.StartTime));
+ Y = 0.5f;
+
+ LifetimeStart = ParentHitObject.LifetimeStart;
+ LifetimeEnd = ParentHitObject.LifetimeEnd;
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ base.CheckForResult(userTriggered, timeOffset);
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+
#endregion
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
index c476aae202..bf9b13b320 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0);
- AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0));
+ AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0));
AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
index 3e415af86e..ae10207de0 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
@@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
@@ -45,6 +46,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
}
+ [Test]
+ public void TestDoesNotFailOnExit()
+ {
+ loadPlayerWithBeatmap();
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+ AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F));
+ AddStep("exit player", () => Player.Exit());
+ AddUntilStep("wait for exit", () => Player.Parent == null);
+ AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F));
+ }
+
[Test]
public void TestPauseViaSpaceWithSkip()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
index a7da8f9832..93fec60de4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay;
using osuTK.Input;
@@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
// Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1;
return new Container
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index 1f2329af4a..0439656aae 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -18,6 +18,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.Tests.Gameplay;
using osuTK.Input;
@@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
- private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
+ private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
[Test]
public void TestComboCounterIncrementing()
@@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty());
// Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
action?.Invoke(hudOverlay);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index 5c69062e67..3f78dbfd96 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.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;
@@ -22,8 +20,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSkinnableSound : OsuTestScene
{
- private TestSkinSourceContainer skinSource;
- private PausableSkinnableSound skinnableSound;
+ private TestSkinSourceContainer skinSource = null!;
+ private PausableSkinnableSound skinnableSound = null!;
+
+ private const string sample_lookup = "Gameplay/normal-sliderslide";
[SetUpSteps]
public void SetUpSteps()
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
// has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached.
- skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide")));
+ skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo(sample_lookup)));
});
}
@@ -99,10 +99,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
}
+ [Test]
+ public void TestSampleUpdatedBeforePlaybackWhenNotPresent()
+ {
+ AddStep("make sample non-present", () => skinnableSound.Hide());
+ AddUntilStep("ensure not present", () => skinnableSound.IsPresent, () => Is.False);
+
+ AddUntilStep("ensure sample loaded", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo(sample_lookup));
+
+ AddStep("change source", () =>
+ {
+ skinSource.OverridingSample = new SampleVirtual("new skin");
+ skinSource.TriggerSourceChanged();
+ });
+
+ AddStep("start sample", () => skinnableSound.Play());
+ AddUntilStep("sample updated", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo("new skin"));
+ }
+
[Test]
public void TestSkinChangeDoesntPlayOnPause()
{
- DrawableSample sample = null;
+ DrawableSample? sample = null;
AddStep("start sample", () =>
{
skinnableSound.Play();
@@ -118,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("retrieve and ensure current sample is different", () =>
{
- DrawableSample oldSample = sample;
+ DrawableSample? oldSample = sample;
sample = skinnableSound.ChildrenOfType().Single();
return sample != oldSample;
});
@@ -134,20 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{
[Resolved]
- private ISkinSource source { get; set; }
+ private ISkinSource source { get; set; } = null!;
- public event Action SourceChanged;
+ public event Action? SourceChanged;
public Bindable SamplePlaybackDisabled { get; } = new Bindable();
+ public ISample? OverridingSample;
+
IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
- public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => source?.GetDrawableComponent(lookup);
- public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
- public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
- public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup);
- public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction);
- public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty());
+ public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => source.GetDrawableComponent(lookup);
+ public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source.GetTexture(componentName, wrapModeS, wrapModeT);
+ public ISample? GetSample(ISampleInfo sampleInfo) => OverridingSample ?? source.GetSample(sampleInfo);
+
+ public IBindable? GetConfig(TLookup lookup)
+ where TLookup : notnull
+ where TValue : notnull
+ {
+ return source.GetConfig(lookup);
+ }
+
+ public ISkin? FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source.FindProvider(lookupFunction);
+ public IEnumerable AllSources => new[] { this }.Concat(source.AllSources);
public void TriggerSourceChanged()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 0d88fb01a8..283866bef2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
@@ -106,6 +107,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
+ [Test]
+ public void TestSaveFailedReplayWithStoryboardEndedDoesNotProgress()
+ {
+ CreateTest(() =>
+ {
+ AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true);
+ AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0);
+ });
+ AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
+
+ AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
+ AddUntilStep("wait for button clickable", () => Player.ChildrenOfType().First().ChildrenOfType().First().Enabled.Value);
+ AddStep("click save button", () => Player.ChildrenOfType().First().ChildrenOfType().First().TriggerClick());
+
+ // Test a regression where importing the fail replay would cause progression to results screen in a failed state.
+ AddWaitStep("wait some", 10);
+ AddAssert("player is still current screen", () => Player.IsCurrentScreen());
+ }
+
[Test]
public void TestShowResultsFalse()
{
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index aef6f9ade0..22c7bb64b2 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -114,6 +114,19 @@ namespace osu.Game.Tests.Visual.Menus
}
}
+ [TestCase(OverlayActivation.All)]
+ [TestCase(OverlayActivation.Disabled)]
+ public void TestButtonKeyboardInputRespectsOverlayActivation(OverlayActivation mode)
+ {
+ AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode);
+ AddStep("hide toolbar", () => toolbar.Hide());
+
+ if (mode == OverlayActivation.Disabled)
+ AddAssert("check buttons not accepting input", () => InputManager.NonPositionalInputQueue.OfType().Count(), () => Is.Zero);
+ else
+ AddAssert("check buttons accepting input", () => InputManager.NonPositionalInputQueue.OfType().Count(), () => Is.Not.Zero);
+ }
+
[TestCase(OverlayActivation.All)]
[TestCase(OverlayActivation.Disabled)]
public void TestRespectsOverlayActivation(OverlayActivation mode)
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
new file mode 100644
index 0000000000..603573058e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Menu;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
+ {
+ ///
+ /// When entering the editor, a new beatmap is created as part of the asynchronous load process.
+ /// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
+ /// doesn't leave a dangling beatmap behind.
+ ///
+ /// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough
+ /// as a test.
+ ///
+ [Test]
+ public void TestCancelNavigationToEditor()
+ {
+ BeatmapSetInfo[] beatmapSets = null!;
+
+ AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets());
+
+ AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
+
+ AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader()));
+ AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
+ AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
+
+ AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
+ AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets));
+
+ BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All().Where(x => !x.DeletePending).ToArray());
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
index d937b9e6d7..224e7e411e 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
@@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -69,10 +70,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false);
AddStep("press 'z'", () => InputManager.Key(Key.Z));
- AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0);
+ AddAssert("key counter didn't increase", () => keyCounter.CountPresses.Value == 0);
AddStep("press 's'", () => InputManager.Key(Key.S));
- AddAssert("key counter did increase", () => keyCounter.CountPresses == 1);
+ AddAssert("key counter did increase", () => keyCounter.CountPresses.Value == 1);
}
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 5d13421195..a27c4ddad2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -26,7 +27,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online
{
- public partial class TestSceneBeatmapSetOverlay : OsuTestScene
+ public partial class TestSceneBeatmapSetOverlay : OsuManualInputManagerTestScene
{
private readonly TestBeatmapSetOverlay overlay;
@@ -281,6 +282,22 @@ namespace osu.Game.Tests.Visual.Online
AddAssert(@"type is correct", () => type == lookupType.ToString());
}
+ [Test]
+ public void TestBeatmapSetWithGuestDifficulty()
+ {
+ AddStep("show map", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty()));
+ AddStep("move mouse to host difficulty", () =>
+ {
+ InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(0));
+ });
+ AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().All(s => s.Text != "BanchoBot"));
+ AddStep("move mouse to guest difficulty", () =>
+ {
+ InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1));
+ });
+ AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot"));
+ }
+
private APIBeatmapSet createManyDifficultiesBeatmapSet()
{
var set = getBeatmapSet();
@@ -320,6 +337,60 @@ namespace osu.Game.Tests.Visual.Online
return beatmapSet;
}
+ private APIBeatmapSet createBeatmapSetWithGuestDifficulty()
+ {
+ var set = getBeatmapSet();
+
+ var beatmaps = new List();
+
+ var guestUser = new APIUser
+ {
+ Username = @"BanchoBot",
+ Id = 3,
+ };
+
+ set.RelatedUsers = new[]
+ {
+ set.Author, guestUser
+ };
+
+ beatmaps.Add(new APIBeatmap
+ {
+ OnlineID = 1145,
+ DifficultyName = "Host Diff",
+ RulesetID = Ruleset.Value.OnlineID,
+ StarRating = 1.4,
+ OverallDifficulty = 3.5f,
+ AuthorID = set.AuthorID,
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ Status = BeatmapOnlineStatus.Graveyard
+ });
+
+ beatmaps.Add(new APIBeatmap
+ {
+ OnlineID = 1919,
+ DifficultyName = "Guest Diff",
+ RulesetID = Ruleset.Value.OnlineID,
+ StarRating = 8.1,
+ OverallDifficulty = 3.5f,
+ AuthorID = 3,
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ Status = BeatmapOnlineStatus.Graveyard
+ });
+
+ set.Beatmaps = beatmaps.ToArray();
+
+ return set;
+ }
+
private void downloadAssert(bool shown)
{
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index feab86d3ee..f094d40caa 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -1068,6 +1068,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value);
}
+ [Test]
+ public void TestTextBoxBeatmapDifficultyCount()
+ {
+ createSongSelect();
+
+ AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches");
+
+ addRulesetImportStep(0);
+
+ AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches");
+ AddStep("delete all beatmaps", () => manager.Delete());
+ AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault);
+ AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches");
+ }
+
private void waitForInitialSelection()
{
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
index 316035275f..dd7bf48791 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
@@ -14,10 +14,11 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
- public partial class TestSceneBeatmapListingSortTabControl : OsuTestScene
+ public partial class TestSceneBeatmapListingSortTabControl : OsuManualInputManagerTestScene
{
private readonly BeatmapListingSortTabControl control;
@@ -111,6 +112,29 @@ namespace osu.Game.Tests.Visual.UserInterface
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Mine);
}
+ [Test]
+ public void TestSortDirectionOnCriteriaChange()
+ {
+ AddStep("set category to leaderboard", () => control.Reset(SearchCategory.Leaderboard, false));
+ AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending);
+
+ AddStep("click ranked sort button", () =>
+ {
+ InputManager.MoveMouseTo(control.TabControl.ChildrenOfType().Single(s => s.Active.Value));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("sort direction is ascending", () => control.SortDirection.Value == SortDirection.Ascending);
+
+ AddStep("click first inactive sort button", () =>
+ {
+ InputManager.MoveMouseTo(control.TabControl.ChildrenOfType().First(s => !s.Active.Value));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending);
+ }
+
private void criteriaShowsOnCategory(bool expected, SortCriteria criteria, SearchCategory category)
{
AddAssert($"{criteria.ToString().ToLowerInvariant()} {(expected ? "shown" : "not shown")} on {category.ToString().ToLowerInvariant()}", () =>
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 4752a88199..4731a70753 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
- public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
first.PerformRead(s =>
{
// Re-run processing even in this case. We might have outdated metadata.
- ProcessBeatmap?.Invoke((s, false));
+ ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
});
return first;
}
@@ -206,7 +206,7 @@ namespace osu.Game.Beatmaps
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
base.PostImport(model, realm, parameters);
- ProcessBeatmap?.Invoke((model, parameters.Batch));
+ ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 3208598f56..63e878b80d 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -167,7 +167,7 @@ namespace osu.Game.Beatmaps
///
public double DistanceSpacing { get; set; } = 1.0;
- public int BeatDivisor { get; set; }
+ public int BeatDivisor { get; set; } = 4;
public int GridSize { get; set; }
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index ad56bbbc3a..ae62564b0d 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps
private readonly WorkingBeatmapCache workingBeatmapCache;
- public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public override bool PauseImports
{
@@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm);
- beatmapImporter.ProcessBeatmap = args => ProcessBeatmap?.Invoke(args);
+ beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps
// user requested abort
return;
- var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
+ var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null)
{
@@ -415,6 +415,13 @@ namespace osu.Game.Beatmaps
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
+ // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
+ // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
+ // which influences the beatmap checksums.
+ beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
+ beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
+ beatmapInfo.ResetOnlineInfo();
+
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@@ -438,9 +445,6 @@ namespace osu.Game.Beatmaps
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
- beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
- beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
-
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
@@ -454,7 +458,9 @@ namespace osu.Game.Beatmaps
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
- ProcessBeatmap?.Invoke((liveBeatmapSet, false));
+ // do not look up metadata.
+ // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
+ ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
}
@@ -542,4 +548,11 @@ namespace osu.Game.Beatmaps
public override string HumanisedModelName => "beatmap";
}
+
+ ///
+ /// Delegate type for beatmap processing callbacks.
+ ///
+ /// The beatmap set to be processed.
+ /// The scope to use when looking up metadata.
+ public delegate void ProcessBeatmapDelegate(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope);
}
diff --git a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
index 98aefd75d3..b160043820 100644
--- a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
+++ b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
var matchingSet = r.All().FirstOrDefault(s => s.OnlineID == id);
if (matchingSet != null)
- beatmapUpdater.Queue(matchingSet.ToLive(realm), true);
+ beatmapUpdater.Queue(matchingSet.ToLive(realm), MetadataLookupScope.OnlineFirst);
}
});
}
diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs
index d7b1fac7b3..af9f32f834 100644
--- a/osu.Game/Beatmaps/BeatmapUpdater.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdater.cs
@@ -42,24 +42,25 @@ namespace osu.Game.Beatmaps
/// Queue a beatmap for background processing.
///
/// The managed beatmap set to update. A transaction will be opened to apply changes.
- /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible.
- public void Queue(Live beatmapSet, bool preferOnlineFetch = false)
+ /// The preferred scope to use for metadata lookup.
+ public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
- Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, preferOnlineFetch)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
+ Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
}
///
/// Run all processing on a beatmap immediately.
///
/// The managed beatmap set to update. A transaction will be opened to apply changes.
- /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible.
- public void Process(BeatmapSetInfo beatmapSet, bool preferOnlineFetch = false) => beatmapSet.Realm.Write(r =>
+ /// The preferred scope to use for metadata lookup.
+ public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r =>
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);
- metadataLookup.Update(beatmapSet, preferOnlineFetch);
+ if (lookupScope != MetadataLookupScope.None)
+ metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
foreach (var beatmap in beatmapSet.Beatmaps)
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index eabc63b341..ef1dbc0488 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -363,6 +363,19 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break;
+ case LegacyEventType.Video:
+ string filename = CleanFilename(split[2]);
+
+ // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
+ // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
+ // video extensions and handle similar to a background if it doesn't match.
+ if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
+ {
+ beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
+ }
+
+ break;
+
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break;
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index 44dbb3cc9f..df5d3edb55 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -109,6 +109,14 @@ namespace osu.Game.Beatmaps.Formats
int offset = Parsing.ParseInt(split[1]);
string path = CleanFilename(split[2]);
+ // See handling in LegacyBeatmapDecoder for the special case where a video type is used but
+ // the file extension is not a valid video.
+ //
+ // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
+ // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
+ if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
+ break;
+
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset));
break;
}
@@ -276,7 +284,8 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
case "A":
- timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
+ timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
+ startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break;
case "H":
diff --git a/osu.Game/Beatmaps/MetadataLookupScope.cs b/osu.Game/Beatmaps/MetadataLookupScope.cs
new file mode 100644
index 0000000000..e1fbedc26a
--- /dev/null
+++ b/osu.Game/Beatmaps/MetadataLookupScope.cs
@@ -0,0 +1,26 @@
+// 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
+{
+ ///
+ /// Determines which sources (if any at all) should be queried in which order for a beatmap's metadata.
+ ///
+ public enum MetadataLookupScope
+ {
+ ///
+ /// Do not attempt to look up the beatmap metadata either in the local cache or online.
+ ///
+ None,
+
+ ///
+ /// Try the local metadata cache first before querying online sources.
+ ///
+ LocalCacheFirst,
+
+ ///
+ /// Query online sources immediately.
+ ///
+ OnlineFirst
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
index 37e15c6127..7097102335 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
get
{
- if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension))
+ if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant()))
return FontAwesome.Regular.FileVideo;
switch (File.Extension)
diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs
index f4e23ae7cb..20258b9c35 100644
--- a/osu.Game/Localisation/EditorStrings.cs
+++ b/osu.Game/Localisation/EditorStrings.cs
@@ -99,6 +99,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks");
+ ///
+ /// "{0:0}°"
+ ///
+ public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0}°", newRotation);
+
+ ///
+ /// "{0:0}° (snapped)"
+ ///
+ public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
index 3a7fe4bb12..a77ee066e4 100644
--- a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
@@ -15,9 +15,9 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps");
///
- /// ""Beatmaps" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."
+ /// ""Beatmaps" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."
///
- public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection.");
+ public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection.");
///
/// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay."
diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs
index 6a9793b20c..5e2600bc50 100644
--- a/osu.Game/Localisation/NotificationsStrings.cs
+++ b/osu.Game/Localisation/NotificationsStrings.cs
@@ -50,16 +50,18 @@ namespace osu.Game.Localisation
public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!");
///
- /// "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."
+ /// "osu! doesn't seem to be able to play audio correctly.
+ ///
+ /// Please try changing your audio device to a working setting."
///
- public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"),
- @"osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting.");
+ public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), @"osu! doesn't seem to be able to play audio correctly.
+
+Please try changing your audio device to a working setting.");
///
/// "The score overlay is currently disabled. You can toggle this by pressing {0}."
///
- public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"),
- @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
+ public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
private static string getKey(string key) => $@"{prefix}:{key}";
}
diff --git a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
index d2ff783413..3fa86c188c 100644
--- a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
+++ b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
@@ -65,6 +65,11 @@ namespace osu.Game.Localisation
if (manager == null)
return null;
+ // When using the English culture, prefer the fallbacks rather than osu-resources baked strings.
+ // They are guaranteed to be up-to-date, and is also what a developer expects to see when making changes to `xxxStrings.cs` files.
+ if (EffectiveCulture.Name == @"en")
+ return null;
+
try
{
return manager.GetString(key, EffectiveCulture);
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
index 1b0df6ecf6..52e6a5eaac 100644
--- a/osu.Game/Localisation/RulesetSettingsStrings.cs
+++ b/osu.Game/Localisation/RulesetSettingsStrings.cs
@@ -29,6 +29,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail");
+ ///
+ /// "Cursor ripples"
+ ///
+ public static LocalisableString CursorRipples => new TranslatableString(getKey(@"cursor_ripples"), @"Cursor ripples");
+
///
/// "Playfield border style"
///
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index cf58d07b9e..34e31b0d61 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -71,7 +71,7 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
- public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
+ public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
public const string OSU_PROTOCOL = "osu://";
@@ -310,7 +310,7 @@ namespace osu.Game
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
- BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch);
+ BeatmapManager.ProcessBeatmap = (beatmapSet, scope) => beatmapUpdater.Process(beatmapSet, scope);
dependencies.Cache(userCache = new UserLookupCache());
base.Content.Add(userCache);
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 2e20f83e9e..219cbe7eef 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -209,7 +209,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message });
- game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}");
+ game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
}
}
else
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
index 76b6dec65b..3336c383ff 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Graphics;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
@@ -10,8 +11,12 @@ namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapListingHeader : OverlayHeader
{
+ public BeatmapListingFilterControl FilterControl { get; private set; }
+
protected override OverlayTitle CreateTitle() => new BeatmapListingTitle();
+ protected override Drawable CreateContent() => FilterControl = new BeatmapListingFilterControl();
+
private partial class BeatmapListingTitle : OverlayTitle
{
public BeatmapListingTitle()
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index 23de1cf76d..3fa0fc7a77 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding
{
Vertical = 20,
- Horizontal = 40,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
},
Child = new FillFlowContainer
{
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
index 025738710f..2f290d05e9 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Overlays.BeatmapListing
if (currentParameters == null)
Reset(SearchCategory.Leaderboard, false);
+
+ Current.BindValueChanged(_ => SortDirection.Value = Overlays.SortDirection.Descending);
}
public void Reset(SearchCategory category, bool hasQuery)
@@ -102,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapListing
};
}
- private partial class BeatmapTabButton : TabButton
+ public partial class BeatmapTabButton : TabButton
{
public readonly Bindable SortDirection = new Bindable();
@@ -136,7 +138,7 @@ namespace osu.Game.Overlays.BeatmapListing
SortDirection.BindValueChanged(direction =>
{
- icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown;
+ icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown;
}, true);
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 73961487ed..f8784504b8 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -43,7 +43,8 @@ namespace osu.Game.Overlays
private Container panelTarget;
private FillFlowContainer foundContent;
- private BeatmapListingFilterControl filterControl;
+
+ private BeatmapListingFilterControl filterControl => Header.FilterControl;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue)
@@ -60,12 +61,6 @@ namespace osu.Game.Overlays
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- filterControl = new BeatmapListingFilterControl
- {
- TypingStarted = onTypingStarted,
- SearchStarted = onSearchStarted,
- SearchFinished = onSearchFinished,
- },
new Container
{
AutoSizeAxes = Axes.Y,
@@ -88,6 +83,10 @@ namespace osu.Game.Overlays
},
}
};
+
+ filterControl.TypingStarted = onTypingStarted;
+ filterControl.SearchStarted = onSearchStarted;
+ filterControl.SearchFinished = onSearchFinished;
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
index 585e0dd1a2..104f861df7 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_spacing = 2;
private readonly OsuSpriteText version, starRating, starRatingText;
+ private readonly LinkFlowContainer guestMapperContainer;
private readonly FillFlowContainer starRatingContainer;
private readonly Statistic plays, favourites;
@@ -88,6 +89,14 @@ namespace osu.Game.Overlays.BeatmapSet
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)
},
+ guestMapperContainer = new LinkFlowContainer(s =>
+ s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding { Bottom = 1 },
+ },
starRatingContainer = new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
@@ -198,8 +207,21 @@ namespace osu.Game.Overlays.BeatmapSet
updateDifficultyButtons();
}
- private void showBeatmap(IBeatmapInfo? beatmapInfo)
+ private void showBeatmap(APIBeatmap? beatmapInfo)
{
+ guestMapperContainer.Clear();
+
+ if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID)
+ {
+ APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID);
+
+ if (user != null)
+ {
+ guestMapperContainer.AddText("mapped by ");
+ guestMapperContainer.AddUserLink(user);
+ }
+ }
+
version.Text = beatmapInfo?.DifficultyName ?? string.Empty;
}
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
index 26e6b1f158..7ff8352054 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
@@ -97,8 +97,8 @@ namespace osu.Game.Overlays.BeatmapSet
Padding = new MarginPadding
{
Vertical = BeatmapSetOverlay.Y_PADDING,
- Left = BeatmapSetOverlay.X_PADDING,
- Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
+ Left = WaveOverlayContainer.HORIZONTAL_PADDING,
+ Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
},
Children = new Drawable[]
{
@@ -170,7 +170,7 @@ namespace osu.Game.Overlays.BeatmapSet
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING },
+ Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs
index 58739eb471..8758b9c5cf 100644
--- a/osu.Game/Overlays/BeatmapSet/Info.cs
+++ b/osu.Game/Overlays/BeatmapSet/Info.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet
new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = 15, Horizontal = BeatmapSetOverlay.X_PADDING },
+ Padding = new MarginPadding { Top = 15, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
new Container
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 9eb04d9cc5..6d89313979 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
- Padding = new MarginPadding { Horizontal = 50 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Margin = new MarginPadding { Vertical = 20 },
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index 237ce22767..873336bb6e 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -25,7 +25,6 @@ namespace osu.Game.Overlays
{
public partial class BeatmapSetOverlay : OnlineOverlay
{
- public const float X_PADDING = 40;
public const float Y_PADDING = 25;
public const float RIGHT_WIDTH = 275;
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 96d5203d14..08978ac2ab 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -18,8 +18,6 @@ namespace osu.Game.Overlays.Changelog
{
public partial class ChangelogBuild : FillFlowContainer
{
- public const float HORIZONTAL_PADDING = 70;
-
public Action SelectBuild;
protected readonly APIChangelogBuild Build;
@@ -33,7 +31,7 @@ namespace osu.Game.Overlays.Changelog
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
- Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING };
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING };
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
index 54ada24987..e9be67e977 100644
--- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
@@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Changelog
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
- Horizontal = 65,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - ChangelogUpdateStreamItem.PADDING,
Vertical = 20
},
Child = Streams = new ChangelogUpdateStreamControl { Current = currentStream },
diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs
index d7c9ff67fe..4b784c7a28 100644
--- a/osu.Game/Overlays/Changelog/ChangelogListing.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Changelog
{
RelativeSizeAxes = Axes.X,
Height = 1,
- Padding = new MarginPadding { Horizontal = ChangelogBuild.HORIZONTAL_PADDING },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Margin = new MarginPadding { Top = 30 },
Child = new Box
{
diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs
index 04526eb7ba..4aded1dd59 100644
--- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Changelog
Padding = new MarginPadding
{
Vertical = 20,
- Horizontal = 50,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
};
}
@@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Changelog
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Padding = new MarginPadding { Right = 50 + image_container_width },
+ Padding = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING + image_container_width },
Children = new Drawable[]
{
new OsuSpriteText
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index c4e4700674..24536fe460 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Comments
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = 50, Vertical = 20 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 },
Children = new Drawable[]
{
avatar = new UpdateableAvatar(api.LocalUser.Value)
@@ -152,7 +152,7 @@ namespace osu.Game.Overlays.Comments
ShowDeleted = { BindTarget = ShowDeleted },
Margin = new MarginPadding
{
- Horizontal = 70,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 10
}
},
@@ -393,7 +393,7 @@ namespace osu.Game.Overlays.Comments
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Margin = new MarginPadding { Left = 50 },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Text = CommentsStrings.Empty
}
});
diff --git a/osu.Game/Overlays/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs
index e6d44e618b..0ae1f839a1 100644
--- a/osu.Game/Overlays/Comments/CommentsHeader.cs
+++ b/osu.Game/Overlays/Comments/CommentsHeader.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Comments
new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Horizontal = 50 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
new OverlaySortTabControl
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 397dd46cdc..a710406548 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -537,7 +537,7 @@ namespace osu.Game.Overlays.Comments
{
return new MarginPadding
{
- Horizontal = 70,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 15
};
}
diff --git a/osu.Game/Overlays/Comments/TotalCommentsCounter.cs b/osu.Game/Overlays/Comments/TotalCommentsCounter.cs
index 38928f6f3d..2065f7a76b 100644
--- a/osu.Game/Overlays/Comments/TotalCommentsCounter.cs
+++ b/osu.Game/Overlays/Comments/TotalCommentsCounter.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Margin = new MarginPadding { Left = 50 },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs
index 6cfa5cb9e8..dd418a9e58 100644
--- a/osu.Game/Overlays/Comments/VotePill.cs
+++ b/osu.Game/Overlays/Comments/VotePill.cs
@@ -132,11 +132,10 @@ namespace osu.Game.Overlays.Comments
},
sideNumber = new OsuSpriteText
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreRight,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
Text = "+1",
Font = OsuFont.GetFont(size: 14),
- Margin = new MarginPadding { Right = 3 },
Alpha = 0,
},
votesCounter = new OsuSpriteText
@@ -189,7 +188,7 @@ namespace osu.Game.Overlays.Comments
else
sideNumber.FadeTo(IsHovered ? 1 : 0);
- borderContainer.BorderThickness = IsHovered ? 3 : 0;
+ borderContainer.BorderThickness = IsHovered ? 2 : 0;
}
private void onHoverAction()
diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
index 1540aa8fbb..5047992c8b 100644
--- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Dashboard
new Container
{
RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding(padding),
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = padding },
Child = searchTextBox = new BasicSearchTextBox
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index 73fab6d62b..e3accfd2ad 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
Padding = new MarginPadding
{
Top = 20,
- Horizontal = 45
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - FriendsOnlineStatusItem.PADDING
},
Child = onlineStreamControl = new FriendOnlineStreamControl(),
}
@@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = 50 }
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }
},
loading = new LoadingLayer(true)
}
diff --git a/osu.Game/Overlays/News/Displays/ArticleListing.cs b/osu.Game/Overlays/News/Displays/ArticleListing.cs
index b6ce16ae7d..4fc9dde156 100644
--- a/osu.Game/Overlays/News/Displays/ArticleListing.cs
+++ b/osu.Game/Overlays/News/Displays/ArticleListing.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Overlays.News.Displays
{
Vertical = 20,
Left = 30,
- Right = 50
+ Right = WaveOverlayContainer.HORIZONTAL_PADDING
};
InternalChild = new FillFlowContainer
diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs
index 4fdf7cb2b6..4d2c6bc9d0 100644
--- a/osu.Game/Overlays/OnlineOverlay.cs
+++ b/osu.Game/Overlays/OnlineOverlay.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -22,6 +23,7 @@ namespace osu.Game.Overlays
protected readonly OverlayScrollContainer ScrollFlow;
protected readonly LoadingLayer Loading;
+ private readonly Container loadingContainer;
private readonly Container content;
protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true)
@@ -65,10 +67,22 @@ namespace osu.Game.Overlays
},
}
},
- Loading = new LoadingLayer(true)
+ loadingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = Loading = new LoadingLayer(true),
+ }
});
base.Content.Add(mainContent);
}
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ // don't block header by applying padding equal to the visible header height
+ loadingContainer.Padding = new MarginPadding { Top = Math.Max(0, Header.Height - ScrollFlow.Current) };
+ }
}
}
diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs
index f28d40c429..93de463204 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Overlays
}
});
- ContentSidePadding = 50;
+ ContentSidePadding = WaveOverlayContainer.HORIZONTAL_PADDING;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs
index b8c0032e87..93e5e83ffc 100644
--- a/osu.Game/Overlays/OverlaySidebar.cs
+++ b/osu.Game/Overlays/OverlaySidebar.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding
{
Vertical = 20,
- Left = 50,
+ Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Right = 30
},
Child = CreateContent()
diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs
index 8af2ab3823..5c51f5e4d0 100644
--- a/osu.Game/Overlays/OverlaySortTabControl.cs
+++ b/osu.Game/Overlays/OverlaySortTabControl.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Overlays
}
}
- protected partial class TabButton : HeaderButton
+ public partial class TabButton : HeaderButton
{
public readonly BindableBool Active = new BindableBool();
diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs
index 9b18e5cccf..45181c13e4 100644
--- a/osu.Game/Overlays/OverlayStreamItem.cs
+++ b/osu.Game/Overlays/OverlayStreamItem.cs
@@ -39,12 +39,14 @@ namespace osu.Game.Overlays
private FillFlowContainer text;
private ExpandingBar expandingBar;
+ public const float PADDING = 5;
+
protected OverlayStreamItem(T value)
: base(value)
{
Height = 50;
Width = 90;
- Margin = new MarginPadding(5);
+ Margin = new MarginPadding(PADDING);
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs
index 508041eb76..24be6ce2f5 100644
--- a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10, 10),
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Top = 10 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Top = 10 },
}
};
}
diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 1e80257a57..08a816930e 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
index 0dab4d582d..d964364510 100644
--- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Vertical = 10 },
- Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
@@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Header
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Right = UserProfileOverlay.CONTENT_X_MARGIN },
+ Margin = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
levelBadge = new LevelBadge
@@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Header
Origin = Anchor.CentreRight,
Width = 200,
Height = 6,
- Margin = new MarginPadding { Right = 50 },
+ Margin = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Child = new LevelProgressBar
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
index 1cc3aae735..1f35f39b49 100644
--- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Header
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 2f4f49788f..de678cb5d1 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@@ -31,6 +32,9 @@ namespace osu.Game.Overlays.Profile.Header
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private RankingsOverlay? rankingsOverlay { get; set; }
+
private UserCoverBackground cover = null!;
private SupporterIcon supporterTag = null!;
private UpdateableAvatar avatar = null!;
@@ -38,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header
private ExternalLinkButton openUserExternally = null!;
private OsuSpriteText titleText = null!;
private UpdateableFlag userFlag = null!;
+ private OsuHoverContainer userCountryContainer = null!;
private OsuSpriteText userCountryText = null!;
private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!;
@@ -83,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Header
Direction = FillDirection.Horizontal,
Padding = new MarginPadding
{
- Left = UserProfileOverlay.CONTENT_X_MARGIN,
+ Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = vertical_padding
},
Height = content_height + 2 * vertical_padding,
@@ -156,13 +161,17 @@ namespace osu.Game.Overlays.Profile.Header
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
- userCountryText = new OsuSpriteText
+ userCountryContainer = new OsuHoverContainer
{
- Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
- Margin = new MarginPadding { Left = 5 },
- Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
- }
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding { Left = 5 },
+ Child = userCountryText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
+ },
+ },
}
},
}
@@ -202,6 +211,7 @@ namespace osu.Game.Overlays.Profile.Header
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
userFlag.CountryCode = user?.CountryCode ?? default;
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
+ userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);
supporterTag.SupportLevel = user?.SupportLevel ?? 0;
titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 363eb5d58e..80d48ae09e 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Profile
public ProfileHeader()
{
- ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
+ ContentSidePadding = WaveOverlayContainer.HORIZONTAL_PADDING;
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs
index 4ac86924f8..a8a240ddde 100644
--- a/osu.Game/Overlays/Profile/ProfileSection.cs
+++ b/osu.Game/Overlays/Profile/ProfileSection.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Profile
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN - outer_gutter_width,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - outer_gutter_width,
Top = 20,
Bottom = 20,
},
@@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN - outer_gutter_width,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - outer_gutter_width,
Bottom = 20
}
},
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index 7b26640e50..1a44262ef8 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
set => valueText.Text = value.ToLocalisableString("N0");
}
- public CountSection(LocalisableString header)
+ protected CountSection(LocalisableString header)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
diff --git a/osu.Game/Overlays/Rankings/CountryFilter.cs b/osu.Game/Overlays/Rankings/CountryFilter.cs
index e27fa7c7bd..525816f8fd 100644
--- a/osu.Game/Overlays/Rankings/CountryFilter.cs
+++ b/osu.Game/Overlays/Rankings/CountryFilter.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Rankings
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
- Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
new OsuSpriteText
diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
index 31273e3b01..190da04a5d 100644
--- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Rankings
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
index affd9a2c44..27d894cdc2 100644
--- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
@@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Rankings.Tables
public abstract partial class RankingsTable : TableContainer
{
protected const int TEXT_SIZE = 12;
- private const float horizontal_inset = 20;
private const float row_height = 32;
private const float row_spacing = 3;
private const int items_per_page = 50;
@@ -39,7 +38,7 @@ namespace osu.Game.Overlays.Rankings.Tables
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Padding = new MarginPadding { Horizontal = horizontal_inset };
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING };
RowSize = new Dimension(GridSizeMode.Absolute, row_height + row_spacing);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 523b1237fa..0515e8dc97 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -33,7 +33,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private SettingsSlider dimSlider = null!;
private readonly Bindable currentDisplay = new Bindable();
- private readonly IBindableList windowModes = new BindableList();
private Bindable scalingMode = null!;
private Bindable sizeFullscreen = null!;
@@ -79,7 +78,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
if (window != null)
{
currentDisplay.BindTo(window.CurrentDisplayBindable);
- windowModes.BindTo(window.SupportedWindowModes);
window.DisplaysChanged += onDisplaysChanged;
}
@@ -91,7 +89,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown = new SettingsDropdown
{
LabelText = GraphicsSettingsStrings.ScreenMode,
- ItemSource = windowModes,
+ Items = window?.SupportedWindowModes,
+ CanBeShown = { Value = window?.SupportedWindowModes.Count() > 1 },
Current = config.GetBindable(FrameworkSetting.WindowMode),
},
displayDropdown = new DisplaySettingsDropdown
@@ -193,8 +192,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
updateScreenModeWarning();
}, true);
- windowModes.BindCollectionChanged((_, _) => updateDisplaySettingsVisibility());
-
currentDisplay.BindValueChanged(display => Schedule(() =>
{
resolutions.RemoveRange(1, resolutions.Count - 1);
@@ -254,7 +251,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateDisplaySettingsVisibility()
{
- windowModeDropdown.CanBeShown.Value = windowModes.Count > 1;
resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1;
safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero;
@@ -278,7 +274,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
return;
}
- if (host.Window is WindowsWindow)
+ if (host.Renderer is IWindowsRenderer)
{
switch (fullscreenCapability.Value)
{
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index f21ef0ee98..93294a9d30 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar
protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All);
// Toolbar and its components need keyboard input even when hidden.
- public override bool PropagateNonPositionalInputSubTree => true;
+ public override bool PropagateNonPositionalInputSubTree => OverlayActivationMode.Value != OverlayActivation.Disabled;
public Toolbar()
{
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index c5f8a820ea..d1fe877e55 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -45,8 +45,6 @@ namespace osu.Game.Overlays
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
- public const float CONTENT_X_MARGIN = 50;
-
public UserProfileOverlay()
: base(OverlayColourScheme.Pink)
{
@@ -184,7 +182,7 @@ namespace osu.Game.Overlays
public ProfileSectionTabControl()
{
Height = 40;
- Padding = new MarginPadding { Horizontal = CONTENT_X_MARGIN };
+ Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING };
TabContainer.Spacing = new Vector2(20);
}
diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs
index d25f6a9ae5..00474cc0d8 100644
--- a/osu.Game/Overlays/WaveOverlayContainer.cs
+++ b/osu.Game/Overlays/WaveOverlayContainer.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Overlays
protected override string PopInSampleName => "UI/wave-pop-in";
+ public const float HORIZONTAL_PADDING = 50;
+
protected WaveOverlayContainer()
{
AddInternal(Waves = new WaveContainer
diff --git a/osu.Game/Overlays/Wiki/WikiArticlePage.cs b/osu.Game/Overlays/Wiki/WikiArticlePage.cs
index 6c1dbe3181..342a395871 100644
--- a/osu.Game/Overlays/Wiki/WikiArticlePage.cs
+++ b/osu.Game/Overlays/Wiki/WikiArticlePage.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Wiki
{
Vertical = 20,
Left = 30,
- Right = 50,
+ Right = WaveOverlayContainer.HORIZONTAL_PADDING,
},
OnAddHeading = sidebar.AddEntry,
}
diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs
index 88dc2cd7a4..2444aa4fa2 100644
--- a/osu.Game/Overlays/WikiOverlay.cs
+++ b/osu.Game/Overlays/WikiOverlay.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding
{
Vertical = 20,
- Horizontal = 50,
+ Horizontal = HORIZONTAL_PADDING,
},
});
}
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index 0df481737e..09c6af3820 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -11,8 +11,6 @@ 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.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -47,8 +45,6 @@ namespace osu.Game.Rulesets.Edit
IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
- protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
-
private ExpandableSlider> distanceSpacingSlider;
private ExpandableButton currentDistanceSpacingButton;
@@ -67,47 +63,29 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
- AddInternal(new Container
+ RightToolbox.Add(new EditorToolboxGroup("snapping")
{
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
+ Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Children = new Drawable[]
{
- new Box
+ distanceSpacingSlider = new ExpandableSlider>
{
- Colour = colourProvider.Background5,
- RelativeSizeAxes = Axes.Both,
+ KeyboardStep = adjust_step,
+ // Manual binding in LoadComplete to handle one-way event flow.
+ Current = DistanceSpacingMultiplier.GetUnboundCopy(),
},
- RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
+ currentDistanceSpacingButton = new ExpandableButton
{
- Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
- Child = new EditorToolboxGroup("snapping")
+ Action = () =>
{
- Children = new Drawable[]
- {
- distanceSpacingSlider = new ExpandableSlider>
- {
- KeyboardStep = adjust_step,
- // Manual binding in LoadComplete to handle one-way event flow.
- Current = DistanceSpacingMultiplier.GetUnboundCopy(),
- },
- currentDistanceSpacingButton = new ExpandableButton
- {
- Action = () =>
- {
- (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
+ (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
- Debug.Assert(objects != null);
+ Debug.Assert(objects != null);
- DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
- DistanceSnapToggle.Value = TernaryState.True;
- },
- RelativeSizeAxes = Axes.X,
- }
- }
- }
+ DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
+ DistanceSnapToggle.Value = TernaryState.True;
+ },
+ RelativeSizeAxes = Axes.X,
}
}
});
@@ -115,7 +93,7 @@ namespace osu.Game.Rulesets.Edit
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
{
- HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject;
+ HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject;
if (lastBefore == null)
return null;
@@ -261,7 +239,8 @@ namespace osu.Game.Rulesets.Edit
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
{
- return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
+ / BeatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index aee86fd942..653861c11c 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -58,8 +58,15 @@ namespace osu.Game.Rulesets.Edit
[Resolved]
protected IBeatSnapProvider BeatSnapProvider { get; private set; }
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
+ protected ExpandingToolboxContainer LeftToolbox { get; private set; }
+
+ protected ExpandingToolboxContainer RightToolbox { get; private set; }
+
private DrawableEditorRulesetWrapper drawableRulesetWrapper;
protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
@@ -82,7 +89,7 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
+ private void load(OsuConfigManager config)
{
autoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement);
@@ -131,7 +138,7 @@ namespace osu.Game.Rulesets.Edit
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.Both,
},
- new ExpandingToolboxContainer(60, 200)
+ LeftToolbox = new ExpandingToolboxContainer(60, 200)
{
Children = new Drawable[]
{
@@ -153,6 +160,28 @@ namespace osu.Game.Rulesets.Edit
},
}
},
+ new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ RightToolbox = new ExpandingToolboxContainer(130, 250)
+ {
+ Child = new EditorToolboxGroup("inspector")
+ {
+ Child = new HitObjectInspector()
+ },
+ }
+ }
+ }
};
toolboxCollection.Items = CompositionTools
diff --git a/osu.Game/Rulesets/Edit/HitObjectInspector.cs b/osu.Game/Rulesets/Edit/HitObjectInspector.cs
new file mode 100644
index 0000000000..977d00ede2
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/HitObjectInspector.cs
@@ -0,0 +1,146 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Threading;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Rulesets.Edit
+{
+ internal partial class HitObjectInspector : CompositeDrawable
+ {
+ private OsuTextFlowContainer inspectorText = null!;
+
+ [Resolved]
+ protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+
+ InternalChild = inspectorText = new OsuTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
+ EditorBeatmap.TransactionBegan += updateInspectorText;
+ EditorBeatmap.TransactionEnded += updateInspectorText;
+ updateInspectorText();
+ }
+
+ private ScheduledDelegate? rollingTextUpdate;
+
+ private void updateInspectorText()
+ {
+ inspectorText.Clear();
+ rollingTextUpdate?.Cancel();
+ rollingTextUpdate = null;
+
+ switch (EditorBeatmap.SelectedHitObjects.Count)
+ {
+ case 0:
+ addValue("No selection");
+ break;
+
+ case 1:
+ var selected = EditorBeatmap.SelectedHitObjects.Single();
+
+ addHeader("Type");
+ addValue($"{selected.GetType().ReadableName()}");
+
+ addHeader("Time");
+ addValue($"{selected.StartTime:#,0.##}ms");
+
+ switch (selected)
+ {
+ case IHasPosition pos:
+ addHeader("Position");
+ addValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
+ break;
+
+ case IHasXPosition x:
+ addHeader("Position");
+
+ addValue($"x:{x.X:#,0.##} ");
+ break;
+
+ case IHasYPosition y:
+ addHeader("Position");
+
+ addValue($"y:{y.Y:#,0.##}");
+ break;
+ }
+
+ if (selected is IHasDistance distance)
+ {
+ addHeader("Distance");
+ addValue($"{distance.Distance:#,0.##}px");
+ }
+
+ if (selected is IHasRepeats repeats)
+ {
+ addHeader("Repeats");
+ addValue($"{repeats.RepeatCount:#,0.##}");
+ }
+
+ if (selected is IHasDuration duration)
+ {
+ addHeader("End Time");
+ addValue($"{duration.EndTime:#,0.##}ms");
+ addHeader("Duration");
+ addValue($"{duration.Duration:#,0.##}ms");
+ }
+
+ // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
+ // This is a good middle-ground for the time being.
+ rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
+ break;
+
+ default:
+ addHeader("Selected Objects");
+ addValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
+
+ addHeader("Start Time");
+ addValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
+
+ addHeader("End Time");
+ addValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
+ break;
+ }
+
+ void addHeader(string header) => inspectorText.AddParagraph($"{header}: ", s =>
+ {
+ s.Padding = new MarginPadding { Top = 2 };
+ s.Font = s.Font.With(size: 12);
+ s.Colour = colourProvider.Content2;
+ });
+
+ void addValue(string value) => inspectorText.AddParagraph(value, s =>
+ {
+ s.Font = s.Font.With(weight: FontWeight.SemiBold);
+ s.Colour = colourProvider.Content1;
+ });
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index 9e4469bf25..733610c040 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -24,5 +24,22 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 2,
Precision = 0.01,
};
+
+ public override double ScoreMultiplier
+ {
+ get
+ {
+ // Round to the nearest multiple of 0.1.
+ double value = (int)(SpeedChange.Value * 10) / 10.0;
+
+ // Offset back to 0.
+ value -= 1;
+
+ // Each 0.1 multiple changes score multiplier by 0.02.
+ value /= 5;
+
+ return 1 + value;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
index 4425ece513..97789b7f5a 100644
--- a/osu.Game/Rulesets/Mods/ModFailCondition.cs
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -20,11 +20,31 @@ namespace osu.Game.Rulesets.Mods
public virtual bool RestartOnFail => Restart.Value;
+ private Action? triggerFailureDelegate;
+
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
+ triggerFailureDelegate = healthProcessor.TriggerFailure;
healthProcessor.FailConditions += FailCondition;
}
+ ///
+ /// Immediately triggers a failure on the loaded .
+ ///
+ protected void TriggerFailure() => triggerFailureDelegate?.Invoke();
+
+ ///
+ /// Determines whether should trigger a failure. Called every time a
+ /// judgement is applied to .
+ ///
+ /// The loaded .
+ /// The latest .
+ /// Whether the fail condition has been met.
+ ///
+ /// This method should only be used to trigger failures based on .
+ /// Using outside values to evaluate failure may introduce event ordering discrepancies, use
+ /// an with instead.
+ ///
protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 7d858dca6f..06c7750035 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -24,5 +24,19 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 0.99,
Precision = 0.01,
};
+
+ public override double ScoreMultiplier
+ {
+ get
+ {
+ // Round to the nearest multiple of 0.1.
+ double value = (int)(SpeedChange.Value * 10) / 10.0;
+
+ // Offset back to 0.
+ value -= 1;
+
+ return 1 + value;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 0c50f8341a..f6c3452e48 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnNestedDrawableCreated?.Invoke(drawableNested);
drawableNested.OnNewResult += onNewResult;
+ drawableNested.OnRevertResult += onNestedRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
// This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
@@ -312,6 +313,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var obj in nestedHitObjects)
{
obj.OnNewResult -= onNewResult;
+ obj.OnRevertResult -= onNestedRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
@@ -376,6 +378,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnRevertResult?.Invoke(this, Result);
}
+ private void onNestedRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
+
private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state);
private void onDefaultsApplied(HitObject hitObject)
diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
index b70ddd5e24..3e0b6433c2 100644
--- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
@@ -31,6 +31,15 @@ namespace osu.Game.Rulesets.Scoring
///
public bool HasFailed { get; private set; }
+ ///
+ /// Immediately triggers a failure for this HealthProcessor.
+ ///
+ public void TriggerFailure()
+ {
+ if (Failed?.Invoke() != false)
+ HasFailed = true;
+ }
+
protected override void ApplyResultInternal(JudgementResult result)
{
result.HealthAtJudgement = Health.Value;
@@ -42,10 +51,7 @@ namespace osu.Game.Rulesets.Scoring
Health.Value += GetHealthIncreaseFor(result);
if (meetsAnyFailCondition(result))
- {
- if (Failed?.Invoke() != false)
- HasFailed = true;
- }
+ TriggerFailure();
}
protected override void RevertResultInternal(JudgementResult result)
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 64fe9c8a86..4f22c0c617 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 96b02ee4dc..e34289c968 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -25,21 +25,28 @@ namespace osu.Game.Rulesets.UI
///
/// The texture store to be used for the ruleset.
///
+ ///
+ /// Reads textures from the "Textures" folder in ruleset resources.
+ /// If not available locally, lookups will fallback to the global texture store.
+ ///
public TextureStore TextureStore { get; }
///
/// The sample store to be used for the ruleset.
///
///
- /// This is the local sample store pointing to the ruleset sample resources,
- /// the cached sample store () retrieves from
- /// this store and falls back to the parent store if this store doesn't have the requested sample.
+ /// Reads samples from the "Samples" folder in ruleset resources.
+ /// If not available locally, lookups will fallback to the global sample store.
///
public ISampleStore SampleStore { get; }
///
/// The shader manager to be used for the ruleset.
///
+ ///
+ /// Reads shaders from the "Shaders" folder in ruleset resources.
+ /// If not available locally, lookups will fallback to the global shader manager.
+ ///
public ShaderManager ShaderManager { get; }
///
@@ -61,8 +68,7 @@ namespace osu.Game.Rulesets.UI
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get()));
- ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders"));
- CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get()));
+ CacheAs(ShaderManager = new RulesetShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders"), parent.Get()));
RulesetConfigManager = parent.Get().GetConfigFor(ruleset);
if (RulesetConfigManager != null)
@@ -92,7 +98,7 @@ namespace osu.Game.Rulesets.UI
isDisposed = true;
- if (ShaderManager.IsNotNull()) SampleStore.Dispose();
+ if (SampleStore.IsNotNull()) SampleStore.Dispose();
if (TextureStore.IsNotNull()) TextureStore.Dispose();
if (ShaderManager.IsNotNull()) ShaderManager.Dispose();
}
@@ -190,25 +196,21 @@ namespace osu.Game.Rulesets.UI
}
}
- private class FallbackShaderManager : ShaderManager
+ private class RulesetShaderManager : ShaderManager
{
- private readonly ShaderManager primary;
- private readonly ShaderManager fallback;
+ private readonly ShaderManager parent;
- public FallbackShaderManager(IRenderer renderer, ShaderManager primary, ShaderManager fallback)
- : base(renderer, new ResourceStore())
+ public RulesetShaderManager(IRenderer renderer, NamespacedResourceStore rulesetResources, ShaderManager parent)
+ : base(renderer, rulesetResources)
{
- this.primary = primary;
- this.fallback = fallback;
+ this.parent = parent;
}
- public override byte[]? LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
+ public override IShader? GetCachedShader(string vertex, string fragment) => base.GetCachedShader(vertex, fragment) ?? parent.GetCachedShader(vertex, fragment);
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (primary.IsNotNull()) primary.Dispose();
- }
+ public override IShaderPart? GetCachedShaderPart(string name) => base.GetCachedShaderPart(name) ?? parent.GetCachedShaderPart(name);
+
+ public override byte[]? GetRawData(string fileName) => base.GetRawData(fileName) ?? parent.GetRawData(fileName);
}
}
}
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 7bf0482673..2ae54a3afe 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -19,7 +19,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler;
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.UI
.Select(b => b.GetAction())
.Distinct()
.OrderBy(action => action)
- .Select(action => new KeyCounterAction(action)));
+ .Select(action => new KeyCounterActionTrigger(action)));
}
private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler
@@ -179,11 +179,14 @@ namespace osu.Game.Rulesets.UI
{
}
- public bool OnPressed(KeyBindingPressEvent e) => Target.Children.OfType>().Any(c => c.OnPressed(e.Action, Clock.Rate >= 0));
+ public bool OnPressed(KeyBindingPressEvent e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger)
+ .Select(c => (KeyCounterActionTrigger)c.Trigger)
+ .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0));
public void OnReleased(KeyBindingReleaseEvent e)
{
- foreach (var c in Target.Children.OfType>())
+ foreach (var c
+ in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger).Select(c => (KeyCounterActionTrigger)c.Trigger))
c.OnReleased(e.Action, Clock.Rate >= 0);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs
index 0f702e1c49..c2a3f12efd 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs
@@ -7,14 +7,15 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
-using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
+using Key = osuTK.Input.Key;
namespace osu.Game.Screens.Edit.Compose.Components
{
@@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private SpriteIcon icon;
+ private const float snap_step = 15;
+
private readonly Bindable cumulativeRotation = new Bindable();
[Resolved]
@@ -50,18 +53,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
});
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- cumulativeRotation.BindValueChanged(_ => updateTooltipText(), true);
- }
-
protected override void UpdateHoverState()
{
base.UpdateHoverState();
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
+ private float rawCumulativeRotation;
+
protected override bool OnDragStart(DragStartEvent e)
{
bool handle = base.OnDragStart(e);
@@ -74,21 +73,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.OnDrag(e);
- float instantaneousAngle = convertDragEventToAngleOfRotation(e);
- cumulativeRotation.Value += instantaneousAngle;
+ rawCumulativeRotation += convertDragEventToAngleOfRotation(e);
- if (cumulativeRotation.Value < -180)
- cumulativeRotation.Value += 360;
- else if (cumulativeRotation.Value > 180)
- cumulativeRotation.Value -= 360;
+ applyRotation(shouldSnap: e.ShiftPressed);
+ }
- HandleRotate?.Invoke(instantaneousAngle);
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight))
+ {
+ applyRotation(shouldSnap: true);
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ base.OnKeyUp(e);
+
+ if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight))
+ applyRotation(shouldSnap: false);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
cumulativeRotation.Value = null;
+ rawCumulativeRotation = 0;
+ TooltipText = default;
}
private float convertDragEventToAngleOfRotation(DragEvent e)
@@ -100,9 +114,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
return (endAngle - startAngle) * 180 / MathF.PI;
}
- private void updateTooltipText()
+ private void applyRotation(bool shouldSnap)
{
- TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default;
+ float oldRotation = cumulativeRotation.Value ?? 0;
+
+ float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
+ newRotation = (newRotation - 180) % 360 + 180;
+
+ cumulativeRotation.Value = newRotation;
+
+ HandleRotate?.Invoke(newRotation - oldRotation);
+ TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation);
}
+
+ private float snap(float value, float step) => MathF.Round(value / step) * step;
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index d89392f757..b5d304a031 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -210,7 +210,10 @@ namespace osu.Game.Screens.Edit
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
// the editor has already been exited.
if (!ValidForPush)
+ {
+ beatmapManager.Delete(loadableBeatmap.BeatmapSetInfo);
return;
+ }
}
try
diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs
index b70c1f7ddf..372cfe748e 100644
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -130,6 +130,8 @@ namespace osu.Game.Screens
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
+ loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
+
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}
diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs
new file mode 100644
index 0000000000..6818b30823
--- /dev/null
+++ b/osu.Game/Screens/Play/ArgonKeyCounter.cs
@@ -0,0 +1,76 @@
+// 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.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+
+namespace osu.Game.Screens.Play
+{
+ public partial class ArgonKeyCounter : KeyCounter
+ {
+ private Circle inputIndicator = null!;
+ private OsuSpriteText countText = null!;
+
+ // These values were taken from Figma
+ private const float line_height = 3;
+ private const float name_font_size = 10;
+ private const float count_font_size = 14;
+
+ // Make things look bigger without using Scale
+ private const float scale_factor = 1.5f;
+
+ public ArgonKeyCounter(InputTrigger trigger)
+ : base(trigger)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Children = new Drawable[]
+ {
+ inputIndicator = new Circle
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = line_height * scale_factor,
+ Alpha = 0.5f
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Position = new Vector2(0, -13) * scale_factor,
+ Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold),
+ Colour = colours.Blue0,
+ Text = Trigger.Name
+ },
+ countText = new OsuSpriteText
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold),
+ },
+ };
+
+ // Values from Figma didn't match visually
+ // So these were just eyeballed
+ Height = 30 * scale_factor;
+ Width = 35 * scale_factor;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ IsActive.BindValueChanged(e => inputIndicator.Alpha = e.NewValue ? 1 : 0.5f, true);
+ CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs
new file mode 100644
index 0000000000..984c2a7287
--- /dev/null
+++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+
+namespace osu.Game.Screens.Play
+{
+ public partial class ArgonKeyCounterDisplay : KeyCounterDisplay
+ {
+ private const int duration = 100;
+
+ protected override FillFlowContainer KeyFlow { get; }
+
+ public ArgonKeyCounterDisplay()
+ {
+ InternalChild = KeyFlow = new FillFlowContainer
+ {
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Alpha = 0,
+ Spacing = new Vector2(2),
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Size = KeyFlow.Size;
+ }
+
+ protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger);
+
+ protected override void UpdateVisibility()
+ => KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
+ }
+}
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index 0214d33549..57bdad079e 100644
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -1,15 +1,13 @@
// 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.Audio;
-using osu.Framework.Bindables;
-using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
using ManagedBass.Fx;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Sample;
+using osu.Framework.Audio;
using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -21,6 +19,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -50,8 +49,7 @@ namespace osu.Game.Screens.Play
private const float duration = 2500;
- private ISample? failSample;
- private SampleChannel? failSampleChannel;
+ private SkinnableSound failSample = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@@ -76,10 +74,10 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
- private void load(AudioManager audio, ISkinSource skin, IBindable beatmap)
+ private void load(AudioManager audio, IBindable beatmap)
{
track = beatmap.Value.Track;
- failSample = skin.GetSample(new SampleInfo(@"Gameplay/failsound"));
+ AddInternal(failSample = new SkinnableSound(new SampleInfo("Gameplay/failsound")));
AddRangeInternal(new Drawable[]
{
@@ -126,7 +124,7 @@ namespace osu.Game.Screens.Play
failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
- failSampleChannel = failSample?.Play();
+ failSample.Play();
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
@@ -159,7 +157,7 @@ namespace osu.Game.Screens.Play
///
public void Stop()
{
- failSampleChannel?.Stop();
+ failSample.Stop();
removeFilters();
}
diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
similarity index 69%
rename from osu.Game/Screens/Play/KeyCounter.cs
rename to osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
index 4405542b3b..f7ac72035f 100644
--- a/osu.Game/Screens/Play/KeyCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
@@ -1,8 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -13,70 +11,23 @@ using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public abstract partial class KeyCounter : Container
+ public partial class DefaultKeyCounter : KeyCounter
{
- private Sprite buttonSprite;
- private Sprite glowSprite;
- private Container textLayer;
- private SpriteText countSpriteText;
-
- public bool IsCounting { get; set; } = true;
- private int countPresses;
-
- public int CountPresses
- {
- get => countPresses;
- private set
- {
- if (countPresses != value)
- {
- countPresses = value;
- countSpriteText.Text = value.ToString(@"#,0");
- }
- }
- }
-
- private bool isLit;
-
- public bool IsLit
- {
- get => isLit;
- protected set
- {
- if (isLit != value)
- {
- isLit = value;
- updateGlowSprite(value);
- }
- }
- }
-
- public void Increment()
- {
- if (!IsCounting)
- return;
-
- CountPresses++;
- }
-
- public void Decrement()
- {
- if (!IsCounting)
- return;
-
- CountPresses--;
- }
+ private Sprite buttonSprite = null!;
+ private Sprite glowSprite = null!;
+ private Container textLayer = null!;
+ private SpriteText countSpriteText = null!;
//further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor
public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray;
public Color4 KeyUpTextColor { get; set; } = Color4.White;
public double FadeTime { get; set; }
- protected KeyCounter(string name)
+ public DefaultKeyCounter(InputTrigger trigger)
+ : base(trigger)
{
- Name = name;
}
[BackgroundDependencyLoader(true)]
@@ -106,7 +57,7 @@ namespace osu.Game.Screens.Play
{
new OsuSpriteText
{
- Text = Name,
+ Text = Trigger.Name,
Font = OsuFont.Numeric.With(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -116,7 +67,7 @@ namespace osu.Game.Screens.Play
},
countSpriteText = new OsuSpriteText
{
- Text = CountPresses.ToString(@"#,0"),
+ Text = CountPresses.Value.ToString(@"#,0"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
@@ -130,6 +81,9 @@ namespace osu.Game.Screens.Play
// so the size can be changing between buttonSprite and glowSprite.
Height = buttonSprite.DrawHeight;
Width = buttonSprite.DrawWidth;
+
+ IsActive.BindValueChanged(e => updateGlowSprite(e.NewValue), true);
+ CountPresses.BindValueChanged(e => countSpriteText.Text = e.NewValue.ToString(@"#,0"), true);
}
private void updateGlowSprite(bool show)
diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
new file mode 100644
index 0000000000..e459574243
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
@@ -0,0 +1,80 @@
+// 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 osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public partial class DefaultKeyCounterDisplay : KeyCounterDisplay
+ {
+ private const int duration = 100;
+ private const double key_fade_time = 80;
+
+ protected override FillFlowContainer KeyFlow { get; }
+
+ public DefaultKeyCounterDisplay()
+ {
+ InternalChild = KeyFlow = new FillFlowContainer
+ {
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Alpha = 0,
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Don't use autosize as it will shrink to zero when KeyFlow is hidden.
+ // In turn this can cause the display to be masked off screen and never become visible again.
+ Size = KeyFlow.Size;
+ }
+
+ protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger)
+ {
+ FadeTime = key_fade_time,
+ KeyDownTextColor = KeyDownTextColor,
+ KeyUpTextColor = KeyUpTextColor,
+ };
+
+ protected override void UpdateVisibility() =>
+ // Isolate changing visibility of the key counters from fading this component.
+ KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
+
+ private Color4 keyDownTextColor = Color4.DarkGray;
+
+ public Color4 KeyDownTextColor
+ {
+ get => keyDownTextColor;
+ set
+ {
+ if (value != keyDownTextColor)
+ {
+ keyDownTextColor = value;
+ foreach (var child in KeyFlow.Cast())
+ child.KeyDownTextColor = value;
+ }
+ }
+ }
+
+ private Color4 keyUpTextColor = Color4.White;
+
+ public Color4 KeyUpTextColor
+ {
+ get => keyUpTextColor;
+ set
+ {
+ if (value != keyUpTextColor)
+ {
+ keyUpTextColor = value;
+ foreach (var child in KeyFlow.Cast())
+ child.KeyUpTextColor = value;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/InputTrigger.cs b/osu.Game/Screens/Play/HUD/InputTrigger.cs
new file mode 100644
index 0000000000..b57f2cdf91
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/InputTrigger.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An event trigger which can be used with to create visual tracking of button/key presses.
+ ///
+ public abstract partial class InputTrigger : Component
+ {
+ ///
+ /// Callback to invoke when the associated input has been activated.
+ ///
+ /// Whether gameplay is progressing in the forward direction time-wise.
+ public delegate void OnActivateCallback(bool forwardPlayback);
+
+ ///
+ /// Callback to invoke when the associated input has been deactivated.
+ ///
+ /// Whether gameplay is progressing in the forward direction time-wise.
+ public delegate void OnDeactivateCallback(bool forwardPlayback);
+
+ public event OnActivateCallback? OnActivate;
+ public event OnDeactivateCallback? OnDeactivate;
+
+ protected InputTrigger(string name)
+ {
+ Name = name;
+ }
+
+ protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback);
+
+ protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs
new file mode 100644
index 0000000000..7cdd6b025f
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs
@@ -0,0 +1,96 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An individual key display which is intended to be displayed within a .
+ ///
+ public abstract partial class KeyCounter : Container
+ {
+ ///
+ /// The which activates and deactivates this .
+ ///
+ public readonly InputTrigger Trigger;
+
+ ///
+ /// Whether the actions reported by should be counted.
+ ///
+ public Bindable IsCounting { get; } = new BindableBool(true);
+
+ private readonly Bindable countPresses = new BindableInt
+ {
+ MinValue = 0
+ };
+
+ ///
+ /// The current count of registered key presses.
+ ///
+ public IBindable CountPresses => countPresses;
+
+ private readonly Container content;
+
+ protected override Container Content => content;
+
+ ///
+ /// Whether this is currently in the "activated" state because the associated key is currently pressed.
+ ///
+ protected readonly Bindable IsActive = new BindableBool();
+
+ protected KeyCounter(InputTrigger trigger)
+ {
+ InternalChildren = new Drawable[]
+ {
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ Trigger = trigger,
+ };
+
+ Trigger.OnActivate += Activate;
+ Trigger.OnDeactivate += Deactivate;
+ }
+
+ private void increment()
+ {
+ if (!IsCounting.Value)
+ return;
+
+ countPresses.Value++;
+ }
+
+ private void decrement()
+ {
+ if (!IsCounting.Value)
+ return;
+
+ countPresses.Value--;
+ }
+
+ protected virtual void Activate(bool forwardPlayback = true)
+ {
+ IsActive.Value = true;
+ if (forwardPlayback)
+ increment();
+ }
+
+ protected virtual void Deactivate(bool forwardPlayback = true)
+ {
+ IsActive.Value = false;
+ if (!forwardPlayback)
+ decrement();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ Trigger.OnActivate -= Activate;
+ Trigger.OnDeactivate -= Deactivate;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/KeyCounterAction.cs b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs
similarity index 70%
rename from osu.Game/Screens/Play/KeyCounterAction.cs
rename to osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs
index 900d9bcd0e..e5951a8bf4 100644
--- a/osu.Game/Screens/Play/KeyCounterAction.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs
@@ -1,18 +1,16 @@
// 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;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public partial class KeyCounterAction : KeyCounter
+ public partial class KeyCounterActionTrigger : InputTrigger
where T : struct
{
public T Action { get; }
- public KeyCounterAction(T action)
+ public KeyCounterActionTrigger(T action)
: base($"B{(int)(object)action + 1}")
{
Action = action;
@@ -23,9 +21,7 @@ namespace osu.Game.Screens.Play
if (!EqualityComparer.Default.Equals(action, Action))
return false;
- IsLit = true;
- if (forwards)
- Increment();
+ Activate(forwards);
return false;
}
@@ -34,9 +30,7 @@ namespace osu.Game.Screens.Play
if (!EqualityComparer.Default.Equals(action, Action))
return;
- IsLit = false;
- if (!forwards)
- Decrement();
+ Deactivate(forwards);
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
new file mode 100644
index 0000000000..05427d3a32
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
@@ -0,0 +1,120 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// A flowing display of all gameplay keys. Individual keys can be added using implementations.
+ ///
+ public abstract partial class KeyCounterDisplay : CompositeDrawable
+ {
+ ///
+ /// Whether the key counter should be visible regardless of the configuration value.
+ /// This is true by default, but can be changed.
+ ///
+ public Bindable AlwaysVisible { get; } = new Bindable(true);
+
+ ///
+ /// The s contained in this .
+ ///
+ public IEnumerable Counters => KeyFlow;
+
+ protected abstract FillFlowContainer KeyFlow { get; }
+
+ ///
+ /// Whether the actions reported by all s within this should be counted.
+ ///
+ public Bindable IsCounting { get; } = new BindableBool(true);
+
+ protected readonly Bindable ConfigVisibility = new Bindable();
+
+ protected abstract void UpdateVisibility();
+
+ private Receptor? receptor;
+
+ public void SetReceptor(Receptor receptor)
+ {
+ if (this.receptor != null)
+ throw new InvalidOperationException("Cannot set a new receptor when one is already active");
+
+ this.receptor = receptor;
+ }
+
+ ///
+ /// Add a to this display.
+ ///
+ public void Add(InputTrigger trigger)
+ {
+ var keyCounter = CreateCounter(trigger);
+
+ KeyFlow.Add(keyCounter);
+
+ IsCounting.BindTo(keyCounter.IsCounting);
+ }
+
+ ///
+ /// Add a range of to this display.
+ ///
+ public void AddRange(IEnumerable triggers) => triggers.ForEach(Add);
+
+ protected abstract KeyCounter CreateCounter(InputTrigger trigger);
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AlwaysVisible.BindValueChanged(_ => UpdateVisibility());
+ ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true);
+ }
+
+ public override bool HandleNonPositionalInput => receptor == null;
+
+ public override bool HandlePositionalInput => receptor == null;
+
+ public partial class Receptor : Drawable
+ {
+ protected readonly KeyCounterDisplay Target;
+
+ public Receptor(KeyCounterDisplay target)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Depth = float.MinValue;
+ Target = target;
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
+ protected override bool Handle(UIEvent e)
+ {
+ switch (e)
+ {
+ case KeyDownEvent:
+ case KeyUpEvent:
+ case MouseDownEvent:
+ case MouseUpEvent:
+ return Target.InternalChildren.Any(c => c.TriggerEvent(e));
+ }
+
+ return base.Handle(e);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/KeyCounterKeyboard.cs b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs
similarity index 70%
rename from osu.Game/Screens/Play/KeyCounterKeyboard.cs
rename to osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs
index c5c8b7eeae..3052c1e666 100644
--- a/osu.Game/Screens/Play/KeyCounterKeyboard.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs
@@ -1,18 +1,16 @@
// 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.Input.Events;
using osuTK.Input;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public partial class KeyCounterKeyboard : KeyCounter
+ public partial class KeyCounterKeyboardTrigger : InputTrigger
{
public Key Key { get; }
- public KeyCounterKeyboard(Key key)
+ public KeyCounterKeyboardTrigger(Key key)
: base(key.ToString())
{
Key = key;
@@ -22,8 +20,7 @@ namespace osu.Game.Screens.Play
{
if (e.Key == Key)
{
- IsLit = true;
- Increment();
+ Activate();
}
return base.OnKeyDown(e);
@@ -31,7 +28,9 @@ namespace osu.Game.Screens.Play
protected override void OnKeyUp(KeyUpEvent e)
{
- if (e.Key == Key) IsLit = false;
+ if (e.Key == Key)
+ Deactivate();
+
base.OnKeyUp(e);
}
}
diff --git a/osu.Game/Screens/Play/KeyCounterMouse.cs b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs
similarity index 79%
rename from osu.Game/Screens/Play/KeyCounterMouse.cs
rename to osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs
index cf9c7c029f..369aaa9f74 100644
--- a/osu.Game/Screens/Play/KeyCounterMouse.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs
@@ -1,19 +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 osu.Framework.Input.Events;
-using osuTK.Input;
using osuTK;
+using osuTK.Input;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public partial class KeyCounterMouse : KeyCounter
+ public partial class KeyCounterMouseTrigger : InputTrigger
{
public MouseButton Button { get; }
- public KeyCounterMouse(MouseButton button)
+ public KeyCounterMouseTrigger(MouseButton button)
: base(getStringRepresentation(button))
{
Button = button;
@@ -39,17 +37,16 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == Button)
- {
- IsLit = true;
- Increment();
- }
+ Activate();
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
- if (e.Button == Button) IsLit = false;
+ if (e.Button == Button)
+ Deactivate();
+
base.OnMouseUp(e);
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index c8d06b82e8..9f050a07bd 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -331,7 +331,7 @@ namespace osu.Game.Screens.Play
ShowHealth = { BindTarget = ShowHealthBar }
};
- protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
+ protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs
deleted file mode 100644
index bb50d4a539..0000000000
--- a/osu.Game/Screens/Play/KeyCounterDisplay.cs
+++ /dev/null
@@ -1,172 +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 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.Events;
-using osu.Game.Configuration;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Screens.Play
-{
- public partial class KeyCounterDisplay : Container
- {
- private const int duration = 100;
- private const double key_fade_time = 80;
-
- private readonly Bindable configVisibility = new Bindable();
-
- protected readonly FillFlowContainer KeyFlow;
-
- protected override Container Content => KeyFlow;
-
- ///
- /// Whether the key counter should be visible regardless of the configuration value.
- /// This is true by default, but can be changed.
- ///
- public readonly Bindable AlwaysVisible = new Bindable(true);
-
- public KeyCounterDisplay()
- {
- InternalChild = KeyFlow = new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Alpha = 0,
- };
- }
-
- protected override void Update()
- {
- base.Update();
-
- // Don't use autosize as it will shrink to zero when KeyFlow is hidden.
- // In turn this can cause the display to be masked off screen and never become visible again.
- Size = KeyFlow.Size;
- }
-
- public override void Add(KeyCounter key)
- {
- ArgumentNullException.ThrowIfNull(key);
-
- base.Add(key);
- key.IsCounting = IsCounting;
- key.FadeTime = key_fade_time;
- key.KeyDownTextColor = KeyDownTextColor;
- key.KeyUpTextColor = KeyUpTextColor;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
- {
- config.BindWith(OsuSetting.KeyOverlay, configVisibility);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- AlwaysVisible.BindValueChanged(_ => updateVisibility());
- configVisibility.BindValueChanged(_ => updateVisibility(), true);
- }
-
- private bool isCounting = true;
-
- public bool IsCounting
- {
- get => isCounting;
- set
- {
- if (value == isCounting) return;
-
- isCounting = value;
- foreach (var child in Children)
- child.IsCounting = value;
- }
- }
-
- private Color4 keyDownTextColor = Color4.DarkGray;
-
- public Color4 KeyDownTextColor
- {
- get => keyDownTextColor;
- set
- {
- if (value != keyDownTextColor)
- {
- keyDownTextColor = value;
- foreach (var child in Children)
- child.KeyDownTextColor = value;
- }
- }
- }
-
- private Color4 keyUpTextColor = Color4.White;
-
- public Color4 KeyUpTextColor
- {
- get => keyUpTextColor;
- set
- {
- if (value != keyUpTextColor)
- {
- keyUpTextColor = value;
- foreach (var child in Children)
- child.KeyUpTextColor = value;
- }
- }
- }
-
- private void updateVisibility() =>
- // Isolate changing visibility of the key counters from fading this component.
- KeyFlow.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
-
- public override bool HandleNonPositionalInput => receptor == null;
- public override bool HandlePositionalInput => receptor == null;
-
- private Receptor receptor;
-
- public void SetReceptor(Receptor receptor)
- {
- if (this.receptor != null)
- throw new InvalidOperationException("Cannot set a new receptor when one is already active");
-
- this.receptor = receptor;
- }
-
- public partial class Receptor : Drawable
- {
- protected readonly KeyCounterDisplay Target;
-
- public Receptor(KeyCounterDisplay target)
- {
- RelativeSizeAxes = Axes.Both;
- Depth = float.MinValue;
- Target = target;
- }
-
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
-
- protected override bool Handle(UIEvent e)
- {
- switch (e)
- {
- case KeyDownEvent:
- case KeyUpEvent:
- case MouseDownEvent:
- case MouseUpEvent:
- return Target.Children.Any(c => c.TriggerEvent(e));
- }
-
- return base.Handle(e);
- }
- }
- }
-}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 45a671fb89..5174adfc06 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -358,14 +358,10 @@ namespace osu.Game.Screens.Play
ScoreProcessor.RevertResult(r);
};
- DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
- {
- if (storyboardEnded.NewValue)
- progressToResults(true);
- };
+ DimmableStoryboard.HasStoryboardEnded.ValueChanged += _ => checkScoreCompleted();
// Bind the judgement processors to ourselves
- ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
+ ScoreProcessor.HasCompleted.BindValueChanged(_ => checkScoreCompleted());
HealthProcessor.Failed += onFail;
// Provide judgement processors to mods after they're loaded so that they're on the gameplay clock,
@@ -441,8 +437,11 @@ namespace osu.Game.Screens.Play
},
KeyCounter =
{
+ IsCounting =
+ {
+ Value = false
+ },
AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded },
- IsCounting = false
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre
@@ -482,7 +481,7 @@ namespace osu.Game.Screens.Play
{
updateGameplayState();
updatePauseOnFocusLostState();
- HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
+ HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue;
}
private void updateGameplayState()
@@ -706,19 +705,20 @@ namespace osu.Game.Screens.Play
///
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
///
- /// Thrown if this method is called more than once without changing state.
- private void scoreCompletionChanged(ValueChangedEvent completed)
+ private void checkScoreCompleted()
{
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen())
return;
- // Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
- // TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
- // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
- // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
- // but it still doesn't feel right that this exists here.
- if (!completed.NewValue)
+ // Handle cases of arriving at this method when not in a completed state.
+ // - When a storyboard completion triggered this call earlier than gameplay finishes.
+ // - When a replay has been rewound before a queued resultsDisplayDelegate has run.
+ //
+ // Currently, even if this scenario is hit, prepareAndImportScoreAsync has already been queued (and potentially run).
+ // In the scenarios above, this is a non-issue, but it still feels a bit convoluted to have to cancel in this method.
+ // Maybe this can be improved with further refactoring.
+ if (!ScoreProcessor.HasCompleted.Value)
{
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null;
@@ -742,12 +742,12 @@ namespace osu.Game.Screens.Play
if (!Configuration.ShowResults)
return;
- bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
+ bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
- if (storyboardHasOutro)
+ // If the current beatmap has a storyboard, this method will be called again on storyboard completion.
+ // Alternatively, the user may press the outro skip button, forcing immediate display of the results screen.
+ if (storyboardStillRunning)
{
- // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
- // or the user pressing the skip outro button.
skipOutroOverlay.Show();
return;
}
@@ -793,6 +793,8 @@ namespace osu.Game.Screens.Play
// This player instance may already be in the process of exiting.
return;
+ Debug.Assert(ScoreProcessor.Rank.Value != ScoreRank.F);
+
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
}, Time.Current + delay, 50);
@@ -1112,7 +1114,7 @@ namespace osu.Game.Screens.Play
GameplayState.HasQuit = true;
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
- if (prepareScoreForDisplayTask == null)
+ if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null)
ScoreProcessor.FailScore(Score.ScoreInfo);
}
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 4f7e4add32..30ae5ee5aa 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -67,6 +67,8 @@ namespace osu.Game.Screens.Play
private OsuScrollContainer settingsScroll = null!;
+ private Bindable showStoryboards = null!;
+
private bool backgroundBrightnessReduction;
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
@@ -149,10 +151,11 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
- private void load(SessionStatics sessionStatics, AudioManager audio)
+ private void load(SessionStatics sessionStatics, AudioManager audio, OsuConfigManager config)
{
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
+ showStoryboards = config.GetBindable(OsuSetting.ShowStoryboard);
const float padding = 25;
@@ -463,7 +466,10 @@ namespace osu.Game.Screens.Play
// 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).
- if (epilepsyWarning?.IsAlive == true)
+ //
+ // note the late check of storyboard enable as the user may have just changed it
+ // from the settings on the loader screen.
+ if (epilepsyWarning?.IsAlive == true && showStoryboards.Value)
{
const double epilepsy_display_length = 3000;
@@ -483,6 +489,7 @@ namespace osu.Game.Screens.Play
{
// This goes hand-in-hand with the restoration of low pass filter in contentOut().
this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic);
+ epilepsyWarning?.Expire();
}
pushSequence.Schedule(() =>
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 68d3247275..6ba9843f7b 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select
public float BleedBottom { get; set; }
///
- /// Triggered when the loaded change and are completely loaded.
+ /// Triggered when finish loading, or are subsequently changed.
///
public Action? BeatmapSetsChanged;
@@ -353,6 +353,8 @@ namespace osu.Game.Screens.Select
if (!Scroll.UserScrolling)
ScrollToSelected(true);
+
+ BeatmapSetsChanged?.Invoke();
});
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index c5e914b461..4d6a5398c5 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -861,11 +862,9 @@ namespace osu.Game.Screens.Select
private void updateVisibleBeatmapCount()
{
- FilterControl.InformationalText = Carousel.CountDisplayed == 1
- // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
- // but also in this case we want support for formatting a number within a string).
- ? $"{Carousel.CountDisplayed:#,0} matching beatmap"
- : $"{Carousel.CountDisplayed:#,0} matching beatmaps";
+ // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
+ // but also in this case we want support for formatting a number within a string).
+ FilterControl.InformationalText = $"{"match".ToQuantity(Carousel.CountDisplayed, "#,0")}";
}
private bool boundLocalBindables;
diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 0158c47ea3..76c2c4d7ec 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -9,7 +9,6 @@ using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
@@ -70,20 +69,6 @@ namespace osu.Game.Skinning
updateSample();
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- CurrentSkin.SourceChanged += skinChangedImmediate;
- }
-
- private void skinChangedImmediate()
- {
- // Clean up the previous sample immediately on a source change.
- // This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled).
- clearPreviousSamples();
- }
-
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
@@ -109,6 +94,8 @@ namespace osu.Game.Skinning
private void updateSample()
{
+ clearPreviousSamples();
+
if (sampleInfo == null)
return;
@@ -129,6 +116,8 @@ namespace osu.Game.Skinning
///
public void Play()
{
+ FlushPendingSkinChanges();
+
if (Sample == null)
return;
@@ -172,14 +161,6 @@ namespace osu.Game.Skinning
}
}
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- if (CurrentSkin.IsNotNull())
- CurrentSkin.SourceChanged -= skinChangedImmediate;
- }
-
#region Re-expose AudioContainer
public BindableNumber Volume => sampleContainer.Volume;
diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs
index cef1db4bc0..c7b33dc539 100644
--- a/osu.Game/Skinning/SkinReloadableDrawable.cs
+++ b/osu.Game/Skinning/SkinReloadableDrawable.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Pooling;
+using osu.Framework.Threading;
namespace osu.Game.Skinning
{
@@ -14,6 +15,8 @@ namespace osu.Game.Skinning
///
public abstract partial class SkinReloadableDrawable : PoolableDrawable
{
+ private ScheduledDelegate? pendingSkinChange;
+
///
/// Invoked when has changed.
///
@@ -31,21 +34,30 @@ namespace osu.Game.Skinning
CurrentSkin.SourceChanged += onChange;
}
- private void onChange() =>
- // schedule required to avoid calls after disposed.
- // note that this has the side-effect of components only performing a skin change when they are alive.
- Scheduler.AddOnce(skinChanged);
-
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
skinChanged();
}
- private void skinChanged()
+ ///
+ /// Force any pending calls to be performed immediately.
+ ///
+ ///
+ /// When a skin change occurs, the handling provided by this class is scheduled.
+ /// In some cases, such a sample playback, this can result in the sample being played
+ /// just before it is updated to a potentially different sample.
+ ///
+ /// Calling this method will ensure any pending update operations are run immediately.
+ /// It is recommended to call this before consuming the result of skin changes for anything non-drawable.
+ ///
+ protected void FlushPendingSkinChanges()
{
- SkinChanged(CurrentSkin);
- OnSkinChanged?.Invoke();
+ if (pendingSkinChange == null)
+ return;
+
+ pendingSkinChange.RunTask();
+ pendingSkinChange = null;
}
///
@@ -56,6 +68,22 @@ namespace osu.Game.Skinning
{
}
+ private void onChange()
+ {
+ // schedule required to avoid calls after disposed.
+ // note that this has the side-effect of components only performing a skin change when they are alive.
+ pendingSkinChange?.Cancel();
+ pendingSkinChange = Scheduler.Add(skinChanged);
+ }
+
+ private void skinChanged()
+ {
+ SkinChanged(CurrentSkin);
+ OnSkinChanged?.Invoke();
+
+ pendingSkinChange = null;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 475b79053a..59b3799e0a 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -115,6 +115,8 @@ namespace osu.Game.Skinning
///
public virtual void Play()
{
+ FlushPendingSkinChanges();
+
samplesContainer.ForEach(c =>
{
if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
index e598c79b08..be77c9a98e 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables
Loop = animation.LoopType == AnimationLoopType.LoopForever;
LifetimeStart = animation.StartTime;
- LifetimeEnd = animation.EndTime;
+ LifetimeEnd = animation.EndTimeForDisplay;
}
[Resolved]
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
index f9b09ed57c..400d33481c 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Storyboards.Drawables
Position = sprite.InitialPosition;
LifetimeStart = sprite.StartTime;
- LifetimeEnd = sprite.EndTime;
+ LifetimeEnd = sprite.EndTimeForDisplay;
}
[Resolved]
diff --git a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs
index c8daeb3b3d..9eed139ad4 100644
--- a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs
+++ b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs
@@ -12,9 +12,17 @@ namespace osu.Game.Storyboards
{
///
/// The time at which the ends.
+ /// This is consumed to extend the length of a storyboard to ensure all visuals are played to completion.
///
double EndTime { get; }
+ ///
+ /// The time this element displays until.
+ /// This is used for lifetime purposes, and includes long playing animations which don't necessarily extend
+ /// a storyboard's play time.
+ ///
+ double EndTimeForDisplay { get; }
+
///
/// The duration of the StoryboardElement.
///
diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs
index 16deac8e9e..1a4b6bb923 100644
--- a/osu.Game/Storyboards/StoryboardAnimation.cs
+++ b/osu.Game/Storyboards/StoryboardAnimation.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 osuTK;
using osu.Framework.Graphics;
using osu.Game.Storyboards.Drawables;
diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs
index 5b7b194be7..982185d51b 100644
--- a/osu.Game/Storyboards/StoryboardSprite.cs
+++ b/osu.Game/Storyboards/StoryboardSprite.cs
@@ -1,12 +1,9 @@
// 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;
-using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Game.Storyboards.Drawables;
using osuTK;
@@ -84,6 +81,19 @@ namespace osu.Game.Storyboards
}
}
+ public double EndTimeForDisplay
+ {
+ get
+ {
+ double latestEndTime = TimelineGroup.EndTime;
+
+ foreach (var l in loops)
+ latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
+
+ return latestEndTime;
+ }
+ }
+
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
private delegate void DrawablePropertyInitializer(Drawable drawable, T value);
@@ -114,7 +124,7 @@ namespace osu.Game.Storyboards
public virtual Drawable CreateDrawable()
=> new DrawableStoryboardSprite(this);
- public void ApplyTransforms(Drawable drawable, IEnumerable> triggeredGroups = null)
+ public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null)
{
// For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
// To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list
@@ -156,7 +166,7 @@ namespace osu.Game.Storyboards
foreach (var command in commands)
{
- DrawablePropertyInitializer initFunc = null;
+ DrawablePropertyInitializer? initFunc = null;
if (!initialized)
{
@@ -169,7 +179,7 @@ namespace osu.Game.Storyboards
}
}
- private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable> triggeredGroups)
+ private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups)
{
var commands = TimelineGroup.GetCommands(timelineSelector);
foreach (var loop in loops)
@@ -198,11 +208,11 @@ namespace osu.Game.Storyboards
{
public double StartTime => command.StartTime;
- private readonly DrawablePropertyInitializer initializeProperty;
+ private readonly DrawablePropertyInitializer? initializeProperty;
private readonly DrawableTransformer transform;
private readonly CommandTimeline.TypedCommand command;
- public GeneratedCommand([NotNull] CommandTimeline.TypedCommand command, [CanBeNull] DrawablePropertyInitializer initializeProperty, [NotNull] DrawableTransformer transform)
+ public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty, DrawableTransformer transform)
{
this.command = command;
this.initializeProperty = initializeProperty;
diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs
index cd9e9e1d52..78188d7cf7 100644
--- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs
@@ -3,9 +3,12 @@
#nullable disable
+using System;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Input;
using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
@@ -24,18 +27,27 @@ namespace osu.Game.Tests.Visual
protected EditorBeatmap EditorBeatmap => (EditorBeatmap)Editor.Dependencies.Get(typeof(EditorBeatmap));
+ [CanBeNull]
+ protected Func CreateInitialBeatmap { get; set; }
+
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
- AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
+ if (CreateInitialBeatmap == null)
+ AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
+ else
+ {
+ AddStep("set test beatmap", () => Game.Beatmap.Value = CreateInitialBeatmap?.Invoke());
+ }
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => Editor?.IsLoaded == true);
- AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+ if (CreateInitialBeatmap == null)
+ AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
@@ -50,6 +62,14 @@ namespace osu.Game.Tests.Visual
protected void ReloadEditorToSameBeatmap()
{
+ Guid beatmapSetGuid = Guid.Empty;
+ Guid beatmapGuid = Guid.Empty;
+
+ AddStep("Store beatmap GUIDs", () =>
+ {
+ beatmapSetGuid = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID;
+ beatmapGuid = EditorBeatmap.BeatmapInfo.ID;
+ });
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -59,7 +79,8 @@ namespace osu.Game.Tests.Visual
PushAndConfirm(() => songSelect = new PlaySongSelect());
AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
- AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
+ AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid));
+ AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 3de022e88d..085f78b27b 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index eb7ba24336..127994c670 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+