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

Merge branch 'master' into diffcalc-total-scorev1

This commit is contained in:
Dan Balasescu 2023-06-24 02:46:15 +09:00
commit 5fadadc3d0
106 changed files with 1506 additions and 505 deletions

View File

@ -15,7 +15,7 @@
] ]
}, },
"codefilesanity": { "codefilesanity": {
"version": "0.0.36", "version": "0.0.37",
"commands": [ "commands": [
"CodeFileSanity" "CodeFileSanity"
] ]

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger> <AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.608.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.620.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" /> <AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32; using Microsoft.Win32;
using osu.Desktop.Security; using osu.Desktop.Security;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -17,7 +15,6 @@ using osu.Framework;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Desktop.Windows; using osu.Desktop.Windows;
using osu.Framework.Threading;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Utils; using osu.Game.Utils;
@ -138,52 +135,10 @@ namespace osu.Desktop
desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name; 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(); protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
private readonly List<string> importableFiles = new List<string>();
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) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
/// <summary> /// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary> /// </summary>
public partial class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter public partial class LegacyCatchComboCounter : UprightAspectMaintainingContainer, ICatchComboCounter
{ {
private readonly LegacyRollingCounter counter; private readonly LegacyRollingCounter counter;

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
@ -25,22 +26,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition(2) new StageDefinition(2)
}; };
SetContents(_ => new ManiaPlayfield(stageDefinitions)); SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, 2)
{
Child = new ManiaPlayfield(stageDefinitions)
});
}); });
} }
[Test] [TestCase(2)]
public void TestDualStages() [TestCase(3)]
[TestCase(5)]
public void TestDualStages(int columnCount)
{ {
AddStep("create stage", () => AddStep("create stage", () =>
{ {
stageDefinitions = new List<StageDefinition> stageDefinitions = new List<StageDefinition>
{ {
new StageDefinition(2), new StageDefinition(columnCount),
new StageDefinition(2) 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)
}
});
}); });
} }

View File

@ -119,14 +119,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
yield return obj; yield return obj;
} }
private readonly List<double> prevNoteTimes = new List<double>(max_notes_for_density); private readonly LimitedCapacityQueue<double> prevNoteTimes = new LimitedCapacityQueue<double>(max_notes_for_density);
private double density = int.MaxValue; private double density = int.MaxValue;
private void computeDensity(double newNoteTime) private void computeDensity(double newNoteTime)
{ {
if (prevNoteTimes.Count == max_notes_for_density) prevNoteTimes.Enqueue(newNoteTime);
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
if (prevNoteTimes.Count >= 2) if (prevNoteTimes.Count >= 2)
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;

View File

@ -242,18 +242,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.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: if (Time.Current >= HitObject.StartTime)
// 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)
{ {
// How far past the hit target this hold note is. // As the note is being held, adjust the size of the sizing container. This has two effects:
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; // 1. The contained masking container will mask the body and ticks.
sizingContainer.Height = 1 - yOffset / DrawHeight; // 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) protected override void CheckForResult(bool userTriggered, double timeOffset)

View File

@ -35,10 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
var stage = beatmap.GetStageForColumnIndex(column); var stage = beatmap.GetStageForColumnIndex(column);
if (stage.IsSpecialColumn(column)) int columnInStage = column % stage.Columns;
if (stage.IsSpecialColumn(columnInStage))
return SkinUtils.As<TValue>(new Bindable<Color4>(colourSpecial)); return SkinUtils.As<TValue>(new Bindable<Color4>(colourSpecial));
int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column); int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage);
return SkinUtils.As<TValue>(new Bindable<Color4>(distanceToEdge % 2 == 0 ? colourOdd : colourEven)); return SkinUtils.As<TValue>(new Bindable<Color4>(distanceToEdge % 2 == 0 ? colourOdd : colourEven));
} }
} }

View File

@ -185,7 +185,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("Ensure cursor is on a grid line", () => AddAssert("Ensure cursor is on a grid line", () =>
{ {
return grid.ChildrenOfType<CircularProgress>().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X)); return grid.ChildrenOfType<CircularProgress>().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);
});
}); });
} }

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.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
{
/// <summary>
/// Mod that colours <see cref="HitObject"/>s based on the musical division they are on
/// </summary>
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;
};
}
}
}

View File

@ -204,7 +204,8 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed(), new ModAdaptiveSpeed(),
new OsuModFreezeFrame(), new OsuModFreezeFrame(),
new OsuModBubbles() new OsuModBubbles(),
new OsuModSynesthesia()
}; };
case ModType.System: case ModType.System:

View File

@ -7,12 +7,15 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -21,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private GameplayClockContainer gameplayClockContainer = null!; private GameplayClockContainer gameplayClockContainer = null!;
private Box background = null!;
private const double skip_target_time = -2000; private const double skip_target_time = -2000;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -30,11 +35,20 @@ namespace osu.Game.Tests.Visual.Gameplay
FrameStabilityContainer frameStabilityContainer; 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); 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); 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<ArgonSongProgress> action) => private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
this.ChildrenOfType<ArgonSongProgress>().ForEach(action); this.ChildrenOfType<ArgonSongProgress>().ForEach(action);

View File

@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -57,11 +58,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0); AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
} }
[Test]
public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged()
{
createFreeModSelect();
AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea");
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddStep("click select all button", navigateAndClick<SelectAllModsButton>);
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e");
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
void navigateAndClick<T>() where T : Drawable
{
InputManager.MoveMouseTo(this.ChildrenOfType<T>().Single());
InputManager.Click(MouseButton.Left);
}
}
[Test] [Test]
public void TestSelectDeselectAllViaKeyboard() public void TestSelectDeselectAllViaKeyboard()
{ {
createFreeModSelect(); createFreeModSelect();
AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus());
AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll)); AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll));
AddUntilStep("all mods selected", assertAllAvailableModsSelected); AddUntilStep("all mods selected", assertAllAvailableModsSelected);

View File

@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) 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 {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) }); 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. // A previous test's mod overlay could still be fading out.
AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1); AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1);
assertHasFreeModButton(allowedMod, false); assertFreeModNotShown(allowedMod);
assertHasFreeModButton(requiredMod, false); 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<FreeModSelectOverlay>() () => this.ChildrenOfType<FreeModSelectOverlay>()
.Single() .Single()
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.Where(panel => !panel.Filtered.Value) .Where(panel => panel.Visible)
.All(b => b.Mod.GetType() != type)); .All(b => b.Mod.GetType() != type));
} }

View File

@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contains only double time mod", AddUntilStep("mod select contains only double time mod",
() => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime);
} }
[Test] [Test]

View File

@ -86,6 +86,7 @@ namespace osu.Game.Tests.Visual.Online
StarRating = 9.99, StarRating = 9.99,
DifficultyName = @"TEST", DifficultyName = @"TEST",
Length = 456000, Length = 456000,
HitLength = 400000,
RulesetID = 3, RulesetID = 3,
CircleSize = 1, CircleSize = 1,
DrainRate = 2.3f, DrainRate = 2.3f,

View File

@ -453,6 +453,25 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); 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);
}
/// <summary> /// <summary>
/// Test adding and removing beatmap sets /// Test adding and removing beatmap sets
/// </summary> /// </summary>

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
using osuTK; using osuTK;
@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private TestCommentEditor commentEditor = null!; private TestCommentEditor commentEditor = null!;
private TestCancellableCommentEditor cancellableCommentEditor = null!; private TestCancellableCommentEditor cancellableCommentEditor = null!;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -96,12 +98,43 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown); 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] [Test]
public void TestCancelAction() public void TestCancelAction()
{ {
AddStep("click cancel button", () => AddStep("click cancel button", () =>
{ {
InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[1]); InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[2]);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
@ -112,6 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public new Bindable<string> Current => base.Current; public new Bindable<string> Current => base.Current;
public new FillFlowContainer ButtonsContainer => base.ButtonsContainer; public new FillFlowContainer ButtonsContainer => base.ButtonsContainer;
public new TextBox TextBox => base.TextBox;
public string CommittedText { get; private set; } = string.Empty; 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 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 private partial class TestCancellableCommentEditor : CancellableCommentEditor
@ -146,8 +184,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
} }
protected override LocalisableString CommitButtonText => @"Save"; protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
protected override LocalisableString TextBoxPlaceholder => @"Multiline textboxes soon"; protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon";
} }
} }
} }

View File

