diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 1f937e1837..8c8a3be771 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,7 +15,7 @@
]
},
"codefilesanity": {
- "version": "0.0.36",
+ "version": "0.0.37",
"commands": [
"CodeFileSanity"
]
diff --git a/osu.Android.props b/osu.Android.props
index f4d08e443c..66f518f3d5 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index d92fea27bf..21cea3ba76 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
-using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Security;
using osu.Framework.Platform;
@@ -17,7 +15,6 @@ using osu.Framework;
using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.Windows;
-using osu.Framework.Threading;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Utils;
@@ -138,52 +135,10 @@ namespace osu.Desktop
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name;
- desktopWindow.DragDrop += f =>
- {
- // on macOS, URL associations are handled via SDL_DROPFILE events.
- if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
- {
- HandleLink(f);
- return;
- }
-
- fileDrop(new[] { f });
- };
}
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
- private readonly List importableFiles = new List();
- private ScheduledDelegate? importSchedule;
-
- private void fileDrop(string[] filePaths)
- {
- lock (importableFiles)
- {
- importableFiles.AddRange(filePaths);
-
- Logger.Log($"Adding {filePaths.Length} files for import");
-
- // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
- // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
- importSchedule?.Cancel();
- importSchedule = Scheduler.AddDelayed(handlePendingImports, 100);
- }
- }
-
- private void handlePendingImports()
- {
- lock (importableFiles)
- {
- Logger.Log($"Handling batch import of {importableFiles.Count} files");
-
- string[] paths = importableFiles.ToArray();
- importableFiles.Clear();
-
- Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
- }
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
index eba837a52d..55b24b3ffa 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
///
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
///
- public partial class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
+ public partial class LegacyCatchComboCounter : UprightAspectMaintainingContainer, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
index f85e303940..6485cbb76b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
+using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
@@ -25,22 +26,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition(2)
};
- SetContents(_ => new ManiaPlayfield(stageDefinitions));
+ SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, 2)
+ {
+ Child = new ManiaPlayfield(stageDefinitions)
+ });
});
}
- [Test]
- public void TestDualStages()
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(5)]
+ public void TestDualStages(int columnCount)
{
AddStep("create stage", () =>
{
stageDefinitions = new List
{
- new StageDefinition(2),
- new StageDefinition(2)
+ new StageDefinition(columnCount),
+ new StageDefinition(columnCount)
};
- SetContents(_ => new ManiaPlayfield(stageDefinitions));
+ SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, (int)PlayfieldType.Dual + 2 * columnCount)
+ {
+ Child = new ManiaPlayfield(stageDefinitions)
+ {
+ // bit of a hack to make sure the dual stages fit on screen without overlapping each other.
+ Size = new Vector2(1.5f),
+ Scale = new Vector2(1 / 1.5f)
+ }
+ });
});
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 632b7cdcc7..bdc5a00583 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -119,14 +119,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
yield return obj;
}
- private readonly List prevNoteTimes = new List(max_notes_for_density);
+ private readonly LimitedCapacityQueue prevNoteTimes = new LimitedCapacityQueue(max_notes_for_density);
private double density = int.MaxValue;
private void computeDensity(double newNoteTime)
{
- if (prevNoteTimes.Count == max_notes_for_density)
- prevNoteTimes.RemoveAt(0);
- prevNoteTimes.Add(newNoteTime);
+ prevNoteTimes.Enqueue(newNoteTime);
if (prevNoteTimes.Count >= 2)
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 3f91328128..a8563d65c4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -242,18 +242,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
- // As the note is being held, adjust the size of the sizing container. This has two effects:
- // 1. The contained masking container will mask the body and ticks.
- // 2. The head note will move along with the new "head position" in the container.
- //
- // As per stable, this should not apply for early hits, waiting until the object starts to touch the
- // judgement area first.
- if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime)
+ if (Time.Current >= HitObject.StartTime)
{
- // How far past the hit target this hold note is.
- float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
- sizingContainer.Height = 1 - yOffset / DrawHeight;
+ // As the note is being held, adjust the size of the sizing container. This has two effects:
+ // 1. The contained masking container will mask the body and ticks.
+ // 2. The head note will move along with the new "head position" in the container.
+ //
+ // As per stable, this should not apply for early hits, waiting until the object starts to touch the
+ // judgement area first.
+ if (Head.IsHit && releaseTime == null && DrawHeight > 0)
+ {
+ // How far past the hit target this hold note is.
+ float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
+ sizingContainer.Height = 1 - yOffset / DrawHeight;
+ }
}
+ else
+ sizingContainer.Height = 1;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
index eb51179cea..3e0fe8ed4b 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
@@ -35,10 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
var stage = beatmap.GetStageForColumnIndex(column);
- if (stage.IsSpecialColumn(column))
+ int columnInStage = column % stage.Columns;
+
+ if (stage.IsSpecialColumn(columnInStage))
return SkinUtils.As(new Bindable(colourSpecial));
- int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column);
+ int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage);
return SkinUtils.As(new Bindable(distanceToEdge % 2 == 0 ? colourOdd : colourEven));
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index 7579e8077b..0c064ecfa6 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -185,7 +185,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("Ensure cursor is on a grid line", () =>
{
- return grid.ChildrenOfType().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X));
+ return grid.ChildrenOfType().Any(ring =>
+ {
+ // the grid rings are actually slightly _larger_ than the snapping radii.
+ // this is done such that the snapping radius falls right in the middle of each grid ring thickness-wise,
+ // but it does however complicate the following calculations slightly.
+
+ // we want to calculate the coordinates of the rightmost point on the grid line, which is in the exact middle of the ring thickness-wise.
+ // for the X component, we take the entire width of the ring, minus one half of the inner radius (since we want the middle of the line on the right side).
+ // for the Y component, we just take 0.5f.
+ var rightMiddleOfGridLine = ring.ToScreenSpace(ring.DrawSize * new Vector2(1 - ring.InnerRadius / 2, 0.5f));
+ return Precision.AlmostEquals(rightMiddleOfGridLine.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X);
+ });
});
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs
new file mode 100644
index 0000000000..9537f8b388
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Screens.Edit;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ ///
+ /// Mod that colours s based on the musical division they are on
+ ///
+ public class OsuModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject
+ {
+ private readonly OsuColour colours = new OsuColour();
+
+ private IBeatmap? currentBeatmap { get; set; }
+
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ //Store a reference to the current beatmap to look up the beat divisor when notes are drawn
+ if (currentBeatmap != beatmap)
+ currentBeatmap = beatmap;
+ }
+
+ public void ApplyToDrawableHitObject(DrawableHitObject d)
+ {
+ if (currentBeatmap == null) return;
+
+ Color4? timingBasedColour = null;
+
+ d.HitObjectApplied += _ =>
+ {
+ // slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`).
+ // to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap.
+ double snapTime = d is DrawableSliderTail tail
+ ? tail.Slider.GetEndTime()
+ : d.HitObject.StartTime;
+ timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(snapTime), colours);
+ };
+
+ // Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour().
+ d.OnUpdate += _ =>
+ {
+ if (timingBasedColour != null)
+ d.AccentColour.Value = timingBasedColour.Value;
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 8ce55d78dd..4cff16b46f 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -204,7 +204,8 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed(),
new OsuModFreezeFrame(),
- new OsuModBubbles()
+ new OsuModBubbles(),
+ new OsuModSynesthesia()
};
case ModType.System:
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs
index 5855838d3c..e975a85401 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs
@@ -7,12 +7,15 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
+using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -21,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private GameplayClockContainer gameplayClockContainer = null!;
+ private Box background = null!;
+
private const double skip_target_time = -2000;
[BackgroundDependencyLoader]
@@ -30,11 +35,20 @@ namespace osu.Game.Tests.Visual.Gameplay
FrameStabilityContainer frameStabilityContainer;
- Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
+ AddRange(new Drawable[]
{
- Child = frameStabilityContainer = new FrameStabilityContainer
+ background = new Box
{
- MaxCatchUpFrames = 1
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue
+ },
+ gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
+ {
+ Child = frameStabilityContainer = new FrameStabilityContainer
+ {
+ MaxCatchUpFrames = 1
+ }
}
});
@@ -71,9 +85,20 @@ namespace osu.Game.Tests.Visual.Gameplay
applyToArgonProgress(s => s.ShowGraph.Value = b);
});
+ AddStep("set white background", () => background.FadeColour(Color4.White, 200, Easing.OutQuint));
+ AddStep("randomise background colour", () => background.FadeColour(new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), 200, Easing.OutQuint));
+
AddStep("stop", gameplayClockContainer.Stop);
}
+ [Test]
+ public void TestSeekToKnownTime()
+ {
+ AddStep("seek to known time", () => gameplayClockContainer.Seek(60000));
+ AddWaitStep("wait some for seek", 15);
+ AddStep("stop", () => gameplayClockContainer.Stop());
+ }
+
private void applyToArgonProgress(Action action) =>
this.ChildrenOfType().ForEach(action);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
index 45f671618e..66ba908879 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
@@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
@@ -57,11 +58,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0);
}
+ [Test]
+ public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged()
+ {
+ createFreeModSelect();
+
+ AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea");
+
+ AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click select all button", navigateAndClick);
+ AddAssert("select all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e");
+
+ AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+
+ void navigateAndClick() where T : Drawable
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ }
+ }
+
[Test]
public void TestSelectDeselectAllViaKeyboard()
{
createFreeModSelect();
+ AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus());
+
AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll));
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 9b130071cc..947b7e5be6 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
{
+ AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
@@ -102,17 +103,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
// A previous test's mod overlay could still be fading out.
AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType().Count() == 1);
- assertHasFreeModButton(allowedMod, false);
- assertHasFreeModButton(requiredMod, false);
+ assertFreeModNotShown(allowedMod);
+ assertFreeModNotShown(requiredMod);
}
- private void assertHasFreeModButton(Type type, bool hasButton = true)
+ private void assertFreeModNotShown(Type type)
{
- AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
+ AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
() => this.ChildrenOfType()
.Single()
.ChildrenOfType()
- .Where(panel => !panel.Filtered.Value)
+ .Where(panel => panel.Visible)
.All(b => b.Mod.GetType() != type));
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 8816787ceb..a41eff067b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contains only double time mod",
() => this.ChildrenOfType().Single().UserModsSelectOverlay
.ChildrenOfType()
- .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
+ .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index a27c4ddad2..d9763ef6c8 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -86,6 +86,7 @@ namespace osu.Game.Tests.Visual.Online
StarRating = 9.99,
DifficultyName = @"TEST",
Length = 456000,
+ HitLength = 400000,
RulesetID = 3,
CircleSize = 1,
DrainRate = 2.3f,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 61a8322ee3..61f95dc628 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -453,6 +453,25 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
}
+ [Test]
+ public void TestRewindToDeletedBeatmap()
+ {
+ loadBeatmaps();
+
+ var firstAdded = TestResources.CreateTestBeatmapSetInfo();
+
+ AddStep("add new set", () => carousel.UpdateBeatmapSet(firstAdded));
+ AddStep("select set", () => carousel.SelectBeatmap(firstAdded.Beatmaps.First()));
+
+ nextRandom();
+
+ AddStep("delete set", () => carousel.RemoveBeatmapSet(firstAdded));
+
+ prevRandom();
+
+ AddAssert("deleted set not selected", () => carousel.SelectedBeatmapSet?.Equals(firstAdded) == false);
+ }
+
///
/// Test adding and removing beatmap sets
///
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
index e7840d4a2a..b17024ae8f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Comments;
using osuTK;
@@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private TestCommentEditor commentEditor = null!;
private TestCancellableCommentEditor cancellableCommentEditor = null!;
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -96,12 +98,43 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown);
}
+ [Test]
+ public void TestLoggingInAndOut()
+ {
+ void assertLoggedInState()
+ {
+ AddAssert("commit button visible", () => commentEditor.ButtonsContainer[0].Alpha == 1);
+ AddAssert("login button hidden", () => commentEditor.ButtonsContainer[1].Alpha == 0);
+ AddAssert("text box editable", () => !commentEditor.TextBox.ReadOnly);
+ }
+
+ void assertLoggedOutState()
+ {
+ AddAssert("commit button hidden", () => commentEditor.ButtonsContainer[0].Alpha == 0);
+ AddAssert("login button visible", () => commentEditor.ButtonsContainer[1].Alpha == 1);
+ AddAssert("text box readonly", () => commentEditor.TextBox.ReadOnly);
+ }
+
+ // there's also the case of starting logged out, but more annoying to test.
+
+ // starting logged in
+ assertLoggedInState();
+
+ // moving from logged in -> logged out
+ AddStep("log out", () => dummyAPI.Logout());
+ assertLoggedOutState();
+
+ // moving from logged out -> logged in
+ AddStep("log back in", () => dummyAPI.Login("username", "password"));
+ assertLoggedInState();
+ }
+
[Test]
public void TestCancelAction()
{
AddStep("click cancel button", () =>
{
- InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[1]);
+ InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[2]);
InputManager.Click(MouseButton.Left);
});
@@ -112,6 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public new Bindable Current => base.Current;
public new FillFlowContainer ButtonsContainer => base.ButtonsContainer;
+ public new TextBox TextBox => base.TextBox;
public string CommittedText { get; private set; } = string.Empty;
@@ -125,8 +159,12 @@ namespace osu.Game.Tests.Visual.UserInterface
}
protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool.";
- protected override LocalisableString CommitButtonText => @"Commit";
- protected override LocalisableString TextBoxPlaceholder => @"This text box is empty";
+
+ protected override LocalisableString GetButtonText(bool isLoggedIn) =>
+ isLoggedIn ? @"Commit" : "You're logged out!";
+
+ protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
+ isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it.";
}
private partial class TestCancellableCommentEditor : CancellableCommentEditor
@@ -146,8 +184,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
}
- protected override LocalisableString CommitButtonText => @"Save";
- protected override LocalisableString TextBoxPlaceholder => @"Multiline textboxes soon";
+ protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
+ protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon";
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs
index a11000214c..18739c0275 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs
@@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
- AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2);
+ AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2);
clickToggle();
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
- AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value));
+ AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.Visible));
AddStep("unset filter", () => setFilter(null));
- AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value));
+ AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible));
AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value);
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
- AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2);
+ AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2);
AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value);
AddStep("filter out everything", () => setFilter(_ => false));
- AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value));
+ AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Visible));
AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent);
AddStep("inset filter", () => setFilter(null));
- AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value));
+ AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible));
AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent);
void clickToggle() => AddStep("click toggle", () =>
@@ -288,10 +288,53 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("no change", () => this.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
}
+ [Test]
+ public void TestApplySearchTerms()
+ {
+ Mod hidden = getExampleModsFor(ModType.DifficultyIncrease).Where(modState => modState.Mod is ModHidden).Select(modState => modState.Mod).Single();
+
+ ModColumn column = null!;
+ AddStep("create content", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(30),
+ Child = column = new ModColumn(ModType.DifficultyIncrease, false)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
+ }
+ });
+
+ applySearchAndAssert(hidden.Name);
+
+ clearSearch();
+
+ applySearchAndAssert(hidden.Acronym);
+
+ clearSearch();
+
+ applySearchAndAssert(hidden.Description.ToString());
+
+ void applySearchAndAssert(string searchTerm)
+ {
+ AddStep("search by mod name", () => column.SearchTerm = searchTerm);
+
+ AddAssert("only hidden is visible", () => column.ChildrenOfType().Where(panel => panel.Visible).All(panel => panel.Mod is ModHidden));
+ }
+
+ void clearSearch()
+ {
+ AddStep("clear search", () => column.SearchTerm = string.Empty);
+
+ AddAssert("all mods are visible", () => column.ChildrenOfType().All(panel => panel.Visible));
+ }
+ }
+
private void setFilter(Func? filter)
{
foreach (var modState in this.ChildrenOfType().Single().AvailableMods)
- modState.Filtered.Value = filter?.Invoke(modState.Mod) == false;
+ modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false;
}
private partial class TestModColumn : ModColumn
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs
index 3efdba8754..2d54a4e566 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs
@@ -392,6 +392,28 @@ namespace osu.Game.Tests.Visual.UserInterface
new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods));
}
+ [Test]
+ public void TestTextFiltering()
+ {
+ ModPresetColumn modPresetColumn = null!;
+
+ AddStep("clear mods", () => SelectedMods.Value = Array.Empty());
+ AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
+ AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
+
+ AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
+ AddStep("set text filter", () => modPresetColumn.SearchTerm = "First");
+ AddUntilStep("one panel visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(1));
+
+ AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3));
+ AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(0));
+ }
+
private ICollection createTestPresets() => new[]
{
new ModPreset
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 5cf24c1960..d566a04261 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -490,15 +490,15 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
changeRuleset(0);
- AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
+ AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
- AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
- AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
+ AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible));
+ AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible));
AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true);
- AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
- AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
+ AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
+ AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible));
}
[Test]
@@ -524,7 +524,57 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
waitForColumnLoad();
- AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
+ AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).Visible);
+ }
+
+ [Test]
+ public void TestFirstModSelectDeselect()
+ {
+ createScreen();
+
+ AddStep("apply search", () => modSelectOverlay.SearchTerm = "HD");
+
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
+ AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value);
+
+ AddStep("press enter again", () => InputManager.Key(Key.Enter));
+ AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value);
+
+ AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
+ AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
+ }
+
+ [Test]
+ public void TestSearchFocusChangeViaClick()
+ {
+ createScreen();
+
+ AddStep("click on search", navigateAndClick);
+ AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
+
+ AddStep("click on mod column", navigateAndClick);
+ AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
+
+ void navigateAndClick() where T : Drawable
+ {
+ InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().FirstOrDefault());
+ InputManager.Click(MouseButton.Left);
+ }
+ }
+
+ [Test]
+ public void TestSearchFocusChangeViaKey()
+ {
+ createScreen();
+
+ const Key focus_switch_key = Key.Tab;
+
+ AddStep("press tab", () => InputManager.Key(focus_switch_key));
+ AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
+
+ AddStep("press tab", () => InputManager.Key(focus_switch_key));
+ AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
@@ -533,6 +583,8 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
changeRuleset(0);
+ AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus());
+
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
@@ -540,6 +592,26 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
}
+ [Test]
+ public void TestDeselectAllViaKey_WithSearchApplied()
+ {
+ createScreen();
+ changeRuleset(0);
+
+ AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
+ AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus());
+ AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
+ AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 2);
+
+ AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
+ AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
+ AddAssert("search term changed", () => modSelectOverlay.SearchTerm == "Eas");
+
+ AddStep("kill focus", () => modSelectOverlay.SearchTextBox.KillFocus());
+ AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
+ AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
+ }
+
[Test]
public void TestDeselectAllViaButton()
{
@@ -561,6 +633,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
}
+ [Test]
+ public void TestDeselectAllViaButton_WithSearchApplied()
+ {
+ createScreen();
+ changeRuleset(0);
+
+ AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("select DT + HD + RD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModRandom() });
+ AddAssert("DT + HD + RD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 3);
+ AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
+ AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 3);
+ AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click deselect all button", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
+ AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+ }
+
[Test]
public void TestCloseViaBackButton()
{
@@ -580,8 +677,11 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
+ ///
+ /// Covers columns hiding/unhiding on changes of .
+ ///
[Test]
- public void TestColumnHiding()
+ public void TestColumnHidingOnIsValidChange()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
@@ -610,6 +710,56 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3);
}
+ ///
+ /// Covers columns hiding/unhiding on changes of .
+ ///
+ [Test]
+ public void TestColumnHidingOnTextFilterChange()
+ {
+ AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ SelectedMods = { BindTarget = SelectedMods }
+ });
+ waitForColumnLoad();
+ changeRuleset(0);
+
+ AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent));
+
+ AddStep("set search", () => modSelectOverlay.SearchTerm = "HD");
+ AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 1);
+
+ AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches");
+ AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent));
+
+ AddStep("clear search bar", () => modSelectOverlay.SearchTerm = "");
+ AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent));
+ }
+
+ [Test]
+ public void TestHidingOverlayClearsTextSearch()
+ {
+ AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ SelectedMods = { BindTarget = SelectedMods }
+ });
+ waitForColumnLoad();
+ changeRuleset(0);
+
+ AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent));
+
+ AddStep("set search", () => modSelectOverlay.SearchTerm = "fail");
+ AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2);
+
+ AddStep("hide", () => modSelectOverlay.Hide());
+ AddStep("show", () => modSelectOverlay.Show());
+
+ AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent));
+ }
+
[Test]
public void TestColumnHidingOnRulesetChange()
{
@@ -688,12 +838,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public override string ShortName => "unimplemented";
- public override IEnumerable GetModsFor(ModType type)
- {
- if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
-
- return base.GetModsFor(type);
- }
+ public override IEnumerable GetModsFor(ModType type) =>
+ type == ModType.Conversion
+ ? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() })
+ : base.GetModsFor(type);
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
index d9c2774611..bb94912c83 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
@@ -101,6 +101,10 @@ namespace osu.Game.Tests.Visual.UserInterface
},
};
}
+
+ protected override void PopIn()
+ {
+ }
}
}
}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
index 5695cb5574..94086f10f2 100644
--- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Tournament.Tests.Screens
Add(screen = new MapPoolScreen { Width = 0.7f });
}
+ [SetUp]
+ public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true);
+
[Test]
public void TestFewMaps()
{
@@ -92,7 +95,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 11; i++)
- addBeatmap(i > 4 ? $"M{i}" : "NM");
+ addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("reset match", () =>
@@ -118,7 +121,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 12; i++)
- addBeatmap(i > 4 ? $"M{i}" : "NM");
+ addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("reset match", () =>
@@ -130,7 +133,27 @@ namespace osu.Game.Tournament.Tests.Screens
assertThreeWide();
}
- private void addBeatmap(string mods = "nm")
+ [Test]
+ public void TestSplitMapPoolByMods()
+ {
+ AddStep("load many maps", () =>
+ {
+ Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
+
+ for (int i = 0; i < 12; i++)
+ addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
+ });
+
+ AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false);
+
+ AddStep("reset match", () =>
+ {
+ Ladder.CurrentMatch.Value = new TournamentMatch();
+ Ladder.CurrentMatch.Value = Ladder.Matches.First();
+ });
+ }
+
+ private void addBeatmap(string mods = "NM")
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
{
diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs
index 6b64a1156e..cb4e8bc16a 100644
--- a/osu.Game.Tournament/Models/LadderInfo.cs
+++ b/osu.Game.Tournament/Models/LadderInfo.cs
@@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
};
public Bindable AutoProgressScreens = new BindableBool(true);
+
+ public Bindable SplitMapPoolByMods = new BindableBool(true);
}
}
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
index f0e34d78c3..cb6c5902ec 100644
--- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{
public partial class MapPoolScreen : TournamentMatchScreen
{
- private readonly FillFlowContainer> mapFlows;
+ private FillFlowContainer> mapFlows;
[Resolved(canBeNull: true)]
private TournamentSceneManager sceneManager { get; set; }
@@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Screens.MapPool
private TeamColour pickColour;
private ChoiceType pickType;
- private readonly OsuButton buttonRedBan;
- private readonly OsuButton buttonBlueBan;
- private readonly OsuButton buttonRedPick;
- private readonly OsuButton buttonBluePick;
+ private OsuButton buttonRedBan;
+ private OsuButton buttonBlueBan;
+ private OsuButton buttonRedPick;
+ private OsuButton buttonBluePick;
- public MapPoolScreen()
+ [BackgroundDependencyLoader]
+ private void load(MatchIPCInfo ipc)
{
InternalChildren = new Drawable[]
{
@@ -98,15 +99,26 @@ namespace osu.Game.Tournament.Screens.MapPool
Action = reset
},
new ControlPanel.Spacer(),
+ new OsuCheckbox
+ {
+ LabelText = "Split display by mods",
+ Current = LadderInfo.SplitMapPoolByMods,
+ },
},
}
};
+
+ ipc.Beatmap.BindValueChanged(beatmapChanged);
}
- [BackgroundDependencyLoader]
- private void load(MatchIPCInfo ipc)
+ private Bindable splitMapPoolByMods;
+
+ protected override void LoadComplete()
{
- ipc.Beatmap.BindValueChanged(beatmapChanged);
+ base.LoadComplete();
+
+ splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy();
+ splitMapPoolByMods.BindValueChanged(_ => updateDisplay());
}
private void beatmapChanged(ValueChangedEvent beatmap)
@@ -213,24 +225,27 @@ namespace osu.Game.Tournament.Screens.MapPool
protected override void CurrentMatchChanged(ValueChangedEvent match)
{
base.CurrentMatchChanged(match);
+ updateDisplay();
+ }
+ private void updateDisplay()
+ {
mapFlows.Clear();
- if (match.NewValue == null)
+ if (CurrentMatch.Value == null)
return;
int totalRows = 0;
- if (match.NewValue.Round.Value != null)
+ if (CurrentMatch.Value.Round.Value != null)
{
FillFlowContainer currentFlow = null;
- string currentMod = null;
-
+ string currentMods = null;
int flowCount = 0;
- foreach (var b in match.NewValue.Round.Value.Beatmaps)
+ foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps)
{
- if (currentFlow == null || currentMod != b.Mods)
+ if (currentFlow == null || (LadderInfo.SplitMapPoolByMods.Value && currentMods != b.Mods))
{
mapFlows.Add(currentFlow = new FillFlowContainer
{
@@ -240,7 +255,7 @@ namespace osu.Game.Tournament.Screens.MapPool
AutoSizeAxes = Axes.Y
});
- currentMod = b.Mods;
+ currentMods = b.Mods;
totalRows++;
flowCount = 0;
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
index 5c6f0c4ee1..175c15ea7b 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
@@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
titleBadgeArea = new FillFlowContainer
{
@@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
Empty()
},
}
},
- new OsuSpriteText
+ new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
- Truncate = true,
Text = BeatmapSet.Source,
Shadow = false,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
index 720d892495..18e1584a98 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
@@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
titleBadgeArea = new FillFlowContainer
{
@@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
Empty()
},
diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs
index 4f2c08f63d..b8c69cc525 100644
--- a/osu.Game/Beatmaps/IBeatmapInfo.cs
+++ b/osu.Game/Beatmaps/IBeatmapInfo.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
IBeatmapSetInfo? BeatmapSet { get; }
///
- /// The playable length in milliseconds of this beatmap.
+ /// The total length in milliseconds of this beatmap.
///
double Length { get; }
diff --git a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs
index e1634e7d24..707a0696ba 100644
--- a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs
+++ b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs
@@ -59,5 +59,10 @@ namespace osu.Game.Beatmaps
int PassCount { get; }
APIFailTimes? FailTimes { get; }
+
+ ///
+ /// The playable length in milliseconds of this beatmap.
+ ///
+ double HitLength { get; }
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index 0f3d61f527..78eed626f2 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -264,7 +264,7 @@ namespace osu.Game.Beatmaps
if (beatmapFileStream == null)
{
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
- return null;
+ return new Storyboard();
}
using (var reader = new LineBufferedReader(beatmapFileStream))
diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs
index 36142cf26f..31016b807b 100644
--- a/osu.Game/Collections/ManageCollectionsDialog.cs
+++ b/osu.Game/Collections/ManageCollectionsDialog.cs
@@ -114,8 +114,6 @@ namespace osu.Game.Collections
protected override void PopIn()
{
- base.PopIn();
-
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
this.FadeIn(enter_duration, Easing.OutQuint);
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 365ad37f4c..193068193a 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -178,6 +178,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
SetDefault(OsuSetting.EditorShowHitMarkers, true);
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
+ SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@@ -383,5 +384,6 @@ namespace osu.Game.Configuration
SafeAreaConsiderations,
ComboColourNormalisationAmount,
ProfileCoverExpanded,
+ EditorLimitedDistanceSnap
}
}
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 63ab18db8c..da4caa42ba 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -76,8 +76,10 @@ namespace osu.Game.Database
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
+ /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
+ /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
///
- private const int schema_version = 28;
+ private const int schema_version = 30;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -724,6 +726,11 @@ namespace osu.Game.Database
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
{
+ Logger.Log($"Running realm migration to version {targetVersion}...");
+ Stopwatch stopwatch = new Stopwatch();
+
+ stopwatch.Start();
+
switch (targetVersion)
{
case 7:
@@ -930,7 +937,38 @@ namespace osu.Game.Database
break;
}
+
+ case 29:
+ case 30:
+ {
+ var scores = migration.NewRealm
+ .All()
+ .Where(s => !s.IsLegacyScore);
+
+ foreach (var score in scores)
+ {
+ try
+ {
+ if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(score))
+ {
+ try
+ {
+ long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
+ score.TotalScore = calculatedNew;
+ }
+ catch
+ {
+ }
+ }
+ }
+ catch { }
+ }
+
+ break;
+ }
}
+
+ Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
}
private string? getRulesetShortNameFromLegacyID(long rulesetId)
diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs
new file mode 100644
index 0000000000..582a656efa
--- /dev/null
+++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs
@@ -0,0 +1,210 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Database
+{
+ public static class StandardisedScoreMigrationTools
+ {
+ public static bool ShouldMigrateToNewStandardised(ScoreInfo score)
+ {
+ if (score.IsLegacyScore)
+ return false;
+
+ // Recalculate the old-style standardised score to see if this was an old lazer score.
+ bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore;
+ // Some older scores don't have correct statistics populated, so let's give them benefit of doubt.
+ bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0);
+
+ return oldScoreMatchesExpectations || scoreIsVeryOld;
+ }
+
+ public static long GetNewStandardised(ScoreInfo score)
+ {
+ int maxJudgementIndex = 0;
+
+ // Avoid retrieving from realm inside loops.
+ int maxCombo = score.MaxCombo;
+
+ var ruleset = score.Ruleset.CreateInstance();
+ var processor = ruleset.CreateScoreProcessor();
+
+ processor.TrackHitEvents = false;
+
+ var beatmap = new Beatmap();
+
+ HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
+
+ // This is a list of all results, ordered from best to worst.
+ // We are constructing a "best possible" score from the statistics provided because it's the best we can do.
+ List sortedHits = score.Statistics
+ .Where(kvp => kvp.Key.AffectsCombo())
+ .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
+ .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
+ .ToList();
+
+ // Attempt to use maximum statistics from the database.
+ var maximumJudgements = score.MaximumStatistics
+ .Where(kvp => kvp.Key.AffectsCombo())
+ .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
+ .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
+ .ToList();
+
+ // Some older scores may not have maximum statistics populated correctly.
+ // In this case we need to fill them with best-known-defaults.
+ if (maximumJudgements.Count != sortedHits.Count)
+ {
+ maximumJudgements = sortedHits
+ .Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement)))
+ .ToList();
+ }
+
+ // This is required to get the correct maximum combo portion.
+ foreach (var judgement in maximumJudgements)
+ beatmap.HitObjects.Add(new FakeHit(judgement));
+ processor.ApplyBeatmap(beatmap);
+ processor.Mods.Value = score.Mods;
+
+ // Insert all misses into a queue.
+ // These will be nibbled at whenever we need to reset the combo.
+ Queue misses = new Queue(score.Statistics
+ .Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss)
+ .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)));
+
+ foreach (var result in sortedHits)
+ {
+ // For the main part of this loop, ignore all misses, as they will be inserted from the queue.
+ if (result == HitResult.Miss || result == HitResult.LargeTickMiss)
+ continue;
+
+ // Reset combo if required.
+ if (processor.Combo.Value == maxCombo)
+ insertMiss();
+
+ processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
+ {
+ Type = result
+ });
+ }
+
+ // Ensure we haven't forgotten any misses.
+ while (misses.Count > 0)
+ insertMiss();
+
+ var bonusHits = score.Statistics
+ .Where(kvp => kvp.Key.IsBonus())
+ .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value));
+
+ foreach (var result in bonusHits)
+ processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result });
+
+ // Not true for all scores for whatever reason. Oh well.
+ // Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
+
+ return processor.TotalScore.Value;
+
+ void insertMiss()
+ {
+ if (misses.Count > 0)
+ {
+ processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
+ {
+ Type = misses.Dequeue(),
+ });
+ }
+ else
+ {
+ // We ran out of misses. But we can't let max combo increase beyond the known value,
+ // so let's forge a miss.
+ processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement)))
+ {
+ Type = HitResult.Miss,
+ });
+ }
+ }
+ }
+
+ private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max)
+ {
+ switch (hitResult)
+ {
+ case HitResult.Miss:
+ case HitResult.Meh:
+ case HitResult.Ok:
+ case HitResult.Good:
+ case HitResult.Great:
+ case HitResult.Perfect:
+ return max;
+
+ case HitResult.SmallTickMiss:
+ case HitResult.SmallTickHit:
+ return HitResult.SmallTickHit;
+
+ case HitResult.LargeTickMiss:
+ case HitResult.LargeTickHit:
+ return HitResult.LargeTickHit;
+ }
+
+ return HitResult.IgnoreHit;
+ }
+
+ public static long GetOldStandardised(ScoreInfo score)
+ {
+ double accuracyScore =
+ (double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
+ / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
+ double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
+ double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
+
+ double accuracyPortion = 0.3;
+
+ switch (score.RulesetID)
+ {
+ case 1:
+ accuracyPortion = 0.75;
+ break;
+
+ case 3:
+ accuracyPortion = 0.99;
+ break;
+ }
+
+ double modMultiplier = 1;
+
+ foreach (var mod in score.Mods)
+ modMultiplier *= mod.ScoreMultiplier;
+
+ return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
+ }
+
+ private class FakeHit : HitObject
+ {
+ private readonly Judgement judgement;
+
+ public override Judgement CreateJudgement() => judgement;
+
+ public FakeHit(Judgement judgement)
+ {
+ this.judgement = judgement;
+ }
+ }
+
+ private class FakeJudgement : Judgement
+ {
+ public override HitResult MaxResult { get; }
+
+ public FakeJudgement(HitResult maxResult)
+ {
+ MaxResult = maxResult;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index 07b5b53e0e..f92cfc2306 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -152,7 +152,6 @@ namespace osu.Game.Graphics.Containers
protected override void PopOut()
{
- base.PopOut();
previewTrackManager.StopAnyPlaying(this);
}
diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs
index e149e0abfb..afbec0eab4 100644
--- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs
+++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs
@@ -3,12 +3,19 @@
#nullable disable
+using System;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Sprites
{
public partial class OsuSpriteText : SpriteText
{
+ [Obsolete("Use TruncatingSpriteText instead.")]
+ public new bool Truncate
+ {
+ set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead.");
+ }
+
public OsuSpriteText()
{
Shadow = true;
diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs
new file mode 100644
index 0000000000..46abdbf09e
--- /dev/null
+++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs
@@ -0,0 +1,29 @@
+// 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.Cursor;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+
+namespace osu.Game.Graphics.Sprites
+{
+ ///
+ /// A derived version of which automatically shows non-truncated text in tooltip when required.
+ ///
+ public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip
+ {
+ ///
+ /// Whether a tooltip should be shown with non-truncated text on hover.
+ ///
+ public bool ShowTooltip { get; init; } = true;
+
+ public LocalisableString TooltipText => Text;
+
+ public override bool HandlePositionalInput => IsTruncated && ShowTooltip;
+
+ public TruncatingSpriteText()
+ {
+ ((SpriteText)this).Truncate = true;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs
index 9dbeba6449..c1ef573848 100644
--- a/osu.Game/Graphics/UserInterface/FPSCounter.cs
+++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs
@@ -167,9 +167,12 @@ namespace osu.Game.Graphics.UserInterface
{
base.Update();
+ double elapsedDrawFrameTime = drawClock.ElapsedFrameTime;
+ double elapsedUpdateFrameTime = updateClock.ElapsedFrameTime;
+
// If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device)
// we want to ignore really long periods of no processing.
- if (updateClock.ElapsedFrameTime > 10000)
+ if (elapsedUpdateFrameTime > 10000)
return;
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth);
@@ -178,17 +181,17 @@ namespace osu.Game.Graphics.UserInterface
// frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier).
bool aimRatesChanged = updateAimFPS();
- bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms;
+ bool hasUpdateSpike = displayedFrameTime < spike_time_ms && elapsedUpdateFrameTime > spike_time_ms;
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
- bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
+ bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && elapsedDrawFrameTime > spike_time_ms;
const float damp_time = 100;
- displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime);
+ displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, elapsedUpdateFrameTime, hasUpdateSpike ? 0 : damp_time, elapsedUpdateFrameTime);
if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
- displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
+ displayedFpsCount = 1000 / elapsedDrawFrameTime;
else
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed);
diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
index 5ef590d253..69e8df0286 100644
--- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
@@ -111,6 +111,10 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e)
{
+ // Handle case where a click is triggered via TriggerClick().
+ if (!IsHovered)
+ hover.FadeOutFromOne(1600);
+
hover.FlashColour(FlashColour, 800, Easing.OutQuint);
return base.OnClick(e);
}
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index 3230bb0569..b530172f3e 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface
{
new Drawable[]
{
- Text = new OsuSpriteText
+ Text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
Icon = new SpriteIcon
{
diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
index 63c98d7838..9de9eceb07 100644
--- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
@@ -16,6 +16,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
+using osu.Game.Localisation;
namespace osu.Game.Graphics.UserInterface
{
@@ -112,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface
private partial class CapsWarning : SpriteIcon, IHasTooltip
{
- public LocalisableString TooltipText => "caps lock is active";
+ public LocalisableString TooltipText => CommonStrings.CapsLockIsActive;
public CapsWarning()
{
diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs
index 7bd083f9d5..fb0a66cb8d 100644
--- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@@ -37,6 +38,14 @@ namespace osu.Game.Graphics.UserInterface
set => textBox.HoldFocus = value;
}
+ public LocalisableString PlaceholderText
+ {
+ get => textBox.PlaceholderText;
+ set => textBox.PlaceholderText = value;
+ }
+
+ public new bool HasFocus => textBox.HasFocus;
+
public void TakeFocus() => textBox.TakeFocus();
public void KillFocus() => textBox.KillFocus();
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index fdd96d3890..64268c73d0 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -119,6 +119,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus),
+ new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
+ new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
};
public IEnumerable ReplayKeyBindings => new[]
@@ -366,5 +368,11 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))]
EditorCycleNextBeatSnapDivisor,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SaveReplay))]
+ SaveReplay,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))]
+ ExportReplay,
}
}
diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs
new file mode 100644
index 0000000000..2183df9b52
--- /dev/null
+++ b/osu.Game/Localisation/AccountCreationStrings.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class AccountCreationStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation";
+
+ ///
+ /// "New player registration"
+ ///
+ public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New player registration");
+
+ ///
+ /// "Let's get you started"
+ ///
+ public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"Let's get you started");
+
+ ///
+ /// "Let's create an account!"
+ ///
+ public static LocalisableString LetsCreateAnAccount => new TranslatableString(getKey(@"lets_create_an_account"), @"Let's create an account!");
+
+ ///
+ /// "Help, I can't access my account!"
+ ///
+ public static LocalisableString MultiAccountWarningHelp => new TranslatableString(getKey(@"multi_account_warning_help"), @"Help, I can't access my account!");
+
+ ///
+ /// "I understand. This account isn't for me."
+ ///
+ public static LocalisableString MultiAccountWarningAccept => new TranslatableString(getKey(@"multi_account_warning_accept"), @"I understand. This account isn't for me.");
+
+ ///
+ /// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"
+ ///
+ public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
+
+ ///
+ /// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever."
+ ///
+ public static LocalisableString EmailDescription1 => new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
+
+ ///
+ /// " Make sure to get it right!"
+ ///
+ public static LocalisableString EmailDescription2 => new TranslatableString(getKey(@"email_description_2"), @" Make sure to get it right!");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs
index fed7b6cab7..c9223db246 100644
--- a/osu.Game/Localisation/CommonStrings.cs
+++ b/osu.Game/Localisation/CommonStrings.cs
@@ -154,6 +154,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit");
+ ///
+ /// "Caps lock is active"
+ ///
+ public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"Caps lock is active");
+
///
/// "Revert to default"
///
diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs
index 20258b9c35..077bd92d4f 100644
--- a/osu.Game/Localisation/EditorStrings.cs
+++ b/osu.Game/Localisation/EditorStrings.cs
@@ -109,6 +109,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
+ ///
+ /// "Limit distance snap placement to current time"
+ ///
+ public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index aa608a603b..9e53b23180 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -324,6 +324,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
+ ///
+ /// "Save replay"
+ ///
+ public static LocalisableString SaveReplay => new TranslatableString(getKey(@"save_replay"), @"Save replay");
+
+ ///
+ /// "Export replay"
+ ///
+ public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/LoginPanelStrings.cs b/osu.Game/Localisation/LoginPanelStrings.cs
new file mode 100644
index 0000000000..19b0ca3b52
--- /dev/null
+++ b/osu.Game/Localisation/LoginPanelStrings.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class LoginPanelStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.LoginPanel";
+
+ ///
+ /// "Do not disturb"
+ ///
+ public static LocalisableString DoNotDisturb => new TranslatableString(getKey(@"do_not_disturb"), @"Do not disturb");
+
+ ///
+ /// "Appear offline"
+ ///
+ public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline");
+
+ ///
+ /// "Signed in"
+ ///
+ public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
+
+ ///
+ /// "Account"
+ ///
+ public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"Account");
+
+ ///
+ /// "Remember username"
+ ///
+ public static LocalisableString RememberUsername => new TranslatableString(getKey(@"remember_username"), @"Remember username");
+
+ ///
+ /// "Stay signed in"
+ ///
+ public static LocalisableString StaySignedIn => new TranslatableString(getKey(@"stay_signed_in"), @"Stay signed in");
+
+ ///
+ /// "Register"
+ ///
+ public static LocalisableString Register => new TranslatableString(getKey(@"register"), @"Register");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs
index f11c52ee20..05dcf138d7 100644
--- a/osu.Game/Localisation/ModSelectOverlayStrings.cs
+++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs
@@ -1,4 +1,4 @@
-// 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.
using osu.Framework.Localisation;
@@ -39,6 +39,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods");
+ ///
+ /// "tab to search..."
+ ///
+ public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
-}
+}
\ No newline at end of file
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 16afef8e30..bf9baa4414 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -34,7 +34,8 @@ namespace osu.Game.Online.API
public string AccessToken => "token";
- public bool IsLoggedIn => State.Value == APIState.Online;
+ ///
+ public bool IsLoggedIn => State.Value > APIState.Offline;
public string ProvidedUsername => LocalUser.Value.Username;
@@ -114,8 +115,10 @@ namespace osu.Game.Online.API
public void Logout()
{
- LocalUser.Value = new GuestUser();
state.Value = APIState.Offline;
+ // must happen after `state.Value` is changed such that subscribers to that bindable's value changes see the correct user.
+ // compare: `APIAccess.Logout()`.
+ LocalUser.Value = new GuestUser();
}
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
index 7d6740ee46..902b651be9 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
@@ -63,6 +63,16 @@ namespace osu.Game.Online.API.Requests.Responses
set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds;
}
+ [JsonIgnore]
+ public double HitLength { get; set; }
+
+ [JsonProperty(@"hit_length")]
+ private double hitLengthInSeconds
+ {
+ get => TimeSpan.FromMilliseconds(HitLength).TotalSeconds;
+ set => HitLength = TimeSpan.FromSeconds(value).TotalMilliseconds;
+ }
+
[JsonProperty(@"convert")]
public bool Convert { get; set; }
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 3768dad370..a80639d4ff 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -28,6 +29,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Localisation;
using osu.Framework.Logging;
+using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
@@ -281,6 +283,52 @@ namespace osu.Game
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+ private readonly List dragDropFiles = new List();
+ private ScheduledDelegate dragDropImportSchedule;
+
+ public override void SetHost(GameHost host)
+ {
+ base.SetHost(host);
+
+ if (host.Window is SDL2Window sdlWindow)
+ {
+ sdlWindow.DragDrop += path =>
+ {
+ // on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
+ if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
+ {
+ HandleLink(path);
+ return;
+ }
+
+ lock (dragDropFiles)
+ {
+ dragDropFiles.Add(path);
+
+ Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import");
+
+ // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
+ // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
+ dragDropImportSchedule?.Cancel();
+ dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100);
+ }
+ };
+ }
+ }
+
+ private void handlePendingDragDropImports()
+ {
+ lock (dragDropFiles)
+ {
+ Logger.Log($"Handling batch import of {dragDropFiles.Count} files");
+
+ string[] paths = dragDropFiles.ToArray();
+ dragDropFiles.Clear();
+
+ Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
+ }
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 219cbe7eef..9ad507d82a 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -17,6 +17,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
@@ -71,7 +72,7 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 20),
- Text = "Let's create an account!",
+ Text = AccountCreationStrings.LetsCreateAnAccount
},
usernameTextBox = new OsuTextBox
{
@@ -86,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation
},
emailTextBox = new OsuTextBox
{
- PlaceholderText = "email address",
+ PlaceholderText = ModelValidationStrings.UserAttributesUserEmail.ToLower(),
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this
},
@@ -118,7 +119,7 @@ namespace osu.Game.Overlays.AccountCreation
AutoSizeAxes = Axes.Y,
Child = new SettingsButton
{
- Text = "Register",
+ Text = LoginPanelStrings.Register,
Margin = new MarginPadding { Vertical = 20 },
Action = performRegistration
}
@@ -132,10 +133,10 @@ namespace osu.Game.Overlays.AccountCreation
textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox };
- usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
+ usernameDescription.AddText(AccountCreationStrings.UsernameDescription);
- emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
- emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
+ emailAddressDescription.AddText(AccountCreationStrings.EmailDescription1);
+ emailAddressDescription.AddText(AccountCreationStrings.EmailDescription2, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
passwordDescription.AddText("At least ");
characterCheckText = passwordDescription.AddText("8 characters long");
diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
index a833a871f9..0fbf6ba59e 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
@@ -17,6 +17,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Screens.Menu;
using osuTK;
using osuTK.Graphics;
+using osu.Game.Localisation;
namespace osu.Game.Overlays.AccountCreation
{
@@ -101,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation
},
new SettingsButton
{
- Text = "Help, I can't access my account!",
+ Text = AccountCreationStrings.MultiAccountWarningHelp,
Margin = new MarginPadding { Top = 50 },
Action = () => game?.OpenUrlExternally(help_centre_url)
},
new DangerousSettingsButton
{
- Text = "I understand. This account isn't for me.",
+ Text = AccountCreationStrings.MultiAccountWarningAccept,
Action = () => this.Push(new ScreenEntry())
},
furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12))
diff --git a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs
index 4becb225f8..610b9ee282 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenWelcome.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 osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
@@ -12,6 +11,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.Menu;
using osuTK;
+using osu.Game.Localisation;
namespace osu.Game.Overlays.AccountCreation
{
@@ -46,18 +46,18 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
- Text = "New Player Registration",
+ Text = AccountCreationStrings.NewPlayerRegistration.ToTitle(),
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 12),
- Text = "let's get you started",
+ Text = AccountCreationStrings.LetsGetYouStarted.ToLower(),
},
new SettingsButton
{
- Text = "Let's create an account!",
+ Text = AccountCreationStrings.LetsCreateAnAccount,
Margin = new MarginPadding { Vertical = 120 },
Action = () => this.Push(new ScreenWarning())
}
diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs
index 6f79316670..ef2e055eae 100644
--- a/osu.Game/Overlays/AccountCreationOverlay.cs
+++ b/osu.Game/Overlays/AccountCreationOverlay.cs
@@ -90,7 +90,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
this.FadeIn(transition_time, Easing.OutQuint);
if (welcomeScreen.GetChildScreen() != null)
diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
index 4a9a3d8089..0b1befe7b9 100644
--- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs
+++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
@@ -58,23 +58,25 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDisplay()
{
- bpm.Value = BeatmapSet?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-";
-
if (beatmapInfo == null)
{
+ bpm.Value = "-";
+
length.Value = string.Empty;
circleCount.Value = string.Empty;
sliderCount.Value = string.Empty;
}
else
{
- length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration());
+ bpm.Value = beatmapInfo.BPM.ToLocalisableString(@"0.##");
+
length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration();
- var onlineInfo = beatmapInfo as IBeatmapOnlineInfo;
+ if (beatmapInfo is not IBeatmapOnlineInfo onlineInfo) return;
- circleCount.Value = (onlineInfo?.CircleCount ?? 0).ToLocalisableString(@"N0");
- sliderCount.Value = (onlineInfo?.SliderCount ?? 0).ToLocalisableString(@"N0");
+ circleCount.Value = onlineInfo.CircleCount.ToLocalisableString(@"N0");
+ sliderCount.Value = onlineInfo.SliderCount.ToLocalisableString(@"N0");
+ length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(onlineInfo.HitLength).ToFormattedDuration());
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 6d89313979..b53b7826f3 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -47,9 +47,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
[Resolved]
private RulesetStore rulesets { get; set; }
- [Resolved]
- private ScoreManager scoreManager { get; set; }
-
private GetScoresRequest getScoresRequest;
private CancellationTokenSource loadCancellationSource;
@@ -85,7 +82,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
MD5Hash = apiBeatmap.MD5Hash
};
- var scores = scoreManager.OrderByTotalScore(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo))).ToArray();
+ var scores = value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).OrderByTotalScore().ToArray();
var topScore = scores.First();
scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints());
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
index 57b6f6268c..21b6147113 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
new Drawable?[]
{
createIcon(),
- text = new OsuSpriteText
+ text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -94,7 +94,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
createMentionPill(),
close = createCloseButton(),
diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs
index fd5e0e9836..87e787fcb8 100644
--- a/osu.Game/Overlays/Chat/ChatTextBar.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBar.cs
@@ -73,14 +73,13 @@ namespace osu.Game.Overlays.Chat
Width = chatting_text_width,
Masking = true,
Padding = new MarginPadding { Horizontal = padding },
- Child = chattingText = new OsuSpriteText
+ Child = chattingText = new TruncatingSpriteText
{
MaxWidth = chatting_text_width - padding * 2,
Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- Truncate = true,
},
},
searchIconContainer = new Container
diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs
index 21c3bd4b40..67191f6836 100644
--- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs
+++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs
@@ -92,11 +92,9 @@ namespace osu.Game.Overlays.Chat
Action = openUserProfile;
- drawableText = new OsuSpriteText
+ drawableText = new TruncatingSpriteText
{
Shadow = false,
- Truncate = true,
- EllipsisString = "…",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 96dbfe31f3..87df08ceec 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -276,8 +276,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
-
this.MoveToY(0, transition_length, Easing.OutQuint);
this.FadeIn(transition_length, Easing.OutQuint);
}
diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs
index 2af7dd3093..02bcbb9d05 100644
--- a/osu.Game/Overlays/Comments/CommentEditor.cs
+++ b/osu.Game/Overlays/Comments/CommentEditor.cs
@@ -13,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Online.API;
using osuTK;
using osuTK.Graphics;
@@ -24,19 +25,37 @@ namespace osu.Game.Overlays.Comments
protected abstract LocalisableString FooterText { get; }
- protected abstract LocalisableString CommitButtonText { get; }
-
- protected abstract LocalisableString TextBoxPlaceholder { get; }
-
protected FillFlowContainer ButtonsContainer { get; private set; } = null!;
protected readonly Bindable Current = new Bindable(string.Empty);
private RoundedButton commitButton = null!;
+ private RoundedButton logInButton = null!;
private LoadingSpinner loadingSpinner = null!;
protected TextBox TextBox { get; private set; } = null!;
+ [Resolved]
+ protected IAPIProvider API { get; private set; } = null!;
+
+ [Resolved]
+ private LoginOverlay? loginOverlay { get; set; }
+
+ private readonly IBindable apiState = new Bindable();
+
+ ///
+ /// Returns the text content of the main action button.
+ /// When is , the text will apply to a button that posts a comment.
+ /// When is , the text will apply to a button that directs the user to the login overlay.
+ ///
+ protected abstract LocalisableString GetButtonText(bool isLoggedIn);
+
+ ///
+ /// Returns the placeholder text for the comment box.
+ ///
+ /// Whether the current user is logged in.
+ protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn);
+
protected bool ShowLoadingSpinner
{
set
@@ -78,7 +97,6 @@ namespace osu.Game.Overlays.Comments
{
Height = 40,
RelativeSizeAxes = Axes.X,
- PlaceholderText = TextBoxPlaceholder,
Current = Current
},
new Container
@@ -113,10 +131,19 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
- Child = commitButton = new EditorButton
+ Children = new Drawable[]
{
- Text = CommitButtonText,
- Action = () => OnCommit(Current.Value)
+ commitButton = new EditorButton
+ {
+ Action = () => OnCommit(Current.Value),
+ Text = GetButtonText(true)
+ },
+ logInButton = new EditorButton
+ {
+ Width = 100,
+ Action = () => loginOverlay?.Show(),
+ Text = GetButtonText(false)
+ }
}
},
loadingSpinner = new LoadingSpinner
@@ -134,12 +161,14 @@ namespace osu.Game.Overlays.Comments
});
TextBox.OnCommit += (_, _) => commitButton.TriggerClick();
+ apiState.BindTo(API.State);
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateCommitButtonState(), true);
+ apiState.BindValueChanged(updateStateForLoggedIn, true);
}
protected abstract void OnCommit(string text);
@@ -147,6 +176,25 @@ namespace osu.Game.Overlays.Comments
private void updateCommitButtonState() =>
commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
+ private void updateStateForLoggedIn(ValueChangedEvent state) => Schedule(() =>
+ {
+ bool isAvailable = state.NewValue > APIState.Offline;
+
+ TextBox.PlaceholderText = GetPlaceholderText(isAvailable);
+ TextBox.ReadOnly = !isAvailable;
+
+ if (isAvailable)
+ {
+ commitButton.Show();
+ logInButton.Hide();
+ }
+ else
+ {
+ commitButton.Hide();
+ logInButton.Show();
+ }
+ });
+
private partial class EditorTextBox : OsuTextBox
{
protected override float LeftRightPadding => side_padding;
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index 24536fe460..af5f4dd280 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -405,17 +405,16 @@ namespace osu.Game.Overlays.Comments
[Resolved]
private CommentsContainer commentsContainer { get; set; }
- [Resolved]
- private IAPIProvider api { get; set; }
-
public Action OnPost;
//TODO should match web, left empty due to no multiline support
protected override LocalisableString FooterText => default;
- protected override LocalisableString CommitButtonText => CommonStrings.ButtonsPost;
+ protected override LocalisableString GetButtonText(bool isLoggedIn) =>
+ isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew;
- protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderNew;
+ protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
+ isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin;
protected override void OnCommit(string text)
{
@@ -432,7 +431,7 @@ namespace osu.Game.Overlays.Comments
Current.Value = string.Empty;
OnPost?.Invoke(cb);
});
- api.Queue(req);
+ API.Queue(req);
}
}
}
diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs
index 8aca183dee..dd4c35ef20 100644
--- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs
+++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs
@@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Framework.Logging;
-using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
@@ -18,16 +17,17 @@ namespace osu.Game.Overlays.Comments
[Resolved]
private CommentsContainer commentsContainer { get; set; } = null!;
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
private readonly Comment parentComment;
public Action? OnPost;
protected override LocalisableString FooterText => default;
- protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply;
- protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply;
+
+ protected override LocalisableString GetButtonText(bool isLoggedIn) =>
+ isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply;
+
+ protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
+ isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin;
public ReplyCommentEditor(Comment parent)
{
@@ -38,7 +38,8 @@ namespace osu.Game.Overlays.Comments
{
base.LoadComplete();
- GetContainingInputManager().ChangeFocus(TextBox);
+ if (!TextBox.ReadOnly)
+ GetContainingInputManager().ChangeFocus(TextBox);
}
protected override void OnCommit(string text)
@@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Comments
Logger.Error(e, "Posting reply comment failed.");
});
req.Success += cb => Schedule(processPostedComments, cb);
- api.Queue(req);
+ API.Queue(req);
}
private void processPostedComments(CommentBundle cb)
diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
index 792d6cc785..f36e6b49bb 100644
--- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
+++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
@@ -100,17 +100,15 @@ namespace osu.Game.Overlays.Dashboard.Home
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
- Truncate = true,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = BeatmapSet.Title
},
- new OsuSpriteText
+ new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
- Truncate = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Text = BeatmapSet.Artist
},
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index 098a5d0a33..005162bbcc 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -99,7 +99,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
}
diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs
index af145c418c..9f9b8d9342 100644
--- a/osu.Game/Overlays/Login/LoginForm.cs
+++ b/osu.Game/Overlays/Login/LoginForm.cs
@@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
using osuTK;
+using osu.Game.Localisation;
namespace osu.Game.Overlays.Login
{
@@ -47,7 +48,7 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X;
ErrorTextFlowContainer errorText;
- LinkFlowContainer forgottenPaswordLink;
+ LinkFlowContainer forgottenPasswordLink;
Children = new Drawable[]
{
@@ -71,15 +72,15 @@ namespace osu.Game.Overlays.Login
},
new SettingsCheckbox
{
- LabelText = "Remember username",
+ LabelText = LoginPanelStrings.RememberUsername,
Current = config.GetBindable(OsuSetting.SaveUsername),
},
new SettingsCheckbox
{
- LabelText = "Stay signed in",
+ LabelText = LoginPanelStrings.StaySignedIn,
Current = config.GetBindable(OsuSetting.SavePassword),
},
- forgottenPaswordLink = new LinkFlowContainer
+ forgottenPasswordLink = new LinkFlowContainer
{
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
RelativeSizeAxes = Axes.X,
@@ -105,7 +106,7 @@ namespace osu.Game.Overlays.Login
},
new SettingsButton
{
- Text = "Register",
+ Text = LoginPanelStrings.Register,
Action = () =>
{
RequestHide?.Invoke();
@@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Login
}
};
- forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
+ forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
password.OnCommit += (_, _) => performLogin();
diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs
index 44f2f3273a..79569ada65 100644
--- a/osu.Game/Overlays/Login/LoginPanel.cs
+++ b/osu.Game/Overlays/Login/LoginPanel.cs
@@ -6,6 +6,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
@@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Login
{
new OsuSpriteText
{
- Text = "ACCOUNT",
+ Text = LoginPanelStrings.Account.ToUpper(),
Margin = new MarginPadding { Bottom = 5 },
Font = OsuFont.GetFont(weight: FontWeight.Bold),
},
@@ -115,7 +116,7 @@ namespace osu.Game.Overlays.Login
},
};
- linkFlow.AddLink("cancel", api.Logout, string.Empty);
+ linkFlow.AddLink(Resources.Localisation.Web.CommonStrings.ButtonsCancel.ToLower(), api.Logout, string.Empty);
break;
case APIState.Online:
@@ -140,7 +141,7 @@ namespace osu.Game.Overlays.Login
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Text = "Signed in",
+ Text = LoginPanelStrings.SignedIn,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
Margin = new MarginPadding { Top = 5, Bottom = 5 },
},
diff --git a/osu.Game/Overlays/Login/UserAction.cs b/osu.Game/Overlays/Login/UserAction.cs
index 7a18e38109..d4d639f2fb 100644
--- a/osu.Game/Overlays/Login/UserAction.cs
+++ b/osu.Game/Overlays/Login/UserAction.cs
@@ -1,11 +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.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
+using osu.Game.Localisation;
namespace osu.Game.Overlays.Login
{
@@ -14,13 +12,13 @@ namespace osu.Game.Overlays.Login
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))]
Online,
- [Description(@"Do not disturb")]
+ [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.DoNotDisturb))]
DoNotDisturb,
- [Description(@"Appear offline")]
+ [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
AppearOffline,
- [Description(@"Sign out")]
+ [LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))]
SignOut,
}
}
diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs
index 536811dfcf..8b60024682 100644
--- a/osu.Game/Overlays/LoginOverlay.cs
+++ b/osu.Game/Overlays/LoginOverlay.cs
@@ -75,8 +75,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
-
panel.Bounding = true;
this.FadeIn(transition_time, Easing.OutQuint);
diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs
index bd895fe6bf..eba35ec6f9 100644
--- a/osu.Game/Overlays/MedalOverlay.cs
+++ b/osu.Game/Overlays/MedalOverlay.cs
@@ -246,9 +246,13 @@ namespace osu.Game.Overlays
}
}
+ protected override void PopIn()
+ {
+ this.FadeIn(200);
+ }
+
protected override void PopOut()
{
- base.PopOut();
this.FadeOut(200);
}
diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs
index 3e5a3b12d1..817b6beac3 100644
--- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs
+++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs
@@ -6,16 +6,13 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
- public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler
+ public partial class DeselectAllModsButton : ShearedButton
{
private readonly Bindable> selectedMods = new Bindable>();
@@ -39,18 +36,5 @@ namespace osu.Game.Overlays.Mods
{
Enabled.Value = selectedMods.Value.Any();
}
-
- public bool OnPressed(KeyBindingPressEvent e)
- {
- if (e.Repeat || e.Action != GlobalAction.DeselectAllMods)
- return false;
-
- TriggerClick();
- return true;
- }
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
- }
}
}
diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs
index 4f3c18fc43..59a631a7b5 100644
--- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs
+++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input
if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch))
return false;
- var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray();
+ var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.Visible).ToArray();
if (matchingMods.Length == 0)
return false;
diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs
index dedb556304..e638063438 100644
--- a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs
+++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input
if (index < 0)
return false;
- var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index);
+ var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index);
if (modState == null)
return false;
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index fe42cc0abf..d65c94d14d 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -46,7 +46,8 @@ namespace osu.Game.Overlays.Mods
foreach (var mod in availableMods)
{
mod.Active.BindValueChanged(_ => updateState());
- mod.Filtered.BindValueChanged(_ => updateState());
+ mod.MatchingTextFilter.BindValueChanged(_ => updateState());
+ mod.ValidForSelection.BindValueChanged(_ => updateState());
}
updateState();
@@ -145,12 +146,17 @@ namespace osu.Game.Overlays.Mods
private void updateState()
{
- Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1;
+ Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1;
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{
- toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0;
- toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
+ bool anyPanelsVisible = availableMods.Any(panel => panel.Visible);
+
+ toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0;
+
+ // checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables.
+ if (anyPanelsVisible)
+ toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value);
}
}
@@ -195,7 +201,7 @@ namespace osu.Game.Overlays.Mods
{
pendingSelectionOperations.Clear();
- foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value))
+ foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible))
pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
}
@@ -206,8 +212,13 @@ namespace osu.Game.Overlays.Mods
{
pendingSelectionOperations.Clear();
- foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value))
- pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
+ foreach (var button in availableMods.Where(b => b.Active.Value))
+ {
+ if (!button.Visible)
+ button.Active.Value = false;
+ else
+ pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
+ }
}
///
diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs
index b5fee9d116..f294b1892d 100644
--- a/osu.Game/Overlays/Mods/ModPanel.cs
+++ b/osu.Game/Overlays/Mods/ModPanel.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -11,11 +14,10 @@ using osuTK;
namespace osu.Game.Overlays.Mods
{
- public partial class ModPanel : ModSelectPanel
+ public partial class ModPanel : ModSelectPanel, IFilterable
{
public Mod Mod => modState.Mod;
public override BindableBool Active => modState.Active;
- public BindableBool Filtered => modState.Filtered;
protected override float IdleSwitchWidth => 54;
protected override float ExpandedSwitchWidth => 70;
@@ -54,7 +56,8 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
- Filtered.BindValueChanged(_ => updateFilterState(), true);
+ modState.ValidForSelection.BindValueChanged(_ => updateFilterState());
+ modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true);
}
protected override void Select()
@@ -71,9 +74,25 @@ namespace osu.Game.Overlays.Mods
#region Filtering support
+ ///
+ public bool Visible => modState.Visible;
+
+ public override IEnumerable FilterTerms => new[]
+ {
+ Mod.Name,
+ Mod.Acronym,
+ Mod.Description
+ };
+
+ public override bool MatchingFilter
+ {
+ get => modState.MatchingTextFilter.Value;
+ set => modState.MatchingTextFilter.Value = value;
+ }
+
private void updateFilterState()
{
- this.FadeTo(Filtered.Value ? 0 : 1);
+ this.FadeTo(Visible ? 1 : 0);
}
#endregion
diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs
index 8bcb5e4e4e..00f6e36972 100644
--- a/osu.Game/Overlays/Mods/ModPresetPanel.cs
+++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
@@ -81,6 +82,27 @@ namespace osu.Game.Overlays.Mods
Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value);
}
+ #region Filtering support
+
+ public override IEnumerable FilterTerms => getFilterTerms();
+
+ private IEnumerable getFilterTerms()
+ {
+ var preset = Preset.Value;
+
+ yield return preset.Name;
+ yield return preset.Description;
+
+ foreach (Mod mod in preset.Mods)
+ {
+ yield return mod.Name;
+ yield return mod.Acronym;
+ yield return mod.Description;
+ }
+ }
+
+ #endregion
+
#region IHasCustomTooltip
public ModPreset TooltipContent => Preset.Value;
diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs
new file mode 100644
index 0000000000..8787530d5c
--- /dev/null
+++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs
@@ -0,0 +1,27 @@
+// 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.Containers;
+
+namespace osu.Game.Overlays.Mods
+{
+ public partial class ModSearchContainer : SearchContainer
+ {
+ public new string SearchTerm
+ {
+ get => base.SearchTerm;
+ set
+ {
+ if (value == SearchTerm)
+ return;
+
+ base.SearchTerm = value;
+
+ // Manual filtering here is required because ModColumn can be hidden when search term applied,
+ // causing the whole SearchContainer to become non-present and never actually perform a subsequent
+ // filter.
+ Filter();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs
index e6d7bcd97d..338ebdaef4 100644
--- a/osu.Game/Overlays/Mods/ModSelectColumn.cs
+++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs
@@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods
///
public readonly Bindable Active = new BindableBool(true);
+ public string SearchTerm
+ {
+ set => ItemsFlow.SearchTerm = value;
+ }
+
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected readonly Container ControlContainer;
- protected readonly FillFlowContainer ItemsFlow;
+ protected readonly ModSearchContainer ItemsFlow;
private readonly TextFlowContainer headerText;
private readonly Box headerBackground;
@@ -150,7 +155,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Both,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
- Child = ItemsFlow = new FillFlowContainer
+ Child = ItemsFlow = new ModSearchContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 38ae8c68cb..9035503723 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -12,6 +12,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
@@ -25,10 +27,11 @@ using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
- public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler
+ public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler
{
public const int BUTTON_WIDTH = 200;
@@ -64,6 +67,14 @@ namespace osu.Game.Overlays.Mods
}
}
+ public string SearchTerm
+ {
+ get => SearchTextBox.Current.Value;
+ set => SearchTextBox.Current.Value = value;
+ }
+
+ public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
+
///
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
///
@@ -94,7 +105,7 @@ namespace osu.Game.Overlays.Mods
};
}
- yield return new DeselectAllModsButton(this);
+ yield return deselectAllModsButton = new DeselectAllModsButton(this);
}
private readonly Bindable>> globalAvailableMods = new Bindable>>();
@@ -107,11 +118,14 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer footerButtonFlow = null!;
+ private DeselectAllModsButton deselectAllModsButton = null!;
+ private Container aboveColumnsContent = null!;
private DifficultyMultiplierDisplay? multiplierDisplay;
protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; }
+ protected SelectAllModsButton? SelectAllModsButton { get; set; }
private Sample? columnAppearSample;
@@ -146,6 +160,17 @@ namespace osu.Game.Overlays.Mods
MainAreaContent.AddRange(new Drawable[]
{
+ aboveColumnsContent = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = ModsEffectDisplay.HEIGHT,
+ Padding = new MarginPadding { Horizontal = 100 },
+ Child = SearchTextBox = new ShearedSearchTextBox
+ {
+ HoldFocus = false,
+ Width = 300
+ }
+ },
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
@@ -153,7 +178,7 @@ namespace osu.Game.Overlays.Mods
{
Padding = new MarginPadding
{
- Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING,
+ Top = ModsEffectDisplay.HEIGHT + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
@@ -186,18 +211,10 @@ namespace osu.Game.Overlays.Mods
if (ShowTotalMultiplier)
{
- MainAreaContent.Add(new Container
+ aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- AutoSizeAxes = Axes.X,
- Height = ModsEffectDisplay.HEIGHT,
- Margin = new MarginPadding { Horizontal = 100 },
- Child = multiplierDisplay = new DifficultyMultiplierDisplay
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre
- },
+ Origin = Anchor.TopRight
});
}
@@ -226,6 +243,14 @@ namespace osu.Game.Overlays.Mods
globalAvailableMods.BindTo(game.AvailableMods);
}
+ public override void Hide()
+ {
+ base.Hide();
+
+ // clear search for next user interaction with mod overlay
+ SearchTextBox.Current.Value = string.Empty;
+ }
+
private ModSettingChangeTracker? modSettingChangeTracker;
protected override void LoadComplete()
@@ -263,6 +288,12 @@ namespace osu.Game.Overlays.Mods
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
+ SearchTextBox.Current.BindValueChanged(query =>
+ {
+ foreach (var column in columnFlow.Columns)
+ column.SearchTerm = query.NewValue;
+ }, true);
+
// Start scrolled slightly to the right to give the user a sense that
// there is more horizontal content available.
ScheduleAfterChildren(() =>
@@ -272,6 +303,13 @@ namespace osu.Game.Overlays.Mods
});
}
+ protected override void Update()
+ {
+ base.Update();
+
+ SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch;
+ }
+
///
/// Select all visible mods in all columns.
///
@@ -344,7 +382,7 @@ namespace osu.Game.Overlays.Mods
private void filterMods()
{
foreach (var modState in allAvailableMods)
- modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod);
+ modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
}
private void updateMultiplier()
@@ -469,7 +507,7 @@ namespace osu.Game.Overlays.Mods
base.PopIn();
- multiplierDisplay?
+ aboveColumnsContent
.FadeIn(fade_in_duration, Easing.OutQuint)
.MoveToY(0, fade_in_duration, Easing.OutQuint);
@@ -479,7 +517,7 @@ namespace osu.Game.Overlays.Mods
{
var column = columnFlow[i].Column;
- bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value);
+ bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.Visible);
double delay = allFiltered ? 0 : nonFilteredColumnCount * 30;
double duration = allFiltered ? 0 : fade_in_duration;
@@ -527,7 +565,7 @@ namespace osu.Game.Overlays.Mods
base.PopOut();
- multiplierDisplay?
+ aboveColumnsContent
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
.MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
@@ -541,7 +579,7 @@ namespace osu.Game.Overlays.Mods
if (column is ModColumn modColumn)
{
- allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value);
+ allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible);
modColumn.FlushPendingSelections();
}
@@ -578,10 +616,38 @@ namespace osu.Game.Overlays.Mods
// This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection:
+ // Pressing toggle should completely hide the overlay in one shot.
+ hideOverlay(true);
+ return true;
+
+ // This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button.
+ // Attempting to handle this action locally in both places leads to a possible scenario
+ // wherein activating the binding will both change the contents of the search text box and deselect all mods.
+ case GlobalAction.DeselectAllMods:
+ {
+ if (!SearchTextBox.HasFocus)
+ {
+ deselectAllModsButton.TriggerClick();
+ return true;
+ }
+
+ break;
+ }
+
case GlobalAction.Select:
{
- // Pressing toggle or select should completely hide the overlay in one shot.
- hideOverlay(true);
+ // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty.
+ if (string.IsNullOrEmpty(SearchTerm))
+ {
+ hideOverlay(true);
+ return true;
+ }
+
+ ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);
+
+ if (firstMod is not null)
+ firstMod.Active.Value = !firstMod.Active.Value;
+
return true;
}
}
@@ -603,6 +669,39 @@ namespace osu.Game.Overlays.Mods
}
}
+ ///
+ ///
+ /// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button.
+ /// Attempting to handle this action locally in both places leads to a possible scenario
+ /// wherein activating the "select all" platform binding will both select all text in the search box and select all mods.
+ /// >
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
+ return false;
+
+ SelectAllModsButton.TriggerClick();
+ return true;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.Repeat || e.Key != Key.Tab)
+ return false;
+
+ // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
+ if (SearchTextBox.HasFocus)
+ SearchTextBox.KillFocus();
+ else
+ SearchTextBox.TakeFocus();
+
+ return true;
+ }
+
#endregion
#region Sample playback control
@@ -743,6 +842,9 @@ namespace osu.Game.Overlays.Mods
if (!Active.Value)
RequestScroll?.Invoke(this);
+ // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action.
+ Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null));
+
return true;
}
diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs
index be01d239b6..29f4c93e88 100644
--- a/osu.Game/Overlays/Mods/ModSelectPanel.cs
+++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -25,7 +26,7 @@ using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
- public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour
+ public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour, IFilterable
{
public abstract BindableBool Active { get; }
@@ -123,23 +124,23 @@ namespace osu.Game.Overlays.Mods
Direction = FillDirection.Vertical,
Children = new[]
{
- titleText = new OsuSpriteText
+ titleText = new TruncatingSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
- }
+ },
+ ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
},
- descriptionText = new OsuSpriteText
+ descriptionText = new TruncatingSpriteText
{
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
- Truncate = true,
- Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
+ Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
+ ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
}
}
}
@@ -199,6 +200,9 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value)
return;
+ if (!IsPresent)
+ return;
+
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
if (enoughTimePassedSinceLastPlayback)
@@ -277,5 +281,28 @@ namespace osu.Game.Overlays.Mods
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
}
+
+ #region IFilterable
+
+ public abstract IEnumerable FilterTerms { get; }
+
+ private bool matchingFilter = true;
+
+ public virtual bool MatchingFilter
+ {
+ get => matchingFilter;
+ set
+ {
+ if (matchingFilter == value)
+ return;
+
+ matchingFilter = value;
+ this.FadeTo(value ? 1 : 0);
+ }
+ }
+
+ public bool FilteringActive { set { } }
+
+ #endregion
}
}
diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs
index 3ee890e876..7a5bc0f3ae 100644
--- a/osu.Game/Overlays/Mods/ModState.cs
+++ b/osu.Game/Overlays/Mods/ModState.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.Bindables;
using osu.Game.Rulesets.Mods;
@@ -32,9 +30,21 @@ namespace osu.Game.Overlays.Mods
public bool PendingConfiguration { get; set; }
///
- /// Whether the mod is currently filtered out due to not matching imposed criteria.
+ /// Whether the mod is currently valid for selection.
+ /// This can be in scenarios such as the free mod select overlay, where not all mods are selectable
+ /// regardless of search criteria imposed by the user selecting.
///
- public BindableBool Filtered { get; } = new BindableBool();
+ public BindableBool ValidForSelection { get; } = new BindableBool(true);
+
+ ///
+ /// Whether the mod is matching the current textual filter.
+ ///
+ public BindableBool MatchingTextFilter { get; } = new BindableBool(true);
+
+ ///
+ /// Whether the matches all applicable filters and visible for the user to select.
+ ///
+ public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value;
public ModState(Mod mod)
{
diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs
index f4b8025227..bb61cdc35d 100644
--- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs
+++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs
@@ -1,14 +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.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Input;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
@@ -16,10 +11,11 @@ using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Overlays.Mods
{
- public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler
+ public partial class SelectAllModsButton : ShearedButton
{
private readonly Bindable> selectedMods = new Bindable>();
private readonly Bindable>> availableMods = new Bindable>>();
+ private readonly Bindable searchTerm = new Bindable();
public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay)
: base(ModSelectOverlay.BUTTON_WIDTH)
@@ -29,6 +25,7 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindTo(modSelectOverlay.SelectedMods);
availableMods.BindTo(modSelectOverlay.AvailableMods);
+ searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current);
}
protected override void LoadComplete()
@@ -37,6 +34,7 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
+ searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
updateEnabledState();
}
@@ -44,20 +42,7 @@ namespace osu.Game.Overlays.Mods
{
Enabled.Value = availableMods.Value
.SelectMany(pair => pair.Value)
- .Any(modState => !modState.Active.Value && !modState.Filtered.Value);
- }
-
- public bool OnPressed(KeyBindingPressEvent e)
- {
- if (e.Repeat || e.Action != PlatformAction.SelectAll)
- return false;
-
- TriggerClick();
- return true;
- }
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
+ .Any(modState => !modState.Active.Value && modState.Visible);
}
}
}
diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs
index 7f7b09a62c..a372ec70db 100644
--- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs
+++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs
@@ -130,7 +130,6 @@ namespace osu.Game.Overlays.Mods
{
const double fade_in_duration = 400;
- base.PopIn();
this.FadeIn(fade_in_duration, Easing.OutQuint);
Header.MoveToY(0, fade_in_duration, Easing.OutQuint);
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 1ad5a8c08b..0d175a624c 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -316,6 +316,8 @@ namespace osu.Game.Overlays
var queuedTrack = getQueuedTrack();
var lastTrack = CurrentTrack;
+ lastTrack.Completed -= onTrackCompleted;
+
CurrentTrack = queuedTrack;
// At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now.
@@ -344,16 +346,12 @@ namespace osu.Game.Overlays
// Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback.
// Can lead to leaks.
var queuedTrack = new DrawableTrack(current.LoadTrack());
- queuedTrack.Completed += () => onTrackCompleted(current);
+ queuedTrack.Completed += onTrackCompleted;
return queuedTrack;
}
- private void onTrackCompleted(WorkingBeatmap workingBeatmap)
+ private void onTrackCompleted()
{
- // the source of track completion is the audio thread, so the beatmap may have changed before firing.
- if (current != workingBeatmap)
- return;
-
if (!CurrentTrack.Looping && !beatmap.Disabled)
NextTrack();
}
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index f2eefb6e4b..beebc9daaf 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -118,7 +118,7 @@ namespace osu.Game.Overlays
private void updateProcessingMode()
{
- bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible;
+ bool enabled = OverlayActivationMode.Value != OverlayActivation.Disabled || State.Value == Visibility.Visible;
notificationsEnabler?.Cancel();
@@ -206,8 +206,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
-
this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index e3e3b4bd80..15eefb2d9f 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -229,8 +229,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
-
this.FadeIn(transition_length, Easing.OutQuint);
dragContainer.ScaleTo(1, transition_length, Easing.OutElastic);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
index 1755c12f94..fc354027c1 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
- public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" });
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" });
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index 1681187f82..d7f39a9d8f 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -163,8 +163,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
-
ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
index 1c0ece28fe..68d6b7ced5 100644
--- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
@@ -3,11 +3,13 @@
using System.Diagnostics;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Screens;
@@ -45,6 +47,12 @@ namespace osu.Game.Overlays.SkinEditor
RelativeSizeAxes = Axes.Both;
}
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
+ }
+
public bool OnPressed(KeyBindingPressEvent e)
{
switch (e.Action)
@@ -62,6 +70,8 @@ namespace osu.Game.Overlays.SkinEditor
protected override void PopIn()
{
+ globallyDisableBeatmapSkinSetting();
+
if (skinEditor != null)
{
skinEditor.Show();
@@ -87,7 +97,13 @@ namespace osu.Game.Overlays.SkinEditor
});
}
- protected override void PopOut() => skinEditor?.Hide();
+ protected override void PopOut()
+ {
+ skinEditor?.Save(false);
+ skinEditor?.Hide();
+
+ globallyReenableBeatmapSkinSetting();
+ }
protected override void Update()
{
@@ -151,8 +167,6 @@ namespace osu.Game.Overlays.SkinEditor
if (skinEditor == null) return;
- skinEditor.Save(userTriggered: false);
-
// ensure the toolbar is re-hidden even if a new screen decides to try and show it.
updateComponentVisibility();
@@ -182,5 +196,25 @@ namespace osu.Game.Overlays.SkinEditor
skinEditor = null;
}
}
+
+ private readonly Bindable beatmapSkins = new Bindable();
+ private LeasedBindable? leasedBeatmapSkins;
+
+ private void globallyDisableBeatmapSkinSetting()
+ {
+ if (beatmapSkins.Disabled)
+ return;
+
+ // The skin editor doesn't work well if beatmap skins are being applied to the player screen.
+ // To keep things simple, disable the setting game-wide while using the skin editor.
+ leasedBeatmapSkins = beatmapSkins.BeginLease(true);
+ leasedBeatmapSkins.Value = false;
+ }
+
+ private void globallyReenableBeatmapSkinSetting()
+ {
+ leasedBeatmapSkins?.Return();
+ leasedBeatmapSkins = null;
+ }
}
}
diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs
index 00474cc0d8..34fbec93b7 100644
--- a/osu.Game/Overlays/WaveOverlayContainer.cs
+++ b/osu.Game/Overlays/WaveOverlayContainer.cs
@@ -34,8 +34,6 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
- base.PopIn();
-
Waves.Show();
this.FadeIn(100, Easing.OutQuint);
}
diff --git a/osu.Game/Rulesets/Mods/ModSynesthesia.cs b/osu.Game/Rulesets/Mods/ModSynesthesia.cs
new file mode 100644
index 0000000000..23cb135c50
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModSynesthesia.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Rulesets.Mods
+{
+ ///
+ /// Mod that colours hitobjects based on the musical division they are on
+ ///
+ public class ModSynesthesia : Mod
+ {
+ public override string Name => "Synesthesia";
+ public override string Acronym => "SY";
+ public override LocalisableString Description => "Colours hit objects based on the rhythm.";
+ public override double ScoreMultiplier => 1;
+ public override ModType Type => ModType.Fun;
+ }
+}
diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
index 09b5f0a6bc..b16c307206 100644
--- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
@@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring
///
protected int MaxHits { get; private set; }
+ ///
+ /// Whether is currently running.
+ ///
+ protected bool IsSimulating { get; private set; }
+
///
/// The total number of judged s at the current point in time.
///
@@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring
/// The to simulate.
protected virtual void SimulateAutoplay(IBeatmap beatmap)
{
+ IsSimulating = true;
+
foreach (var obj in beatmap.HitObjects)
simulate(obj);
@@ -163,6 +170,8 @@ namespace osu.Game.Rulesets.Scoring
result.Type = GetSimulatedHitResult(judgement);
ApplyResult(result);
}
+
+ IsSimulating = false;
}
protected override void Update()
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index ac17de32d8..35a7dfe369 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -30,6 +30,14 @@ namespace osu.Game.Rulesets.Scoring
private const double accuracy_cutoff_c = 0.7;
private const double accuracy_cutoff_d = 0;
+ ///
+ /// Whether should be populated during application of results.
+ ///
+ ///
+ /// Should only be disabled for special cases.
+ /// When disabled, cannot be used.
+ internal bool TrackHitEvents = true;
+
///
/// Invoked when this was reset from a replay frame.
///
@@ -226,10 +234,16 @@ namespace osu.Game.Rulesets.Scoring
ApplyScoreChange(result);
- hitEvents.Add(CreateHitEvent(result));
- lastHitObject = result.HitObject;
+ if (!IsSimulating)
+ {
+ if (TrackHitEvents)
+ {
+ hitEvents.Add(CreateHitEvent(result));
+ lastHitObject = result.HitObject;
+ }
- updateScore();
+ updateScore();
+ }
}
///
@@ -242,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring
protected sealed override void RevertResultInternal(JudgementResult result)
{
+ if (!TrackHitEvents)
+ throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled.");
+
Combo.Value = result.ComboAtJudgement;
HighestCombo.Value = result.HighestComboAtJudgement;
@@ -311,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring
/// Whether to store the current state of the for future use.
protected override void Reset(bool storeResults)
{
+ // Run one last time to store max values.
+ updateScore();
+
base.Reset(storeResults);
hitEvents.Clear();
diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs
index 1c24cfbc85..16658a598a 100644
--- a/osu.Game/Scoring/ScoreImporter.cs
+++ b/osu.Game/Scoring/ScoreImporter.cs
@@ -83,6 +83,11 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.MaximumStatisticsJson))
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
+
+ // for pre-ScoreV2 lazer scores, apply a best-effort conversion of total score to ScoreV2.
+ // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
+ if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
+ model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
}
///
diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs
index 7979ca8aaa..85598076d6 100644
--- a/osu.Game/Scoring/ScoreInfoExtensions.cs
+++ b/osu.Game/Scoring/ScoreInfoExtensions.cs
@@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring
{
@@ -13,5 +14,23 @@ namespace osu.Game.Scoring
/// A user-presentable display title representing this score.
///
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}";
+
+ ///
+ /// Orders an array of s by total score.
+ ///
+ /// The array of s to reorder.
+ /// The given ordered by decreasing total score.
+ public static IEnumerable OrderByTotalScore(this IEnumerable scores)
+ => scores.OrderByDescending(s => s.TotalScore)
+ .ThenBy(s => s.OnlineID)
+ // Local scores may not have an online ID. Fall back to date in these cases.
+ .ThenBy(s => s.Date);
+
+ ///
+ /// Retrieves the maximum achievable combo for the provided score.
+ ///
+ /// The to compute the maximum achievable combo for.
+ /// The maximum achievable combo.
+ public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
}
}
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index d5509538fd..55bcb9f79d 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -69,17 +69,6 @@ namespace osu.Game.Scoring
return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach());
}
- ///
- /// Orders an array of s by total score.
- ///
- /// The array of s to reorder.
- /// The given ordered by decreasing total score.
- public IEnumerable OrderByTotalScore(IEnumerable scores)
- => scores.OrderByDescending(s => s.TotalScore)
- .ThenBy(s => s.OnlineID)
- // Local scores may not have an online ID. Fall back to date in these cases.
- .ThenBy(s => s.Date);
-
///
/// Retrieves a bindable that represents the total score of a .
///
@@ -100,13 +89,6 @@ namespace osu.Game.Scoring
/// The bindable containing the formatted total score string.
public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
- ///
- /// Retrieves the maximum achievable combo for the provided score.
- ///
- /// The to compute the maximum achievable combo for.
- /// The maximum achievable combo.
- public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
-
///
/// Provides the total score of a . Responds to changes in the currently-selected .
///
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index 0d9b39f099..d9554c10e2 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Screens.Backgrounds
if (nextBackground == background)
return false;
- Logger.Log("🌅 Background change queued");
+ Logger.Log(@"🌅 Global background change queued");
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
@@ -94,6 +94,7 @@ namespace osu.Game.Screens.Backgrounds
nextTask?.Cancel();
nextTask = Scheduler.AddDelayed(() =>
{
+ Logger.Log(@"🌅 Global background loading");
LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token);
}, 500);
diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index 72c299f443..431336aa60 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -76,6 +76,9 @@ namespace osu.Game.Screens.Edit.Components
protected override bool OnKeyDown(KeyDownEvent e)
{
+ if (e.Repeat)
+ return false;
+
switch (e.Key)
{
case Key.Space:
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index d6e4e1f030..602ed6f627 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -18,6 +18,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid
{
+ [Resolved]
+ private EditorClock editorClock { get; set; }
+
protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
: base(referenceObject, startPosition, startTime, endTime)
{
@@ -62,14 +65,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
for (int i = 0; i < requiredCircles; i++)
{
- float diameter = (offset + (i + 1) * DistanceBetweenTicks) * 2;
+ const float thickness = 4;
+ float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2;
AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i))
{
Position = StartPosition,
Origin = Anchor.Centre,
Size = new Vector2(diameter),
- InnerRadius = 4 * 1f / diameter,
+ InnerRadius = thickness * 1f / diameter,
});
}
}
@@ -98,9 +102,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (travelLength < DistanceBetweenTicks)
travelLength = DistanceBetweenTicks;
- // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
- // to allow for snapping at a non-multiplied ratio.
- float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier);
+ float snappedDistance = LimitedDistanceSnap.Value
+ ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime())
+ // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
+ // to allow for snapping at a non-multiplied ratio.
+ : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier);
+
double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
if (snappedTime > LatestEndTime)
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 6092ebc08f..8aa2fa9f45 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -60,6 +61,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
+ ///
+ /// When enabled, distance snap should only snap to the current time (as per the editor clock).
+ /// This is to emulate stable behaviour.
+ ///
+ protected Bindable LimitedDistanceSnap { get; private set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap);
+ }
+
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
protected readonly HitObject ReferenceObject;
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index bb052b1d22..41c11ce5c1 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit
private Bindable editorBackgroundDim;
private Bindable editorHitMarkers;
private Bindable editorAutoSeekOnPlacement;
+ private Bindable editorLimitedDistanceSnap;
public Editor(EditorLoader loader = null)
{
@@ -276,6 +277,7 @@ namespace osu.Game.Screens.Edit
editorBackgroundDim = config.GetBindable(OsuSetting.EditorDim);
editorHitMarkers = config.GetBindable(OsuSetting.EditorShowHitMarkers);
editorAutoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement);
+ editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap);
AddInternal(new OsuContextMenuContainer
{
@@ -337,6 +339,10 @@ namespace osu.Game.Screens.Edit
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
{
State = { BindTarget = editorAutoSeekOnPlacement },
+ },
+ new ToggleMenuItem(EditorStrings.LimitedDistanceSnap)
+ {
+ State = { BindTarget = editorLimitedDistanceSnap },
}
}
},
@@ -533,6 +539,9 @@ namespace osu.Game.Screens.Edit
// Track traversal keys.
// Matching osu-stable implementations.
case Key.Z:
+ if (e.Repeat)
+ return false;
+
// Seek to first object time, or track start if already there.
double? firstObjectTime = editorBeatmap.HitObjects.FirstOrDefault()?.StartTime;
@@ -543,12 +552,18 @@ namespace osu.Game.Screens.Edit
return true;
case Key.X:
+ if (e.Repeat)
+ return false;
+
// Restart playback from beginning of track.
clock.Seek(0);
clock.Start();
return true;
case Key.C:
+ if (e.Repeat)
+ return false;
+
// Pause or resume.
if (clock.IsRunning)
clock.Stop();
@@ -557,6 +572,9 @@ namespace osu.Game.Screens.Edit
return true;
case Key.V:
+ if (e.Repeat)
+ return false;
+
// Seek to last object time, or track end if already there.
// Note that in osu-stable subsequent presses when at track end won't return to last object.
// This has intentionally been changed to make it more useful.
diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
index 6313d907a5..4d5d724089 100644
--- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Game.Overlays;
using System.Collections.Generic;
@@ -34,11 +32,12 @@ namespace osu.Game.Screens.OnlinePlay
protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true);
- protected override IEnumerable CreateFooterButtons() => base.CreateFooterButtons().Prepend(
- new SelectAllModsButton(this)
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- });
+ protected override IEnumerable CreateFooterButtons()
+ => base.CreateFooterButtons()
+ .Prepend(SelectAllModsButton = new SelectAllModsButton(this)
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ });
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
index 8c85a8235c..ef06d21655 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
@@ -103,118 +103,129 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.Background5,
+ Width = 0.2f,
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)),
+ Width = 0.8f,
+ },
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
- new Dimension(GridSizeMode.Relative, 0.2f)
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
- new Box
+ new Container
{
+ Name = @"Left details",
RelativeSizeAxes = Axes.Both,
- Colour = colours.Background5,
- },
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
- },
- }
- }
- },
- new Container
- {
- Name = @"Left details",
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Left = 20,
- Vertical = 5
- },
- Children = new Drawable[]
- {
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- new FillFlowContainer
+ Padding = new MarginPadding
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(5),
- Children = new Drawable[]
- {
- new RoomStatusPill
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft
- },
- specialCategoryPill = new RoomSpecialCategoryPill
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft
- },
- endDateInfo = new EndDateInfo
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- }
+ Left = 20,
+ Right = DrawableRoomParticipantsList.SHEAR_WIDTH,
+ Vertical = 5
},
- new FillFlowContainer
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Top = 3 },
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new FillFlowContainer
{
- new RoomNameText(),
- new RoomStatusText()
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ new RoomStatusPill
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft
+ },
+ specialCategoryPill = new RoomSpecialCategoryPill
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft
+ },
+ endDateInfo = new EndDateInfo
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ }
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Top = 3 },
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new TruncatingSpriteText
+ {
+ RelativeSizeAxes = Axes.X,
+ Font = OsuFont.GetFont(size: 28),
+ Current = { BindTarget = Room.Name }
+ },
+ new RoomStatusText()
+ }
+ }
+ },
+ },
+ new FillFlowContainer
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5),
+ ChildrenEnumerable = CreateBottomDetails()
+ }
+ }
+ },
+ new FillFlowContainer
+ {
+ Name = "Right content",
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ Spacing = new Vector2(5),
+ Padding = new MarginPadding
+ {
+ Right = 10,
+ Vertical = 20,
+ },
+ Children = new Drawable[]
+ {
+ ButtonsContainer,
+ drawableRoomParticipantsList = new DrawableRoomParticipantsList
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ NumberOfCircles = NumberOfAvatars
}
}
},
- },
- new FillFlowContainer
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(5),
- ChildrenEnumerable = CreateBottomDetails()
- }
- }
- },
- new FillFlowContainer
- {
- Name = "Right content",
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- AutoSizeAxes = Axes.X,
- RelativeSizeAxes = Axes.Y,
- Spacing = new Vector2(5),
- Padding = new MarginPadding
- {
- Right = 10,
- Vertical = 20,
- },
- Children = new Drawable[]
- {
- ButtonsContainer,
- drawableRoomParticipantsList = new DrawableRoomParticipantsList
- {
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- NumberOfCircles = NumberOfAvatars
}
}
},
@@ -311,23 +322,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return pills;
}
- private partial class RoomNameText : OsuSpriteText
- {
- [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
- private Bindable name { get; set; }
-
- public RoomNameText()
- {
- Font = OsuFont.GetFont(size: 28);
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Current = name;
- }
- }
-
private partial class RoomStatusText : OnlinePlayComposite
{
[Resolved]
@@ -343,7 +337,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Width = 0.5f;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs
index c31633eefc..06f9f35479 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs
@@ -24,8 +24,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public partial class DrawableRoomParticipantsList : OnlinePlayComposite
{
+ public const float SHEAR_WIDTH = 12f;
+
private const float avatar_size = 36;
+ private const float height = 60f;
+
+ private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0);
+
private FillFlowContainer avatarFlow;
private CircularAvatar hostAvatar;
@@ -36,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public DrawableRoomParticipantsList()
{
AutoSizeAxes = Axes.X;
- Height = 60;
+ Height = height;
}
[BackgroundDependencyLoader]
@@ -49,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
- Shear = new Vector2(0.2f, 0),
+ Shear = shear,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
@@ -98,7 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
- Shear = new Vector2(0.2f, 0),
+ Shear = shear,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs
index 4d4fe4ea56..05232fe0e2 100644
--- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs
@@ -54,14 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override void PopIn()
{
- base.PopIn();
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);
Settings.FadeIn(TRANSITION_DURATION / 2);
}
protected override void PopOut()
{
- base.PopOut();
Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine);
Settings.Delay(TRANSITION_DURATION / 2).FadeOut(TRANSITION_DURATION / 2);
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index a36c7e801e..978d77b4f1 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -49,6 +49,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; }
+ [Resolved(canBeNull: true)]
+ private OsuGame game { get; set; }
+
private AddItemButton addItemButton;
public MultiplayerMatchSubScreen(Room room)
@@ -334,11 +337,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
updateCurrentItem();
- addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0;
+ addItemButton.Alpha = localUserCanAddItem ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
}
+ private bool localUserCanAddItem => client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly;
+
private void updateCurrentItem()
{
Debug.Assert(client.Room != null);
@@ -403,18 +408,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!this.IsCurrentScreen())
return;
- if (client.Room == null)
+ if (!localUserCanAddItem)
return;
- if (!client.IsHost)
- {
- // todo: should handle this when the request queue is implemented.
- // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap
- // flow may need to change to support an "unable to present" return value.
- return;
- }
+ // If there's only one playlist item and we are the host, assume we want to change it. Else add a new one.
+ PlaylistItem itemToEdit = client.IsHost && Room.Playlist.Count == 1 ? Room.Playlist.Single() : null;
- this.Push(new MultiplayerMatchSongSelect(Room, Room.Playlist.Single(item => item.ID == client.Room.Settings.PlaylistItemId)));
+ OpenSongSelection(itemToEdit);
+
+ // Re-run PresentBeatmap now that we've pushed a song select that can handle it.
+ game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
index d40d43cd54..aa72394ac9 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
@@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// An optional pivot around which the scores were retrieved.
private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() =>
{
- var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
+ var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
index 9dce8996c3..be2ce3b272 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
@@ -95,7 +95,6 @@ namespace osu.Game.Screens.Play.HUD
private void updateGraphVisibility()
{
graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In);
- bar.ShowBackground = !ShowGraph.Value;
}
protected override void Update()
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
index dd6e10ba5d..beaee0e9ee 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
@@ -14,7 +14,6 @@ using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
@@ -32,18 +31,8 @@ namespace osu.Game.Screens.Play.HUD
private readonly Box background;
- private readonly BindableBool showBackground = new BindableBool();
-
private readonly ColourInfo mainColour;
- private readonly ColourInfo mainColourDarkened;
private ColourInfo catchUpColour;
- private ColourInfo catchUpColourDarkened;
-
- public bool ShowBackground
- {
- get => showBackground.Value;
- set => showBackground.Value = value;
- }
public double StartTime
{
@@ -95,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
- Colour = Colour4.White.Darken(1 + 1 / 4f)
+ Colour = OsuColour.Gray(0.2f),
},
catchupBar = new RoundedBar
{
@@ -112,12 +101,10 @@ namespace osu.Game.Screens.Play.HUD
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
- AccentColour = mainColour = Color4.White,
+ AccentColour = mainColour = OsuColour.Gray(0.9f),
RelativeSizeAxes = Axes.Both
},
};
-
- mainColourDarkened = Colour4.White.Darken(1 / 3f);
}
private void setupAlternateValue()
@@ -141,16 +128,15 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- catchUpColour = colours.BlueLight;
- catchUpColourDarkened = colours.BlueDark;
-
- showBackground.BindValueChanged(_ => updateBackground(), true);
+ catchUpColour = colours.BlueDark;
}
- private void updateBackground()
+ protected override void LoadComplete()
{
- background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In);
- playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In);
+ base.LoadComplete();
+
+ background.FadeTo(0.3f, 200, Easing.In);
+ playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), mainColour, 200, Easing.In);
}
protected override bool OnHover(HoverEvent e)
@@ -190,8 +176,8 @@ namespace osu.Game.Screens.Play.HUD
catchupBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold),
- ShowBackground ? mainColour : mainColourDarkened,
- ShowBackground ? catchUpColour : catchUpColourDarkened,
+ mainColour,
+ catchUpColour,
0, colour_transition_threshold,
Easing.OutQuint);
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs
index 63ab9d15e0..be570c1578 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs
@@ -4,8 +4,10 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
+using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface;
@@ -13,6 +15,10 @@ namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressGraph : SegmentedGraph
{
+ private const int tier_count = 5;
+
+ private const int display_granularity = 200;
+
private IEnumerable? objects;
public IEnumerable Objects
@@ -21,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD
{
objects = value;
- const int granularity = 200;
- int[] values = new int[granularity];
+ int[] values = new int[display_granularity];
if (!objects.Any())
return;
@@ -32,7 +37,7 @@ namespace osu.Game.Screens.Play.HUD
if (lastHit == 0)
lastHit = objects.Last().StartTime;
- double interval = (lastHit - firstHit + 1) / granularity;
+ double interval = (lastHit - firstHit + 1) / display_granularity;
foreach (var h in objects)
{
@@ -51,12 +56,12 @@ namespace osu.Game.Screens.Play.HUD
}
public ArgonSongProgressGraph()
- : base(5)
+ : base(tier_count)
{
var colours = new List();
- for (int i = 0; i < 5; i++)
- colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f));
+ for (int i = 0; i < tier_count; i++)
+ colours.Add(OsuColour.Gray(0.2f).Opacity(0.1f));
TierColours = colours;
}
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 4ac2f1afda..dcb2c1071e 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -235,7 +235,7 @@ namespace osu.Game.Screens.Play.HUD
}
}
},
- usernameText = new OsuSpriteText
+ usernameText = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
@@ -244,7 +244,6 @@ namespace osu.Game.Screens.Play.HUD
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username ?? string.Empty,
- Truncate = true,
Shadow = false,
}
}
diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs
index 20d2130e76..0a2696339c 100644
--- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs
+++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs
@@ -8,16 +8,26 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Scoring;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Online;
+using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.Play
{
- public partial class SaveFailedScoreButton : CompositeDrawable
+ public partial class SaveFailedScoreButton : CompositeDrawable, IKeyBindingHandler
{
+ [Resolved]
+ private RealmAccess realm { get; set; } = null!;
+
+ [Resolved]
+ private ScoreManager scoreManager { get; set; } = null!;
+
private readonly Bindable state = new Bindable();
private readonly Func> importFailedScore;
@@ -34,7 +44,7 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
- private void load(OsuGame? game, Player? player, RealmAccess realm)
+ private void load(OsuGame? game, Player? player)
{
InternalChild = button = new DownloadButton
{
@@ -54,7 +64,7 @@ namespace osu.Game.Screens.Play
{
importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach());
Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded);
- });
+ }).FireAndForget();
break;
}
}
@@ -87,5 +97,43 @@ namespace osu.Game.Screens.Play
}
}, true);
}
+
+ #region Export via hotkey logic (also in ReplayDownloadButton)
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ switch (e.Action)
+ {
+ case GlobalAction.SaveReplay:
+ button.TriggerClick();
+ return true;
+
+ case GlobalAction.ExportReplay:
+ state.BindValueChanged(exportWhenReady, true);
+
+ // start the import via button
+ if (state.Value != DownloadState.LocallyAvailable)
+ button.TriggerClick();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ private void exportWhenReady(ValueChangedEvent state)
+ {
+ if (state.NewValue != DownloadState.LocallyAvailable) return;
+
+ scoreManager.Export(importedScore);
+
+ this.state.ValueChanged -= exportWhenReady;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index f23b469f5c..82c429798e 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Screens.Ranking.Expanded
var topStatistics = new List
{
new AccuracyStatistic(score.Accuracy),
- new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)),
+ new ComboStatistic(score.MaxCombo, score.GetMaximumAchievableCombo()),
new PerformanceStatistic(score),
};
@@ -101,23 +101,21 @@ namespace osu.Game.Screens.Ranking.Expanded
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
- Truncate = true,
},
- new OsuSpriteText
+ new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
- Truncate = true,
},
new Container
{
@@ -156,14 +154,13 @@ namespace osu.Game.Screens.Ranking.Expanded
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = beatmap.DifficultyName,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
- Truncate = true,
},
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
{
diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs
index 5c5cb61b79..799041b7de 100644
--- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs
+++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs
@@ -1,30 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Online;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.Ranking
{
- public partial class ReplayDownloadButton : CompositeDrawable
+ public partial class ReplayDownloadButton : CompositeDrawable, IKeyBindingHandler
{
public readonly Bindable Score = new Bindable();
protected readonly Bindable State = new Bindable();
- private DownloadButton button;
- private ShakeContainer shakeContainer;
+ private DownloadButton button = null!;
+ private ShakeContainer shakeContainer = null!;
- private ScoreDownloadTracker downloadTracker;
+ private ScoreDownloadTracker? downloadTracker;
+
+ [Resolved]
+ private ScoreManager scoreManager { get; set; } = null!;
private ReplayAvailability replayAvailability
{
@@ -46,8 +50,8 @@ namespace osu.Game.Screens.Ranking
Size = new Vector2(50, 30);
}
- [BackgroundDependencyLoader(true)]
- private void load(OsuGame game, ScoreModelDownloader scores)
+ [BackgroundDependencyLoader]
+ private void load(OsuGame? game, ScoreModelDownloader scoreDownloader)
{
InternalChild = shakeContainer = new ShakeContainer
{
@@ -67,7 +71,7 @@ namespace osu.Game.Screens.Ranking
break;
case DownloadState.NotDownloaded:
- scores.Download(Score.Value);
+ scoreDownloader.Download(Score.Value);
break;
case DownloadState.Importing:
@@ -99,6 +103,44 @@ namespace osu.Game.Screens.Ranking
}, true);
}
+ #region Export via hotkey logic (also in SaveFailedScoreButton)
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ switch (e.Action)
+ {
+ case GlobalAction.SaveReplay:
+ button.TriggerClick();
+ return true;
+
+ case GlobalAction.ExportReplay:
+ State.BindValueChanged(exportWhenReady, true);
+
+ // start the import via button
+ if (State.Value != DownloadState.LocallyAvailable)
+ button.TriggerClick();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ private void exportWhenReady(ValueChangedEvent state)
+ {
+ if (state.NewValue != DownloadState.LocallyAvailable) return;
+
+ scoreManager.Export(Score.Value);
+
+ State.ValueChanged -= exportWhenReady;
+ }
+
+ #endregion
+
private void updateState()
{
switch (replayAvailability)
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 78239e0dbe..b9f3b65129 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -160,7 +160,7 @@ namespace osu.Game.Screens.Ranking
if (allowWatchingReplay)
{
- buttons.Add(new ReplayDownloadButton(null)
+ buttons.Add(new ReplayDownloadButton(SelectedScore.Value)
{
Score = { BindTarget = SelectedScore },
Width = 300
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 6ba9843f7b..3d87a57295 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -155,7 +155,7 @@ namespace osu.Game.Screens.Select
public Bindable RandomAlgorithm = new Bindable();
private readonly List previouslyVisitedRandomSets = new List();
- private readonly Stack randomSelectedBeatmaps = new Stack();
+ private readonly List randomSelectedBeatmaps = new List();
private CarouselRoot root;
@@ -348,6 +348,11 @@ namespace osu.Game.Screens.Select
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
return;
+ foreach (var beatmap in existingSet.Beatmaps)
+ randomSelectedBeatmaps.Remove(beatmap);
+
+ previouslyVisitedRandomSets.Remove(existingSet);
+
root.RemoveItem(existingSet);
itemsCache.Invalidate();
@@ -501,7 +506,7 @@ namespace osu.Game.Screens.Select
if (selectedBeatmap != null && selectedBeatmapSet != null)
{
- randomSelectedBeatmaps.Push(selectedBeatmap);
+ randomSelectedBeatmaps.Add(selectedBeatmap);
// when performing a random, we want to add the current set to the previously visited list
// else the user may be "randomised" to the existing selection.
@@ -538,9 +543,10 @@ namespace osu.Game.Screens.Select
{
while (randomSelectedBeatmaps.Any())
{
- var beatmap = randomSelectedBeatmaps.Pop();
+ var beatmap = randomSelectedBeatmaps[^1];
+ randomSelectedBeatmaps.Remove(beatmap);
- if (!beatmap.Filtered.Value)
+ if (!beatmap.Filtered.Value && beatmap.BeatmapInfo.BeatmapSet?.DeletePending != true)
{
if (selectedBeatmapSet != null)
{
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 2102df1022..961f8684ce 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -233,12 +233,11 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
- VersionLabel = new OsuSpriteText
+ VersionLabel = new TruncatingSpriteText
{
Text = beatmapInfo.DifficultyName,
Font = OsuFont.GetFont(size: 24, italics: true),
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
}
},
@@ -286,19 +285,17 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
- TitleLabel = new OsuSpriteText
+ TitleLabel = new TruncatingSpriteText
{
Current = { BindTarget = titleBinding },
Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
- ArtistLabel = new OsuSpriteText
+ ArtistLabel = new TruncatingSpriteText
{
Current = { BindTarget = artistBinding },
Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
MapperContainer = new FillFlowContainer
{
diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
index a57a8b0f27..7c632b63db 100644
--- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
+++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
@@ -29,9 +29,6 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private RealmAccess realm { get; set; } = null!;
- [Resolved]
- private ScoreManager scoreManager { get; set; } = null!;
-
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -78,7 +75,7 @@ namespace osu.Game.Screens.Select.Carousel
if (changes?.HasCollectionChanges() == false)
return;
- ScoreInfo? topScore = scoreManager.OrderByTotalScore(sender.Detach()).FirstOrDefault();
+ ScoreInfo? topScore = sender.Detach().OrderByTotalScore().FirstOrDefault();
updateable.Rank = topScore?.Rank;
updateable.Alpha = topScore != null ? 1 : 0;
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 2b40b9faf8..4c41ed3622 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -67,9 +67,6 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
- [Resolved]
- private ScoreManager scoreManager { get; set; } = null!;
-
[Resolved]
private IBindable ruleset { get; set; } = null!;
@@ -164,7 +161,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return;
SetScores(
- scoreManager.OrderByTotalScore(response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
+ response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(),
response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)
);
});
@@ -222,7 +219,7 @@ namespace osu.Game.Screens.Select.Leaderboards
scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym)));
}
- scores = scoreManager.OrderByTotalScore(scores.Detach());
+ scores = scores.Detach().OrderByTotalScore();
SetScores(scores);
}
diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs
index c92dc2e343..5753c268d9 100644
--- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs
+++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs
@@ -86,8 +86,6 @@ namespace osu.Game.Screens.Select.Options
protected override void PopIn()
{
- base.PopIn();
-
this.FadeIn(transition_duration, Easing.OutQuint);
if (buttonsContainer.Position.X == 1 || Alpha == 0)
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 4d6a5398c5..47e5325baf 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -5,7 +5,6 @@ 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;
@@ -864,7 +863,7 @@ namespace osu.Game.Screens.Select
{
// 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")}";
+ FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed:#,0} matches" : $"{Carousel.CountDisplayed:#,0} match";
}
private bool boundLocalBindables;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b2faa7dfc2..9cb20ee364 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -1,4 +1,4 @@
-
+
net6.0
Library
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 9aafec6c50..256d1e43c4 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+