@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2); AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => panel.Visible) == 2);
clickToggle(); clickToggle();
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => panel.Visible));
AddStep("unset filter", () => setFilter(null)); AddStep("unset filter", () => setFilter(null));
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value)); AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value); AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2); AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => panel.Visible) == 2);
AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value); AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
AddStep("filter out everything", () => setFilter(_ => false)); AddStep("filter out everything", () => setFilter(_ => false));
AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Filtered.Value)); AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Visible));
AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
AddStep("inset filter", () => setFilter(null)); AddStep("inset filter", () => setFilter(null));
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value)); AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent); AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
void clickToggle() => AddStep("click toggle", () => void clickToggle() => AddStep("click toggle", () =>
@ -288,10 +288,53 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2); AddAssert("no change", () => this.ChildrenOfType<ModPanel>().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<ModPanel>().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<ModPanel>().All(panel => panel.Visible));
}
}
private void setFilter(Func<Mod, bool>? filter) private void setFilter(Func<Mod, bool>? filter)
{ {
foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods) foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods)
modState.Filtered.Value = filter?.Invoke(modState.Mod) == false; modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false;
} }
private partial class TestModColumn : ModColumn private partial class TestModColumn : ModColumn

View File

@ -392,6 +392,28 @@ namespace osu.Game.Tests.Visual.UserInterface
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods)); new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods));
} }
[Test]
public void TestTextFiltering()
{
ModPresetColumn modPresetColumn = null!;
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
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<ModPresetPanel>().Count(panel => panel.IsPresent), () => Is.EqualTo(1));
AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3));
AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType<ModPresetPanel>().Count(panel => panel.IsPresent), () => Is.EqualTo(0));
}
private ICollection<ModPreset> createTestPresets() => new[] private ICollection<ModPreset> createTestPresets() => new[]
{ {
new ModPreset new ModPreset

View File

@ -490,15 +490,15 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen(); createScreen();
changeRuleset(0); changeRuleset(0);
AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible));
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible));
AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true);
AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible));
} }
[Test] [Test]
@ -524,7 +524,57 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
waitForColumnLoad(); 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<ShearedSearchTextBox>);
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("click on mod column", navigateAndClick<ModColumn>);
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
void navigateAndClick<T>() where T : Drawable
{
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<T>().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] [Test]
@ -533,6 +583,8 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen(); createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus());
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
@ -540,6 +592,26 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); 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<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 2);
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().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] [Test]
public void TestDeselectAllViaButton() public void TestDeselectAllViaButton()
{ {
@ -561,6 +633,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value); AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
} }
[Test]
public void TestDeselectAllViaButton_WithSearchApplied()
{
createScreen();
changeRuleset(0);
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().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<ModPanel>().Count(panel => panel.Active.Value) == 3);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 3);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
}
[Test] [Test]
public void TestCloseViaBackButton() public void TestCloseViaBackButton()
{ {
@ -580,8 +677,11 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
} }
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.IsValidMod"/>.
/// </summary>
[Test] [Test]
public void TestColumnHiding() public void TestColumnHidingOnIsValidChange()
{ {
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{ {
@ -610,6 +710,56 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3); AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
} }
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.SearchTerm"/>.
/// </summary>
[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<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "HD");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches");
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("clear search bar", () => modSelectOverlay.SearchTerm = "");
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().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<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "fail");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("hide", () => modSelectOverlay.Hide());
AddStep("show", () => modSelectOverlay.Show());
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
}
[Test] [Test]
public void TestColumnHidingOnRulesetChange() public void TestColumnHidingOnRulesetChange()
{ {
@ -688,12 +838,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public override string ShortName => "unimplemented"; public override string ShortName => "unimplemented";
public override IEnumerable<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type) =>
{ type == ModType.Conversion
if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); ? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() })
: base.GetModsFor(type);
return base.GetModsFor(type);
}
} }
} }
} }

View File

@ -101,6 +101,10 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
}; };
} }
protected override void PopIn()
{
}
} }
} }
} }

View File

@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
IBeatmapSetInfo? BeatmapSet { get; } IBeatmapSetInfo? BeatmapSet { get; }
/// <summary> /// <summary>
/// The playable length in milliseconds of this beatmap. /// The total length in milliseconds of this beatmap.
/// </summary> /// </summary>
double Length { get; } double Length { get; }

View File

@ -59,5 +59,10 @@ namespace osu.Game.Beatmaps
int PassCount { get; } int PassCount { get; }
APIFailTimes? FailTimes { get; } APIFailTimes? FailTimes { get; }
/// <summary>
/// The playable length in milliseconds of this beatmap.
/// </summary>
double HitLength { get; }
} }
} }

View File

@ -114,8 +114,6 @@ namespace osu.Game.Collections
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
this.FadeIn(enter_duration, Easing.OutQuint); this.FadeIn(enter_duration, Easing.OutQuint);
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);

View File

@ -178,6 +178,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f); SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.EditorShowHitMarkers, true);
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true); SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@ -383,5 +384,6 @@ namespace osu.Game.Configuration
SafeAreaConsiderations, SafeAreaConsiderations,
ComboColourNormalisationAmount, ComboColourNormalisationAmount,
ProfileCoverExpanded, ProfileCoverExpanded,
EditorLimitedDistanceSnap
} }
} }

View File

@ -77,8 +77,9 @@ namespace osu.Game.Database
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 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. /// 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.
/// </summary> /// </summary>
private const int schema_version = 29; private const int schema_version = 30;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -938,6 +939,7 @@ namespace osu.Game.Database
} }
case 29: case 29:
case 30:
{ {
var scores = migration.NewRealm var scores = migration.NewRealm
.All<ScoreInfo>() .All<ScoreInfo>()
@ -945,22 +947,21 @@ namespace osu.Game.Database
foreach (var score in scores) foreach (var score in scores)
{ {
// Recalculate the old-style standardised score to see if this was an old lazer score. try
bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.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);
if (oldScoreMatchesExpectations || scoreIsVeryOld)
{ {
try if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(score))
{
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
score.TotalScore = calculatedNew;
}
catch
{ {
try
{
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
score.TotalScore = calculatedNew;
}
catch
{
}
} }
} }
catch { }
} }
break; break;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,6 +14,19 @@ namespace osu.Game.Database
{ {
public static class StandardisedScoreMigrationTools 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) public static long GetNewStandardised(ScoreInfo score)
{ {
int maxJudgementIndex = 0; int maxJudgementIndex = 0;
@ -168,7 +182,7 @@ namespace osu.Game.Database
foreach (var mod in score.Mods) foreach (var mod in score.Mods)
modMultiplier *= mod.ScoreMultiplier; modMultiplier *= mod.ScoreMultiplier;
return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
} }
private class FakeHit : HitObject private class FakeHit : HitObject

View File

@ -152,7 +152,6 @@ namespace osu.Game.Graphics.Containers
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut();
previewTrackManager.StopAnyPlaying(this); previewTrackManager.StopAnyPlaying(this);
} }

View File

@ -167,9 +167,12 @@ namespace osu.Game.Graphics.UserInterface
{ {
base.Update(); 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) // 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. // we want to ignore really long periods of no processing.
if (updateClock.ElapsedFrameTime > 10000) if (elapsedUpdateFrameTime > 10000)
return; return;
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth); 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). // frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier).
bool aimRatesChanged = updateAimFPS(); 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. // 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; 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) if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. // 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 else
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed); displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed);

View File

@ -111,6 +111,10 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e) 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); hover.FlashColour(FlashColour, 800, Easing.OutQuint);
return base.OnClick(e); return base.OnClick(e);
} }

View File

@ -16,6 +16,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Localisation;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -112,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface
private partial class CapsWarning : SpriteIcon, IHasTooltip private partial class CapsWarning : SpriteIcon, IHasTooltip
{ {
public LocalisableString TooltipText => "caps lock is active"; public LocalisableString TooltipText => CommonStrings.CapsLockIsActive;
public CapsWarning() public CapsWarning()
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -37,6 +38,14 @@ namespace osu.Game.Graphics.UserInterface
set => textBox.HoldFocus = value; 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 TakeFocus() => textBox.TakeFocus();
public void KillFocus() => textBox.KillFocus(); public void KillFocus() => textBox.KillFocus();

View File

@ -119,6 +119,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus),
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
}; };
public IEnumerable<KeyBinding> ReplayKeyBindings => new[] public IEnumerable<KeyBinding> ReplayKeyBindings => new[]
@ -366,5 +368,11 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))]
EditorCycleNextBeatSnapDivisor, EditorCycleNextBeatSnapDivisor,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SaveReplay))]
SaveReplay,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))]
ExportReplay,
} }
} }

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class AccountCreationStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation";
/// <summary>
/// "New player registration"
/// </summary>
public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New player registration");
/// <summary>
/// "Let&#39;s get you started"
/// </summary>
public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"Let's get you started");
/// <summary>
/// "Let&#39;s create an account!"
/// </summary>
public static LocalisableString LetsCreateAnAccount => new TranslatableString(getKey(@"lets_create_an_account"), @"Let's create an account!");
/// <summary>
/// "Help, I can&#39;t access my account!"
/// </summary>
public static LocalisableString MultiAccountWarningHelp => new TranslatableString(getKey(@"multi_account_warning_help"), @"Help, I can't access my account!");
/// <summary>
/// "I understand. This account isn&#39;t for me."
/// </summary>
public static LocalisableString MultiAccountWarningAccept => new TranslatableString(getKey(@"multi_account_warning_accept"), @"I understand. This account isn't for me.");
/// <summary>
/// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"
/// </summary>
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!");
/// <summary>
/// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever."
/// </summary>
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.");
/// <summary>
/// " Make sure to get it right!"
/// </summary>
public static LocalisableString EmailDescription2 => new TranslatableString(getKey(@"email_description_2"), @" Make sure to get it right!");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -154,6 +154,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit"); public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit");
/// <summary>
/// "Caps lock is active"
/// </summary>
public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"Caps lock is active");
/// <summary> /// <summary>
/// "Revert to default" /// "Revert to default"
/// </summary> /// </summary>

View File

@ -109,6 +109,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation); public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
/// <summary>
/// "Limit distance snap placement to current time"
/// </summary>
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}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -324,6 +324,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus"); public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
/// <summary>
/// "Save replay"
/// </summary>
public static LocalisableString SaveReplay => new TranslatableString(getKey(@"save_replay"), @"Save replay");
/// <summary>
/// "Export replay"
/// </summary>
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class LoginPanelStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.LoginPanel";
/// <summary>
/// "Do not disturb"
/// </summary>
public static LocalisableString DoNotDisturb => new TranslatableString(getKey(@"do_not_disturb"), @"Do not disturb");
/// <summary>
/// "Appear offline"
/// </summary>
public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline");
/// <summary>
/// "Signed in"
/// </summary>
public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
/// <summary>
/// "Account"
/// </summary>
public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"Account");
/// <summary>
/// "Remember username"
/// </summary>
public static LocalisableString RememberUsername => new TranslatableString(getKey(@"remember_username"), @"Remember username");
/// <summary>
/// "Stay signed in"
/// </summary>
public static LocalisableString StaySignedIn => new TranslatableString(getKey(@"stay_signed_in"), @"Stay signed in");
/// <summary>
/// "Register"
/// </summary>
public static LocalisableString Register => new TranslatableString(getKey(@"register"), @"Register");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -39,6 +39,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods"); public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods");
/// <summary>
/// "tab to search..."
/// </summary>
public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -34,7 +34,8 @@ namespace osu.Game.Online.API
public string AccessToken => "token"; public string AccessToken => "token";
public bool IsLoggedIn => State.Value == APIState.Online; /// <seealso cref="APIAccess.IsLoggedIn"/>
public bool IsLoggedIn => State.Value > APIState.Offline;
public string ProvidedUsername => LocalUser.Value.Username; public string ProvidedUsername => LocalUser.Value.Username;
@ -114,8 +115,10 @@ namespace osu.Game.Online.API
public void Logout() public void Logout()
{ {
LocalUser.Value = new GuestUser();
state.Value = APIState.Offline; 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; public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;

View File

@ -63,6 +63,16 @@ namespace osu.Game.Online.API.Requests.Responses
set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds; 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")] [JsonProperty(@"convert")]
public bool Convert { get; set; } public bool Convert { get; set; }

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -28,6 +29,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -281,6 +283,52 @@ namespace osu.Game
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
private readonly List<string> dragDropFiles = new List<string>();
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] [BackgroundDependencyLoader]
private void load() private void load()
{ {

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -71,7 +72,7 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 20), Font = OsuFont.GetFont(size: 20),
Text = "Let's create an account!", Text = AccountCreationStrings.LetsCreateAnAccount
}, },
usernameTextBox = new OsuTextBox usernameTextBox = new OsuTextBox
{ {
@ -86,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation
}, },
emailTextBox = new OsuTextBox emailTextBox = new OsuTextBox
{ {
PlaceholderText = "email address", PlaceholderText = ModelValidationStrings.UserAttributesUserEmail.ToLower(),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this TabbableContentContainer = this
}, },
@ -118,7 +119,7 @@ namespace osu.Game.Overlays.AccountCreation
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Child = new SettingsButton Child = new SettingsButton
{ {
Text = "Register", Text = LoginPanelStrings.Register,
Margin = new MarginPadding { Vertical = 20 }, Margin = new MarginPadding { Vertical = 20 },
Action = performRegistration Action = performRegistration
} }
@ -132,10 +133,10 @@ namespace osu.Game.Overlays.AccountCreation
textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox }; 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(AccountCreationStrings.EmailDescription1);
emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); emailAddressDescription.AddText(AccountCreationStrings.EmailDescription2, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
passwordDescription.AddText("At least "); passwordDescription.AddText("At least ");
characterCheckText = passwordDescription.AddText("8 characters long"); characterCheckText = passwordDescription.AddText("8 characters long");

View File

@ -17,6 +17,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays.AccountCreation namespace osu.Game.Overlays.AccountCreation
{ {
@ -101,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation
}, },
new SettingsButton new SettingsButton
{ {
Text = "Help, I can't access my account!", Text = AccountCreationStrings.MultiAccountWarningHelp,
Margin = new MarginPadding { Top = 50 }, Margin = new MarginPadding { Top = 50 },
Action = () => game?.OpenUrlExternally(help_centre_url) Action = () => game?.OpenUrlExternally(help_centre_url)
}, },
new DangerousSettingsButton new DangerousSettingsButton
{ {
Text = "I understand. This account isn't for me.", Text = AccountCreationStrings.MultiAccountWarningAccept,
Action = () => this.Push(new ScreenEntry()) Action = () => this.Push(new ScreenEntry())
}, },
furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12)) furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12))

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -12,6 +11,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK; using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.AccountCreation namespace osu.Game.Overlays.AccountCreation
{ {
@ -46,18 +46,18 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Text = "New Player Registration", Text = AccountCreationStrings.NewPlayerRegistration.ToTitle(),
}, },
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 12), Font = OsuFont.GetFont(size: 12),
Text = "let's get you started", Text = AccountCreationStrings.LetsGetYouStarted.ToLower(),
}, },
new SettingsButton new SettingsButton
{ {
Text = "Let's create an account!", Text = AccountCreationStrings.LetsCreateAnAccount,
Margin = new MarginPadding { Vertical = 120 }, Margin = new MarginPadding { Vertical = 120 },
Action = () => this.Push(new ScreenWarning()) Action = () => this.Push(new ScreenWarning())
} }

View File

@ -90,7 +90,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
this.FadeIn(transition_time, Easing.OutQuint); this.FadeIn(transition_time, Easing.OutQuint);
if (welcomeScreen.GetChildScreen() != null) if (welcomeScreen.GetChildScreen() != null)

View File

@ -58,23 +58,25 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDisplay() private void updateDisplay()
{ {
bpm.Value = BeatmapSet?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-";
if (beatmapInfo == null) if (beatmapInfo == null)
{ {
bpm.Value = "-";
length.Value = string.Empty; length.Value = string.Empty;
circleCount.Value = string.Empty; circleCount.Value = string.Empty;
sliderCount.Value = string.Empty; sliderCount.Value = string.Empty;
} }
else else
{ {
length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration()); bpm.Value = beatmapInfo.BPM.ToLocalisableString(@"0.##");
length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); 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"); circleCount.Value = onlineInfo.CircleCount.ToLocalisableString(@"N0");
sliderCount.Value = (onlineInfo?.SliderCount ?? 0).ToLocalisableString(@"N0"); sliderCount.Value = onlineInfo.SliderCount.ToLocalisableString(@"N0");
length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(onlineInfo.HitLength).ToFormattedDuration());
} }
} }

View File

@ -47,9 +47,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
[Resolved]
private ScoreManager scoreManager { get; set; }
private GetScoresRequest getScoresRequest; private GetScoresRequest getScoresRequest;
private CancellationTokenSource loadCancellationSource; private CancellationTokenSource loadCancellationSource;
@ -85,7 +82,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
MD5Hash = apiBeatmap.MD5Hash 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(); var topScore = scores.First();
scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints());

View File

@ -276,8 +276,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
this.MoveToY(0, transition_length, Easing.OutQuint); this.MoveToY(0, transition_length, Easing.OutQuint);
this.FadeIn(transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint);
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -24,19 +25,37 @@ namespace osu.Game.Overlays.Comments
protected abstract LocalisableString FooterText { get; } protected abstract LocalisableString FooterText { get; }
protected abstract LocalisableString CommitButtonText { get; }
protected abstract LocalisableString TextBoxPlaceholder { get; }
protected FillFlowContainer ButtonsContainer { get; private set; } = null!; protected FillFlowContainer ButtonsContainer { get; private set; } = null!;
protected readonly Bindable<string> Current = new Bindable<string>(string.Empty); protected readonly Bindable<string> Current = new Bindable<string>(string.Empty);
private RoundedButton commitButton = null!; private RoundedButton commitButton = null!;
private RoundedButton logInButton = null!;
private LoadingSpinner loadingSpinner = null!; private LoadingSpinner loadingSpinner = null!;
protected TextBox TextBox { get; private set; } = 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> apiState = new Bindable<APIState>();
/// <summary>
/// Returns the text content of the main action button.
/// When <paramref name="isLoggedIn"/> is <see langword="true"/>, the text will apply to a button that posts a comment.
/// When <paramref name="isLoggedIn"/> is <see langword="false"/>, the text will apply to a button that directs the user to the login overlay.
/// </summary>
protected abstract LocalisableString GetButtonText(bool isLoggedIn);
/// <summary>
/// Returns the placeholder text for the comment box.
/// </summary>
/// <param name="isLoggedIn">Whether the current user is logged in.</param>
protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn);
protected bool ShowLoadingSpinner protected bool ShowLoadingSpinner
{ {
set set
@ -78,7 +97,6 @@ namespace osu.Game.Overlays.Comments
{ {
Height = 40, Height = 40,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
PlaceholderText = TextBoxPlaceholder,
Current = Current Current = Current
}, },
new Container new Container
@ -113,10 +131,19 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0), Spacing = new Vector2(5, 0),
Child = commitButton = new EditorButton Children = new Drawable[]
{ {
Text = CommitButtonText, commitButton = new EditorButton
Action = () => OnCommit(Current.Value) {
Action = () => OnCommit(Current.Value),
Text = GetButtonText(true)
},
logInButton = new EditorButton
{
Width = 100,
Action = () => loginOverlay?.Show(),
Text = GetButtonText(false)
}
} }
}, },
loadingSpinner = new LoadingSpinner loadingSpinner = new LoadingSpinner
@ -134,12 +161,14 @@ namespace osu.Game.Overlays.Comments
}); });
TextBox.OnCommit += (_, _) => commitButton.TriggerClick(); TextBox.OnCommit += (_, _) => commitButton.TriggerClick();
apiState.BindTo(API.State);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Current.BindValueChanged(_ => updateCommitButtonState(), true); Current.BindValueChanged(_ => updateCommitButtonState(), true);
apiState.BindValueChanged(updateStateForLoggedIn, true);
} }
protected abstract void OnCommit(string text); protected abstract void OnCommit(string text);
@ -147,6 +176,25 @@ namespace osu.Game.Overlays.Comments
private void updateCommitButtonState() => private void updateCommitButtonState() =>
commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
private void updateStateForLoggedIn(ValueChangedEvent<APIState> 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 private partial class EditorTextBox : OsuTextBox
{ {
protected override float LeftRightPadding => side_padding; protected override float LeftRightPadding => side_padding;

View File

@ -405,17 +405,16 @@ namespace osu.Game.Overlays.Comments
[Resolved] [Resolved]
private CommentsContainer commentsContainer { get; set; } private CommentsContainer commentsContainer { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
public Action<CommentBundle> OnPost; public Action<CommentBundle> OnPost;
//TODO should match web, left empty due to no multiline support //TODO should match web, left empty due to no multiline support
protected override LocalisableString FooterText => default; 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) protected override void OnCommit(string text)
{ {
@ -432,7 +431,7 @@ namespace osu.Game.Overlays.Comments
Current.Value = string.Empty; Current.Value = string.Empty;
OnPost?.Invoke(cb); OnPost?.Invoke(cb);
}); });
api.Queue(req); API.Queue(req);
} }
} }
} }

View File

@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -18,16 +17,17 @@ namespace osu.Game.Overlays.Comments
[Resolved] [Resolved]
private CommentsContainer commentsContainer { get; set; } = null!; private CommentsContainer commentsContainer { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Comment parentComment; private readonly Comment parentComment;
public Action<DrawableComment[]>? OnPost; public Action<DrawableComment[]>? OnPost;
protected override LocalisableString FooterText => default; 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) public ReplyCommentEditor(Comment parent)
{ {
@ -38,7 +38,8 @@ namespace osu.Game.Overlays.Comments
{ {
base.LoadComplete(); base.LoadComplete();
GetContainingInputManager().ChangeFocus(TextBox); if (!TextBox.ReadOnly)
GetContainingInputManager().ChangeFocus(TextBox);
} }
protected override void OnCommit(string text) protected override void OnCommit(string text)
@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Comments
Logger.Error(e, "Posting reply comment failed."); Logger.Error(e, "Posting reply comment failed.");
}); });
req.Success += cb => Schedule(processPostedComments, cb); req.Success += cb => Schedule(processPostedComments, cb);
api.Queue(req); API.Queue(req);
} }
private void processPostedComments(CommentBundle cb) private void processPostedComments(CommentBundle cb)

View File

@ -99,7 +99,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Login namespace osu.Game.Overlays.Login
{ {
@ -47,7 +48,7 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
ErrorTextFlowContainer errorText; ErrorTextFlowContainer errorText;
LinkFlowContainer forgottenPaswordLink; LinkFlowContainer forgottenPasswordLink;
Children = new Drawable[] Children = new Drawable[]
{ {
@ -71,15 +72,15 @@ namespace osu.Game.Overlays.Login
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Remember username", LabelText = LoginPanelStrings.RememberUsername,
Current = config.GetBindable<bool>(OsuSetting.SaveUsername), Current = config.GetBindable<bool>(OsuSetting.SaveUsername),
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Stay signed in", LabelText = LoginPanelStrings.StaySignedIn,
Current = config.GetBindable<bool>(OsuSetting.SavePassword), Current = config.GetBindable<bool>(OsuSetting.SavePassword),
}, },
forgottenPaswordLink = new LinkFlowContainer forgottenPasswordLink = new LinkFlowContainer
{ {
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -105,7 +106,7 @@ namespace osu.Game.Overlays.Login
}, },
new SettingsButton new SettingsButton
{ {
Text = "Register", Text = LoginPanelStrings.Register,
Action = () => Action = () =>
{ {
RequestHide?.Invoke(); 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(); password.OnCommit += (_, _) => performLogin();

View File

@ -6,6 +6,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Login
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = "ACCOUNT", Text = LoginPanelStrings.Account.ToUpper(),
Margin = new MarginPadding { Bottom = 5 }, Margin = new MarginPadding { Bottom = 5 },
Font = OsuFont.GetFont(weight: FontWeight.Bold), 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; break;
case APIState.Online: case APIState.Online:
@ -140,7 +141,7 @@ namespace osu.Game.Overlays.Login
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "Signed in", Text = LoginPanelStrings.SignedIn,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
Margin = new MarginPadding { Top = 5, Bottom = 5 }, Margin = new MarginPadding { Top = 5, Bottom = 5 },
}, },

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.ComponentModel;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Login namespace osu.Game.Overlays.Login
{ {
@ -14,13 +12,13 @@ namespace osu.Game.Overlays.Login
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))] [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))]
Online, Online,
[Description(@"Do not disturb")] [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.DoNotDisturb))]
DoNotDisturb, DoNotDisturb,
[Description(@"Appear offline")] [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
AppearOffline, AppearOffline,
[Description(@"Sign out")] [LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))]
SignOut, SignOut,
} }
} }

View File

@ -75,8 +75,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
panel.Bounding = true; panel.Bounding = true;
this.FadeIn(transition_time, Easing.OutQuint); this.FadeIn(transition_time, Easing.OutQuint);

View File

@ -246,9 +246,13 @@ namespace osu.Game.Overlays
} }
} }
protected override void PopIn()
{
this.FadeIn(200);
}
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut();
this.FadeOut(200); this.FadeOut(200);
} }

View File

@ -6,16 +6,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler<GlobalAction> public partial class DeselectAllModsButton : ShearedButton
{ {
private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>(); private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>();
@ -39,18 +36,5 @@ namespace osu.Game.Overlays.Mods
{ {
Enabled.Value = selectedMods.Value.Any(); Enabled.Value = selectedMods.Value.Any();
} }
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat || e.Action != GlobalAction.DeselectAllMods)
return false;
TriggerClick();
return true;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input
if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch))
return false; 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) if (matchingMods.Length == 0)
return false; return false;

View File

@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input
if (index < 0) if (index < 0)
return false; return false;
var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index); var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index);
if (modState == null) if (modState == null)
return false; return false;

View File

@ -46,7 +46,8 @@ namespace osu.Game.Overlays.Mods
foreach (var mod in availableMods) foreach (var mod in availableMods)
{ {
mod.Active.BindValueChanged(_ => updateState()); mod.Active.BindValueChanged(_ => updateState());
mod.Filtered.BindValueChanged(_ => updateState()); mod.MatchingTextFilter.BindValueChanged(_ => updateState());
mod.ValidForSelection.BindValueChanged(_ => updateState());
} }
updateState(); updateState();
@ -145,12 +146,17 @@ namespace osu.Game.Overlays.Mods
private void updateState() private void updateState()
{ {
Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1; Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1;
if (toggleAllCheckbox != null && !SelectionAnimationRunning) if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{ {
toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0; bool anyPanelsVisible = availableMods.Any(panel => panel.Visible);
toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
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(); 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); pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
} }
@ -206,8 +212,13 @@ namespace osu.Game.Overlays.Mods
{ {
pendingSelectionOperations.Clear(); 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))
pendingSelectionOperations.Enqueue(() => button.Active.Value = false); {
if (!button.Visible)
button.Active.Value = false;
else
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
}
} }
/// <summary> /// <summary>

View File

@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -11,11 +14,10 @@ using osuTK;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class ModPanel : ModSelectPanel public partial class ModPanel : ModSelectPanel, IFilterable
{ {
public Mod Mod => modState.Mod; public Mod Mod => modState.Mod;
public override BindableBool Active => modState.Active; public override BindableBool Active => modState.Active;
public BindableBool Filtered => modState.Filtered;
protected override float IdleSwitchWidth => 54; protected override float IdleSwitchWidth => 54;
protected override float ExpandedSwitchWidth => 70; protected override float ExpandedSwitchWidth => 70;
@ -54,7 +56,8 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
Filtered.BindValueChanged(_ => updateFilterState(), true); modState.ValidForSelection.BindValueChanged(_ => updateFilterState());
modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true);
} }
protected override void Select() protected override void Select()
@ -71,9 +74,25 @@ namespace osu.Game.Overlays.Mods
#region Filtering support #region Filtering support
/// <seealso cref="ModState.Visible"/>
public bool Visible => modState.Visible;
public override IEnumerable<LocalisableString> FilterTerms => new[]
{
Mod.Name,
Mod.Acronym,
Mod.Description
};
public override bool MatchingFilter
{
get => modState.MatchingTextFilter.Value;
set => modState.MatchingTextFilter.Value = value;
}
private void updateFilterState() private void updateFilterState()
{ {
this.FadeTo(Filtered.Value ? 0 : 1); this.FadeTo(Visible ? 1 : 0);
} }
#endregion #endregion

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -81,6 +82,27 @@ namespace osu.Game.Overlays.Mods
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value); Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value);
} }
#region Filtering support
public override IEnumerable<LocalisableString> FilterTerms => getFilterTerms();
private IEnumerable<LocalisableString> 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 #region IHasCustomTooltip
public ModPreset TooltipContent => Preset.Value; public ModPreset TooltipContent => Preset.Value;

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.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();
}
}
}
}

View File

@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods
/// </summary> /// </summary>
public readonly Bindable<bool> Active = new BindableBool(true); public readonly Bindable<bool> Active = new BindableBool(true);
public string SearchTerm
{
set => ItemsFlow.SearchTerm = value;
}
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected readonly Container ControlContainer; protected readonly Container ControlContainer;
protected readonly FillFlowContainer ItemsFlow; protected readonly ModSearchContainer ItemsFlow;
private readonly TextFlowContainer headerText; private readonly TextFlowContainer headerText;
private readonly Box headerBackground; private readonly Box headerBackground;
@ -150,7 +155,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ClampExtension = 100, ClampExtension = 100,
ScrollbarOverlapsContent = false, ScrollbarOverlapsContent = false,
Child = ItemsFlow = new FillFlowContainer Child = ItemsFlow = new ModSearchContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -12,6 +12,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
@ -25,10 +27,11 @@ using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler<PlatformAction>
{ {
public const int BUTTON_WIDTH = 200; 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!;
/// <summary> /// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// Whether the total score multiplier calculated from the current selected set of mods should be shown.
/// </summary> /// </summary>
@ -94,7 +105,7 @@ namespace osu.Game.Overlays.Mods
}; };
} }
yield return new DeselectAllModsButton(this); yield return deselectAllModsButton = new DeselectAllModsButton(this);
} }
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(); private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
@ -107,11 +118,14 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!; private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!; private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!; private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private DeselectAllModsButton deselectAllModsButton = null!;
private Container aboveColumnsContent = null!;
private DifficultyMultiplierDisplay? multiplierDisplay; private DifficultyMultiplierDisplay? multiplierDisplay;
protected ShearedButton BackButton { get; private set; } = null!; protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; } protected ShearedToggleButton? CustomisationButton { get; private set; }
protected SelectAllModsButton? SelectAllModsButton { get; set; }
private Sample? columnAppearSample; private Sample? columnAppearSample;
@ -146,6 +160,17 @@ namespace osu.Game.Overlays.Mods
MainAreaContent.AddRange(new Drawable[] 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 new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -153,7 +178,7 @@ namespace osu.Game.Overlays.Mods
{ {
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING, Top = ModsEffectDisplay.HEIGHT + PADDING,
Bottom = PADDING Bottom = PADDING
}, },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -186,18 +211,10 @@ namespace osu.Game.Overlays.Mods
if (ShowTotalMultiplier) if (ShowTotalMultiplier)
{ {
MainAreaContent.Add(new Container aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = 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
},
}); });
} }
@ -226,6 +243,14 @@ namespace osu.Game.Overlays.Mods
globalAvailableMods.BindTo(game.AvailableMods); 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; private ModSettingChangeTracker? modSettingChangeTracker;
protected override void LoadComplete() protected override void LoadComplete()
@ -263,6 +288,12 @@ namespace osu.Game.Overlays.Mods
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); 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 // Start scrolled slightly to the right to give the user a sense that
// there is more horizontal content available. // there is more horizontal content available.
ScheduleAfterChildren(() => 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;
}
/// <summary> /// <summary>
/// Select all visible mods in all columns. /// Select all visible mods in all columns.
/// </summary> /// </summary>
@ -344,7 +382,7 @@ namespace osu.Game.Overlays.Mods
private void filterMods() private void filterMods()
{ {
foreach (var modState in allAvailableMods) 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() private void updateMultiplier()
@ -469,7 +507,7 @@ namespace osu.Game.Overlays.Mods
base.PopIn(); base.PopIn();
multiplierDisplay? aboveColumnsContent
.FadeIn(fade_in_duration, Easing.OutQuint) .FadeIn(fade_in_duration, Easing.OutQuint)
.MoveToY(0, 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; 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 delay = allFiltered ? 0 : nonFilteredColumnCount * 30;
double duration = allFiltered ? 0 : fade_in_duration; double duration = allFiltered ? 0 : fade_in_duration;
@ -527,7 +565,7 @@ namespace osu.Game.Overlays.Mods
base.PopOut(); base.PopOut();
multiplierDisplay? aboveColumnsContent
.FadeOut(fade_out_duration / 2, Easing.OutQuint) .FadeOut(fade_out_duration / 2, Easing.OutQuint)
.MoveToY(-distance, 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) if (column is ModColumn modColumn)
{ {
allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value); allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible);
modColumn.FlushPendingSelections(); 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 // 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. // and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection: 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: case GlobalAction.Select:
{ {
// Pressing toggle or select should completely hide the overlay in one shot. // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty.
hideOverlay(true); if (string.IsNullOrEmpty(SearchTerm))
{
hideOverlay(true);
return true;
}
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);
if (firstMod is not null)
firstMod.Active.Value = !firstMod.Active.Value;
return true; return true;
} }
} }
@ -603,6 +669,39 @@ namespace osu.Game.Overlays.Mods
} }
} }
/// <inheritdoc cref="IKeyBindingHandler{PlatformAction}"/>
/// <remarks>
/// 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.
/// </remarks>>
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
return false;
SelectAllModsButton.TriggerClick();
return true;
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> 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 #endregion
#region Sample playback control #region Sample playback control
@ -743,6 +842,9 @@ namespace osu.Game.Overlays.Mods
if (!Active.Value) if (!Active.Value)
RequestScroll?.Invoke(this); 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; return true;
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -25,7 +26,7 @@ using osuTK.Input;
namespace osu.Game.Overlays.Mods 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; } public abstract BindableBool Active { get; }
@ -199,6 +200,9 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value) if (samplePlaybackDisabled.Value)
return; return;
if (!IsPresent)
return;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY; bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
if (enoughTimePassedSinceLastPlayback) if (enoughTimePassedSinceLastPlayback)
@ -277,5 +281,28 @@ namespace osu.Game.Overlays.Mods
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
} }
#region IFilterable
public abstract IEnumerable<LocalisableString> 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
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -32,9 +30,21 @@ namespace osu.Game.Overlays.Mods
public bool PendingConfiguration { get; set; } public bool PendingConfiguration { get; set; }
/// <summary> /// <summary>
/// Whether the mod is currently filtered out due to not matching imposed criteria. /// Whether the mod is currently valid for selection.
/// This can be <see langword="false"/> in scenarios such as the free mod select overlay, where not all mods are selectable
/// regardless of search criteria imposed by the user selecting.
/// </summary> /// </summary>
public BindableBool Filtered { get; } = new BindableBool(); public BindableBool ValidForSelection { get; } = new BindableBool(true);
/// <summary>
/// Whether the mod is matching the current textual filter.
/// </summary>
public BindableBool MatchingTextFilter { get; } = new BindableBool(true);
/// <summary>
/// Whether the <see cref="Mod"/> matches all applicable filters and visible for the user to select.
/// </summary>
public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value;
public ModState(Mod mod) public ModState(Mod mod)
{ {

View File

@ -1,14 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; 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.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -16,10 +11,11 @@ using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler<PlatformAction> public partial class SelectAllModsButton : ShearedButton
{ {
private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>(); private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>();
private readonly Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(); private readonly Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>();
private readonly Bindable<string> searchTerm = new Bindable<string>();
public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay) public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay)
: base(ModSelectOverlay.BUTTON_WIDTH) : base(ModSelectOverlay.BUTTON_WIDTH)
@ -29,6 +25,7 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindTo(modSelectOverlay.SelectedMods); selectedMods.BindTo(modSelectOverlay.SelectedMods);
availableMods.BindTo(modSelectOverlay.AvailableMods); availableMods.BindTo(modSelectOverlay.AvailableMods);
searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -37,6 +34,7 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
updateEnabledState(); updateEnabledState();
} }
@ -44,20 +42,7 @@ namespace osu.Game.Overlays.Mods
{ {
Enabled.Value = availableMods.Value Enabled.Value = availableMods.Value
.SelectMany(pair => pair.Value) .SelectMany(pair => pair.Value)
.Any(modState => !modState.Active.Value && !modState.Filtered.Value); .Any(modState => !modState.Active.Value && modState.Visible);
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
if (e.Repeat || e.Action != PlatformAction.SelectAll)
return false;
TriggerClick();
return true;
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
} }
} }
} }

View File

@ -130,7 +130,6 @@ namespace osu.Game.Overlays.Mods
{ {
const double fade_in_duration = 400; const double fade_in_duration = 400;
base.PopIn();
this.FadeIn(fade_in_duration, Easing.OutQuint); this.FadeIn(fade_in_duration, Easing.OutQuint);
Header.MoveToY(0, fade_in_duration, Easing.OutQuint); Header.MoveToY(0, fade_in_duration, Easing.OutQuint);

View File

@ -316,6 +316,8 @@ namespace osu.Game.Overlays
var queuedTrack = getQueuedTrack(); var queuedTrack = getQueuedTrack();
var lastTrack = CurrentTrack; var lastTrack = CurrentTrack;
lastTrack.Completed -= onTrackCompleted;
CurrentTrack = queuedTrack; 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. // 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. // Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback.
// Can lead to leaks. // Can lead to leaks.
var queuedTrack = new DrawableTrack(current.LoadTrack()); var queuedTrack = new DrawableTrack(current.LoadTrack());
queuedTrack.Completed += () => onTrackCompleted(current); queuedTrack.Completed += onTrackCompleted;
return queuedTrack; 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) if (!CurrentTrack.Looping && !beatmap.Disabled)
NextTrack(); NextTrack();
} }

View File

@ -118,7 +118,7 @@ namespace osu.Game.Overlays
private void updateProcessingMode() 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(); notificationsEnabler?.Cancel();
@ -206,8 +206,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);

View File

@ -229,8 +229,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
this.FadeIn(transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint);
dragContainer.ScaleTo(1, transition_length, Easing.OutElastic); dragContainer.ScaleTo(1, transition_length, Easing.OutElastic);
} }

View File

@ -163,8 +163,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);

View File

@ -3,11 +3,13 @@
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Screens; using osu.Game.Screens;
@ -45,6 +47,12 @@ namespace osu.Game.Overlays.SkinEditor
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
switch (e.Action) switch (e.Action)
@ -62,6 +70,8 @@ namespace osu.Game.Overlays.SkinEditor
protected override void PopIn() protected override void PopIn()
{ {
globallyDisableBeatmapSkinSetting();
if (skinEditor != null) if (skinEditor != null)
{ {
skinEditor.Show(); 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() protected override void Update()
{ {
@ -151,8 +167,6 @@ namespace osu.Game.Overlays.SkinEditor
if (skinEditor == null) return; 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. // ensure the toolbar is re-hidden even if a new screen decides to try and show it.
updateComponentVisibility(); updateComponentVisibility();
@ -182,5 +196,25 @@ namespace osu.Game.Overlays.SkinEditor
skinEditor = null; skinEditor = null;
} }
} }
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private LeasedBindable<bool>? 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;
}
} }
} }

View File

@ -34,8 +34,6 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
Waves.Show(); Waves.Show();
this.FadeIn(100, Easing.OutQuint); this.FadeIn(100, Easing.OutQuint);
} }

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Mod that colours hitobjects based on the musical division they are on
/// </summary>
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;
}
}

View File

@ -83,6 +83,11 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.MaximumStatisticsJson)) if (string.IsNullOrEmpty(model.MaximumStatisticsJson))
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics); 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);
} }
/// <summary> /// <summary>

View File

@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
@ -13,5 +14,23 @@ namespace osu.Game.Scoring
/// A user-presentable display title representing this score. /// A user-presentable display title representing this score.
/// </summary> /// </summary>
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}"; public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}";
/// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
/// </summary>
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public static IEnumerable<ScoreInfo> OrderByTotalScore(this IEnumerable<ScoreInfo> 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);
/// <summary>
/// Retrieves the maximum achievable combo for the provided score.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
/// <returns>The maximum achievable combo.</returns>
public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
} }
} }

View File

@ -69,17 +69,6 @@ namespace osu.Game.Scoring
return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach()); return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
} }
/// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
/// </summary>
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> 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);
/// <summary> /// <summary>
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>. /// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
/// </summary> /// </summary>
@ -100,13 +89,6 @@ namespace osu.Game.Scoring
/// <returns>The bindable containing the formatted total score string.</returns> /// <returns>The bindable containing the formatted total score string.</returns>
public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
/// <summary>
/// Retrieves the maximum achievable combo for the provided score.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
/// <returns>The maximum achievable combo.</returns>
public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
/// <summary> /// <summary>
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>. /// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
/// </summary> /// </summary>

View File

@ -86,7 +86,7 @@ namespace osu.Game.Screens.Backgrounds
if (nextBackground == background) if (nextBackground == background)
return false; return false;
Logger.Log("🌅 Background change queued"); Logger.Log(@"🌅 Global background change queued");
cancellationTokenSource?.Cancel(); cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource = new CancellationTokenSource();
@ -94,6 +94,7 @@ namespace osu.Game.Screens.Backgrounds
nextTask?.Cancel(); nextTask?.Cancel();
nextTask = Scheduler.AddDelayed(() => nextTask = Scheduler.AddDelayed(() =>
{ {
Logger.Log(@"🌅 Global background loading");
LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token); LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token);
}, 500); }, 500);

View File

@ -18,6 +18,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid
{ {
[Resolved]
private EditorClock editorClock { get; set; }
protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
: base(referenceObject, startPosition, startTime, endTime) : base(referenceObject, startPosition, startTime, endTime)
{ {
@ -62,14 +65,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
for (int i = 0; i < requiredCircles; i++) 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)) AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i))
{ {
Position = StartPosition, Position = StartPosition,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(diameter), 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) if (travelLength < DistanceBetweenTicks)
travelLength = DistanceBetweenTicks; travelLength = DistanceBetweenTicks;
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed float snappedDistance = LimitedDistanceSnap.Value
// to allow for snapping at a non-multiplied ratio. ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime())
float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); // 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); double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
if (snappedTime > LatestEndTime) if (snappedTime > LatestEndTime)

View File

@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -60,6 +61,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved] [Resolved]
private BindableBeatDivisor beatDivisor { get; set; } private BindableBeatDivisor beatDivisor { get; set; }
/// <summary>
/// When enabled, distance snap should only snap to the current time (as per the editor clock).
/// This is to emulate stable behaviour.
/// </summary>
protected Bindable<bool> LimitedDistanceSnap { get; private set; }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
LimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
protected readonly HitObject ReferenceObject; protected readonly HitObject ReferenceObject;

View File

@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit
private Bindable<float> editorBackgroundDim; private Bindable<float> editorBackgroundDim;
private Bindable<bool> editorHitMarkers; private Bindable<bool> editorHitMarkers;
private Bindable<bool> editorAutoSeekOnPlacement; private Bindable<bool> editorAutoSeekOnPlacement;
private Bindable<bool> editorLimitedDistanceSnap;
public Editor(EditorLoader loader = null) public Editor(EditorLoader loader = null)
{ {
@ -276,6 +277,7 @@ namespace osu.Game.Screens.Edit
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim); editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers); editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement); editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
editorLimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
AddInternal(new OsuContextMenuContainer AddInternal(new OsuContextMenuContainer
{ {
@ -337,6 +339,10 @@ namespace osu.Game.Screens.Edit
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
{ {
State = { BindTarget = editorAutoSeekOnPlacement }, State = { BindTarget = editorAutoSeekOnPlacement },
},
new ToggleMenuItem(EditorStrings.LimitedDistanceSnap)
{
State = { BindTarget = editorLimitedDistanceSnap },
} }
} }
}, },

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Game.Overlays; using osu.Game.Overlays;
using System.Collections.Generic; 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 ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true);
protected override IEnumerable<ShearedButton> CreateFooterButtons() => base.CreateFooterButtons().Prepend( protected override IEnumerable<ShearedButton> CreateFooterButtons()
new SelectAllModsButton(this) => base.CreateFooterButtons()
{ .Prepend(SelectAllModsButton = new SelectAllModsButton(this)
Anchor = Anchor.BottomLeft, {
Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
}); Origin = Anchor.BottomLeft,
});
} }
} }

View File

@ -117,94 +117,115 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)),
Width = 0.8f, Width = 0.8f,
}, },
new Container new GridContainer
{ {
Name = @"Left details",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding ColumnDimensions = new[]
{ {
Left = 20, new Dimension(),
Vertical = 5 new Dimension(GridSizeMode.AutoSize),
}, },
Children = new Drawable[] Content = new[]
{ {
new FillFlowContainer new Drawable[]
{ {
RelativeSizeAxes = Axes.X, new Container
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
new FillFlowContainer Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{ {
AutoSizeAxes = Axes.Both, Left = 20,
Direction = FillDirection.Horizontal, Right = DrawableRoomParticipantsList.SHEAR_WIDTH,
Spacing = new Vector2(5), Vertical = 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 Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, new FillFlowContainer
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
new RoomNameText(), RelativeSizeAxes = Axes.X,
new RoomStatusText() 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
} }
} }
}, },
@ -301,23 +322,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return pills; return pills;
} }
private partial class RoomNameText : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
private Bindable<string> name { get; set; }
public RoomNameText()
{
Font = OsuFont.GetFont(size: 28);
}
[BackgroundDependencyLoader]
private void load()
{
Current = name;
}
}
private partial class RoomStatusText : OnlinePlayComposite private partial class RoomStatusText : OnlinePlayComposite
{ {
[Resolved] [Resolved]
@ -333,7 +337,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Width = 0.5f;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -24,8 +24,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
public partial class DrawableRoomParticipantsList : OnlinePlayComposite public partial class DrawableRoomParticipantsList : OnlinePlayComposite
{ {
public const float SHEAR_WIDTH = 12f;
private const float avatar_size = 36; private const float avatar_size = 36;
private const float height = 60f;
private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0);
private FillFlowContainer<CircularAvatar> avatarFlow; private FillFlowContainer<CircularAvatar> avatarFlow;
private CircularAvatar hostAvatar; private CircularAvatar hostAvatar;
@ -36,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public DrawableRoomParticipantsList() public DrawableRoomParticipantsList()
{ {
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Height = 60; Height = height;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -49,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
CornerRadius = 10, CornerRadius = 10,
Shear = new Vector2(0.2f, 0), Shear = shear,
Child = new Box Child = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -98,7 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
CornerRadius = 10, CornerRadius = 10,
Shear = new Vector2(0.2f, 0), Shear = shear,
Child = new Box Child = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -54,14 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn();
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);
Settings.FadeIn(TRANSITION_DURATION / 2); Settings.FadeIn(TRANSITION_DURATION / 2);
} }
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut();
Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine);
Settings.Delay(TRANSITION_DURATION / 2).FadeOut(TRANSITION_DURATION / 2); Settings.Delay(TRANSITION_DURATION / 2).FadeOut(TRANSITION_DURATION / 2);
} }

View File

@ -49,6 +49,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved] [Resolved]
private MultiplayerClient client { get; set; } private MultiplayerClient client { get; set; }
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
private AddItemButton addItemButton; private AddItemButton addItemButton;
public MultiplayerMatchSubScreen(Room room) public MultiplayerMatchSubScreen(Room room)
@ -334,11 +337,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
updateCurrentItem(); updateCurrentItem();
addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0; addItemButton.Alpha = localUserCanAddItem ? 1 : 0;
Scheduler.AddOnce(UpdateMods); Scheduler.AddOnce(UpdateMods);
} }
private bool localUserCanAddItem => client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly;
private void updateCurrentItem() private void updateCurrentItem()
{ {
Debug.Assert(client.Room != null); Debug.Assert(client.Room != null);
@ -403,18 +408,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
if (client.Room == null) if (!localUserCanAddItem)
return; return;
if (!client.IsHost) // 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;
// 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;
}
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) protected override void Dispose(bool isDisposing)

View File

@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param> /// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> 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. // 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). // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).

View File

@ -95,7 +95,6 @@ namespace osu.Game.Screens.Play.HUD
private void updateGraphVisibility() private void updateGraphVisibility()
{ {
graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In); graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In);
bar.ShowBackground = !ShowGraph.Value;
} }
protected override void Update() protected override void Update()

View File

@ -14,7 +14,6 @@ using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
@ -32,18 +31,8 @@ namespace osu.Game.Screens.Play.HUD
private readonly Box background; private readonly Box background;
private readonly BindableBool showBackground = new BindableBool();
private readonly ColourInfo mainColour; private readonly ColourInfo mainColour;
private readonly ColourInfo mainColourDarkened;
private ColourInfo catchUpColour; private ColourInfo catchUpColour;
private ColourInfo catchUpColourDarkened;
public bool ShowBackground
{
get => showBackground.Value;
set => showBackground.Value = value;
}
public double StartTime public double StartTime
{ {
@ -95,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0, Alpha = 0,
Colour = Colour4.White.Darken(1 + 1 / 4f) Colour = OsuColour.Gray(0.2f),
}, },
catchupBar = new RoundedBar catchupBar = new RoundedBar
{ {
@ -112,12 +101,10 @@ namespace osu.Game.Screens.Play.HUD
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
CornerRadius = 5, CornerRadius = 5,
AccentColour = mainColour = Color4.White, AccentColour = mainColour = OsuColour.Gray(0.9f),
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
}; };
mainColourDarkened = Colour4.White.Darken(1 / 3f);
} }
private void setupAlternateValue() private void setupAlternateValue()
@ -141,16 +128,15 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
catchUpColour = colours.BlueLight; catchUpColour = colours.BlueDark;
catchUpColourDarkened = colours.BlueDark;
showBackground.BindValueChanged(_ => updateBackground(), true);
} }
private void updateBackground() protected override void LoadComplete()
{ {
background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In); base.LoadComplete();
playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In);
background.FadeTo(0.3f, 200, Easing.In);
playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), mainColour, 200, Easing.In);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
@ -190,8 +176,8 @@ namespace osu.Game.Screens.Play.HUD
catchupBar.AccentColour = Interpolation.ValueAt( catchupBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold), Math.Min(timeDelta, colour_transition_threshold),
ShowBackground ? mainColour : mainColourDarkened, mainColour,
ShowBackground ? catchUpColour : catchUpColourDarkened, catchUpColour,
0, colour_transition_threshold, 0, colour_transition_threshold,
Easing.OutQuint); Easing.OutQuint);

View File

@ -4,8 +4,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -13,6 +15,10 @@ namespace osu.Game.Screens.Play.HUD
{ {
public partial class ArgonSongProgressGraph : SegmentedGraph<int> public partial class ArgonSongProgressGraph : SegmentedGraph<int>
{ {
private const int tier_count = 5;
private const int display_granularity = 200;
private IEnumerable<HitObject>? objects; private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects public IEnumerable<HitObject> Objects
@ -21,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
objects = value; objects = value;
const int granularity = 200; int[] values = new int[display_granularity];
int[] values = new int[granularity];
if (!objects.Any()) if (!objects.Any())
return; return;
@ -32,7 +37,7 @@ namespace osu.Game.Screens.Play.HUD
if (lastHit == 0) if (lastHit == 0)
lastHit = objects.Last().StartTime; lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity; double interval = (lastHit - firstHit + 1) / display_granularity;
foreach (var h in objects) foreach (var h in objects)
{ {
@ -51,12 +56,12 @@ namespace osu.Game.Screens.Play.HUD
} }
public ArgonSongProgressGraph() public ArgonSongProgressGraph()
: base(5) : base(tier_count)
{ {
var colours = new List<Colour4>(); var colours = new List<Colour4>();
for (int i = 0; i < 5; i++) for (int i = 0; i < tier_count; i++)
colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f)); colours.Add(OsuColour.Gray(0.2f).Opacity(0.1f));
TierColours = colours; TierColours = colours;
} }

View File

@ -8,16 +8,26 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public partial class SaveFailedScoreButton : CompositeDrawable public partial class SaveFailedScoreButton : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{ {
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>(); private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
private readonly Func<Task<ScoreInfo>> importFailedScore; private readonly Func<Task<ScoreInfo>> importFailedScore;
@ -34,7 +44,7 @@ namespace osu.Game.Screens.Play
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGame? game, Player? player, RealmAccess realm) private void load(OsuGame? game, Player? player)
{ {
InternalChild = button = new DownloadButton InternalChild = button = new DownloadButton
{ {
@ -54,7 +64,7 @@ namespace osu.Game.Screens.Play
{ {
importedScore = realm.Run(r => r.Find<ScoreInfo>(t.GetResultSafely().ID)?.Detach()); importedScore = realm.Run(r => r.Find<ScoreInfo>(t.GetResultSafely().ID)?.Detach());
Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded);
}); }).FireAndForget();
break; break;
} }
} }
@ -87,5 +97,43 @@ namespace osu.Game.Screens.Play
} }
}, true); }, true);
} }
#region Export via hotkey logic (also in ReplayDownloadButton)
public bool OnPressed(KeyBindingPressEvent<GlobalAction> 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<GlobalAction> e)
{
}
private void exportWhenReady(ValueChangedEvent<DownloadState> state)
{
if (state.NewValue != DownloadState.LocallyAvailable) return;
scoreManager.Export(importedScore);
this.state.ValueChanged -= exportWhenReady;
}
#endregion
} }
} }

View File

@ -73,7 +73,7 @@ namespace osu.Game.Screens.Ranking.Expanded
var topStatistics = new List<StatisticDisplay> var topStatistics = new List<StatisticDisplay>
{ {
new AccuracyStatistic(score.Accuracy), new AccuracyStatistic(score.Accuracy),
new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)), new ComboStatistic(score.MaxCombo, score.GetMaximumAchievableCombo()),
new PerformanceStatistic(score), new PerformanceStatistic(score),
}; };

View File

@ -1,30 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Scoring; using osu.Game.Scoring;
using osuTK; using osuTK;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
public partial class ReplayDownloadButton : CompositeDrawable public partial class ReplayDownloadButton : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{ {
public readonly Bindable<ScoreInfo> Score = new Bindable<ScoreInfo>(); public readonly Bindable<ScoreInfo> Score = new Bindable<ScoreInfo>();
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>(); protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
private DownloadButton button; private DownloadButton button = null!;
private ShakeContainer shakeContainer; private ShakeContainer shakeContainer = null!;
private ScoreDownloadTracker downloadTracker; private ScoreDownloadTracker? downloadTracker;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
private ReplayAvailability replayAvailability private ReplayAvailability replayAvailability
{ {
@ -46,8 +50,8 @@ namespace osu.Game.Screens.Ranking
Size = new Vector2(50, 30); Size = new Vector2(50, 30);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(OsuGame game, ScoreModelDownloader scores) private void load(OsuGame? game, ScoreModelDownloader scoreDownloader)
{ {
InternalChild = shakeContainer = new ShakeContainer InternalChild = shakeContainer = new ShakeContainer
{ {
@ -67,7 +71,7 @@ namespace osu.Game.Screens.Ranking
break; break;
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
scores.Download(Score.Value); scoreDownloader.Download(Score.Value);
break; break;
case DownloadState.Importing: case DownloadState.Importing:
@ -99,6 +103,44 @@ namespace osu.Game.Screens.Ranking
}, true); }, true);
} }
#region Export via hotkey logic (also in SaveFailedScoreButton)
public bool OnPressed(KeyBindingPressEvent<GlobalAction> 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<GlobalAction> e)
{
}
private void exportWhenReady(ValueChangedEvent<DownloadState> state)
{
if (state.NewValue != DownloadState.LocallyAvailable) return;
scoreManager.Export(Score.Value);
State.ValueChanged -= exportWhenReady;
}
#endregion
private void updateState() private void updateState()
{ {
switch (replayAvailability) switch (replayAvailability)

View File

@ -160,7 +160,7 @@ namespace osu.Game.Screens.Ranking
if (allowWatchingReplay) if (allowWatchingReplay)
{ {
buttons.Add(new ReplayDownloadButton(null) buttons.Add(new ReplayDownloadButton(SelectedScore.Value)
{ {
Score = { BindTarget = SelectedScore }, Score = { BindTarget = SelectedScore },
Width = 300 Width = 300

View File

@ -155,7 +155,7 @@ namespace osu.Game.Screens.Select
public Bindable<RandomSelectAlgorithm> RandomAlgorithm = new Bindable<RandomSelectAlgorithm>(); public Bindable<RandomSelectAlgorithm> RandomAlgorithm = new Bindable<RandomSelectAlgorithm>();
private readonly List<CarouselBeatmapSet> previouslyVisitedRandomSets = new List<CarouselBeatmapSet>(); private readonly List<CarouselBeatmapSet> previouslyVisitedRandomSets = new List<CarouselBeatmapSet>();
private readonly Stack<CarouselBeatmap> randomSelectedBeatmaps = new Stack<CarouselBeatmap>(); private readonly List<CarouselBeatmap> randomSelectedBeatmaps = new List<CarouselBeatmap>();
private CarouselRoot root; private CarouselRoot root;
@ -348,6 +348,11 @@ namespace osu.Game.Screens.Select
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet)) if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
return; return;
foreach (var beatmap in existingSet.Beatmaps)
randomSelectedBeatmaps.Remove(beatmap);
previouslyVisitedRandomSets.Remove(existingSet);
root.RemoveItem(existingSet); root.RemoveItem(existingSet);
itemsCache.Invalidate(); itemsCache.Invalidate();
@ -501,7 +506,7 @@ namespace osu.Game.Screens.Select
if (selectedBeatmap != null && selectedBeatmapSet != null) 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 // 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. // else the user may be "randomised" to the existing selection.
@ -538,9 +543,10 @@ namespace osu.Game.Screens.Select
{ {
while (randomSelectedBeatmaps.Any()) 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) if (selectedBeatmapSet != null)
{ {

Some files were not shown because too many files have changed in this diff Show More