diff --git a/README.md b/README.md
index 7ace47a74f..f64240f67a 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
-- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
+- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
### Downloading the source code
@@ -72,7 +72,7 @@ git pull
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
-- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
+- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations.
You can also build and run *osu!* from the command-line with a single command:
diff --git a/osu.Android.props b/osu.Android.props
index ab1bd553a8..5b26b8f36e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 3642f70a56..d87b25a4c7 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
@@ -108,10 +109,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset
- int onlineID = ruleset.Value.OnlineID;
- bool isLegacyRuleset = onlineID >= 0 && onlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
-
- presence.Assets.SmallImageKey = isLegacyRuleset ? $"mode_{onlineID}" : "mode_custom";
+ presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name;
client.SetPresence(presence);
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
index 9540e35780..99a064d35f 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
get
{
- string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}";
+ string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}";
return string.Join(", ", new[]
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
index 98d8a41674..2efd125f81 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs
@@ -4,11 +4,9 @@
using System;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
-using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Tests.Visual.Editing
{
@@ -22,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("switch between all screens at once", () =>
{
foreach (var screen in Enum.GetValues(typeof(EditorScreenMode)).Cast())
- Editor.ChildrenOfType().Single().Mode.Value = screen;
+ Editor.Mode.Value = screen;
});
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
new file mode 100644
index 0000000000..4b079cbb2c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
@@ -0,0 +1,102 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Settings;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play.PlayerSettings;
+using osu.Game.Tests.Visual.Ranking;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneBeatmapOffsetControl : OsuTestScene
+ {
+ private BeatmapOffsetControl offsetControl;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create control", () =>
+ {
+ Child = new PlayerSettingsGroup("Some settings")
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ offsetControl = new BeatmapOffsetControl()
+ }
+ };
+ });
+ }
+
+ [Test]
+ public void TestTooShortToDisplay()
+ {
+ AddStep("Set short reference score", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2)
+ };
+ });
+
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestCalibrationFromZero()
+ {
+ const double average_error = -4.5;
+
+ AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ AddStep("Set reference score", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
+ };
+ });
+
+ AddAssert("Has calibration button", () => offsetControl.ChildrenOfType().Any());
+ AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick());
+ AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error);
+
+ AddAssert("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value);
+ AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+
+ ///
+ /// When a beatmap offset was already set, the calibration should take it into account.
+ ///
+ [Test]
+ public void TestCalibrationFromNonZero()
+ {
+ const double average_error = -4.5;
+ const double initial_offset = -2;
+
+ AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ AddStep("Set reference score", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
+ };
+ });
+
+ AddAssert("Has calibration button", () => offsetControl.ChildrenOfType().Any());
+ AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick());
+ AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error);
+
+ AddAssert("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value);
+ AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index e03c8d7561..b195d2aa74 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
@@ -131,9 +131,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime;
- protected override void Update()
+ protected override void UpdateAfterChildren()
{
- base.Update();
+ base.UpdateAfterChildren();
if (!FirstFrameClockTime.HasValue)
{
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
index 4bc843096f..f31aec8975 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
@@ -23,6 +23,12 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(CreateDistributedHitEvents());
}
+ [Test]
+ public void TestManyDistributedEventsOffset()
+ {
+ createTest(CreateDistributedHitEvents(-3.5));
+ }
+
[Test]
public void TestAroundCentre()
{
@@ -71,16 +77,16 @@ namespace osu.Game.Tests.Visual.Ranking
};
});
- public static List CreateDistributedHitEvents()
+ public static List CreateDistributedHitEvents(double centre = 0, double range = 25)
{
var hitEvents = new List();
- for (int i = 0; i < 50; i++)
+ for (int i = 0; i < range * 2; i++)
{
- int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2));
+ int count = (int)(Math.Pow(range - Math.Abs(i - range), 2));
for (int j = 0; j < count; j++)
- hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
+ hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
}
return hitEvents;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 31bd3a203c..1ed6648131 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -119,7 +119,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure));
AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn));
- AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardState.Unavailable));
+ AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable));
+ AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable));
AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected));
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs
new file mode 100644
index 0000000000..e47ae860c6
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs
@@ -0,0 +1,187 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneModColumn : OsuManualInputManagerTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ [TestCase(ModType.DifficultyReduction)]
+ [TestCase(ModType.DifficultyIncrease)]
+ [TestCase(ModType.Conversion)]
+ [TestCase(ModType.Automation)]
+ [TestCase(ModType.Fun)]
+ public void TestBasic(ModType modType)
+ {
+ AddStep("create content", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(30),
+ Child = new ModColumn(modType, false)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ }
+ });
+
+ AddStep("change ruleset to osu!", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
+ AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
+ AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
+ AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
+ }
+
+ [Test]
+ public void TestMultiSelection()
+ {
+ ModColumn column = null;
+ AddStep("create content", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(30),
+ Child = column = new ModColumn(ModType.DifficultyIncrease, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ }
+ });
+
+ AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded);
+
+ clickToggle();
+ AddUntilStep("all panels selected", () => this.ChildrenOfType().All(panel => panel.Active.Value));
+
+ clickToggle();
+ AddUntilStep("all panels deselected", () => this.ChildrenOfType().All(panel => !panel.Active.Value));
+
+ AddStep("manually activate all panels", () => this.ChildrenOfType().ForEach(panel => panel.Active.Value = true));
+ AddUntilStep("checkbox selected", () => this.ChildrenOfType().Single().Current.Value);
+
+ AddStep("deselect first panel", () => this.ChildrenOfType().First().Active.Value = false);
+ AddUntilStep("checkbox not selected", () => !this.ChildrenOfType().Single().Current.Value);
+
+ void clickToggle() => AddStep("click toggle", () =>
+ {
+ var checkbox = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(checkbox);
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+
+ [Test]
+ public void TestFiltering()
+ {
+ TestModColumn column = null;
+
+ AddStep("create content", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(30),
+ Child = column = new TestModColumn(ModType.Fun, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ }
+ });
+
+ AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase));
+ AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2);
+
+ clickToggle();
+ AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
+ AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value));
+
+ AddStep("unset filter", () => column.Filter = null);
+ AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value));
+ AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value);
+
+ AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase));
+ AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2);
+ AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value);
+
+ AddStep("filter out everything", () => column.Filter = _ => false);
+ AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value));
+ AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent);
+
+ AddStep("inset filter", () => column.Filter = null);
+ AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value));
+ AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent);
+
+ void clickToggle() => AddStep("click toggle", () =>
+ {
+ var checkbox = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(checkbox);
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+
+ [Test]
+ public void TestKeyboardSelection()
+ {
+ ModColumn column = null;
+ AddStep("create content", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(30),
+ Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P })
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ }
+ });
+
+ AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded);
+
+ AddStep("press W", () => InputManager.Key(Key.W));
+ AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
+
+ AddStep("press W again", () => InputManager.Key(Key.W));
+ AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
+
+ AddStep("set filter to NF", () => column.Filter = mod => mod.Acronym == "NF");
+
+ AddStep("press W", () => InputManager.Key(Key.W));
+ AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
+
+ AddStep("press W again", () => InputManager.Key(Key.W));
+ AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
+
+ AddStep("filter out everything", () => column.Filter = _ => false);
+
+ AddStep("press W", () => InputManager.Key(Key.W));
+ AddAssert("NF panel not selected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
+
+ AddStep("clear filter", () => column.Filter = null);
+ }
+
+ private class TestModColumn : ModColumn
+ {
+ public new bool SelectionAnimationRunning => base.SelectionAnimationRunning;
+
+ public TestModColumn(ModType modType, bool allowBulkSelection)
+ : base(modType, allowBulkSelection)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 305b3979a0..c6f69286cd 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Beatmaps
[Backlink(nameof(ScoreInfo.BeatmapInfo))]
public IQueryable Scores { get; } = null!;
+ public BeatmapUserSettings UserSettings { get; set; } = null!;
+
public BeatmapInfo(RulesetInfo? ruleset = null, BeatmapDifficulty? difficulty = null, BeatmapMetadata? metadata = null)
{
ID = Guid.NewGuid();
@@ -51,6 +53,7 @@ namespace osu.Game.Beatmaps
};
Difficulty = difficulty ?? new BeatmapDifficulty();
Metadata = metadata ?? new BeatmapMetadata();
+ UserSettings = new BeatmapUserSettings();
}
[UsedImplicitly]
diff --git a/osu.Game/Beatmaps/BeatmapUserSettings.cs b/osu.Game/Beatmaps/BeatmapUserSettings.cs
new file mode 100644
index 0000000000..5c71bf34b1
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapUserSettings.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+using Realms;
+
+namespace osu.Game.Beatmaps
+{
+ ///
+ /// User settings overrides that are attached to a beatmap.
+ ///
+ public class BeatmapUserSettings : EmbeddedObject
+ {
+ ///
+ /// An audio offset that can be used for timing adjustments.
+ ///
+ public double Offset { get; set; }
+ }
+}
diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs
index 3949e84f4a..93c2fccbc7 100644
--- a/osu.Game/Beatmaps/DifficultyRecommender.cs
+++ b/osu.Game/Beatmaps/DifficultyRecommender.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
@@ -83,7 +84,7 @@ namespace osu.Game.Beatmaps
requestedUserId = api.LocalUser.Value.Id;
// only query API for built-in rulesets
- rulesets.AvailableRulesets.Where(ruleset => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo =>
+ rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{
var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 07d2026c65..1358b41ad2 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -240,9 +240,9 @@ namespace osu.Game.Configuration
};
}
- public Func LookupSkinName { private get; set; }
+ public Func LookupSkinName { private get; set; } = _ => @"unknown";
- public Func LookupKeyBindings { get; set; }
+ public Func LookupKeyBindings { get; set; } = _ => @"unknown";
}
// IMPORTANT: These are used in user configuration files.
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index bf2b48ea52..fb3052d850 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -54,8 +54,9 @@ namespace osu.Game.Database
/// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings.
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
+ /// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
///
- private const int schema_version = 13;
+ private const int schema_version = 14;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -564,6 +565,11 @@ namespace osu.Game.Database
}
break;
+
+ case 14:
+ foreach (var beatmap in migration.NewRealm.All())
+ beatmap.UserSettings = new BeatmapUserSettings();
+ break;
}
}
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index f89bbbe19d..6dc18df9e0 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -38,6 +38,7 @@ namespace osu.Game.Database
c.CreateMap()
.ForMember(s => s.Ruleset, cc => cc.Ignore())
.ForMember(s => s.Metadata, cc => cc.Ignore())
+ .ForMember(s => s.UserSettings, cc => cc.Ignore())
.ForMember(s => s.Difficulty, cc => cc.Ignore())
.ForMember(s => s.BeatmapSet, cc => cc.Ignore())
.AfterMap((s, d) =>
@@ -154,6 +155,7 @@ namespace osu.Game.Database
c.CreateMap();
c.CreateMap();
+ c.CreateMap();
c.CreateMap();
c.CreateMap();
c.CreateMap();
diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs
index f178a5c97b..13c25e45c8 100644
--- a/osu.Game/Extensions/ModelExtensions.cs
+++ b/osu.Game/Extensions/ModelExtensions.cs
@@ -72,6 +72,11 @@ namespace osu.Game.Extensions
return result;
}
+ ///
+ /// Check whether this 's online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania).
+ ///
+ public static bool IsLegacyRuleset(this IRulesetInfo ruleset) => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
+
///
/// Check whether the online ID of two s match.
///
diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs
index 0d543bdbc8..d331b818a1 100644
--- a/osu.Game/Graphics/Containers/ScalingContainer.cs
+++ b/osu.Game/Graphics/Containers/ScalingContainer.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Screens;
@@ -38,24 +39,24 @@ namespace osu.Game.Graphics.Containers
private BackgroundScreenStack backgroundStack;
- private bool allowScaling = true;
+ private RectangleF? customRect;
+ private bool customRectIsRelativePosition;
///
- /// Whether user scaling preferences should be applied. Enabled by default.
+ /// Set a custom position and scale which overrides any user specification.
///
- public bool AllowScaling
+ /// A rectangle with positional and sizing information for this container to conform to. null will clear the custom rect and revert to user settings.
+ /// Whether the position portion of the provided rect is in relative coordinate space or not.
+ public void SetCustomRect(RectangleF? rect, bool relativePosition = false)
{
- get => allowScaling;
- set
- {
- if (value == allowScaling)
- return;
+ customRect = rect;
+ customRectIsRelativePosition = relativePosition;
- allowScaling = value;
- if (IsLoaded) Scheduler.AddOnce(updateSize);
- }
+ if (IsLoaded) Scheduler.AddOnce(updateSize);
}
+ private const float corner_radius = 10;
+
///
/// Create a new instance.
///
@@ -69,7 +70,7 @@ namespace osu.Game.Graphics.Containers
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
- CornerRadius = 10,
+ CornerRadius = corner_radius,
Child = content = new ScalingDrawSizePreservingFillContainer(targetMode != ScalingMode.Gameplay)
};
}
@@ -137,7 +138,7 @@ namespace osu.Game.Graphics.Containers
private void updateSize()
{
- const float fade_time = 500;
+ const float duration = 500;
if (targetMode == ScalingMode.Everything)
{
@@ -156,17 +157,31 @@ namespace osu.Game.Graphics.Containers
backgroundStack.Push(new ScalingBackgroundScreen());
}
- backgroundStack.FadeIn(fade_time);
+ backgroundStack.FadeIn(duration);
}
else
- backgroundStack?.FadeOut(fade_time);
+ backgroundStack?.FadeOut(duration);
}
- bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode);
+ RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One);
- var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
- var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;
- bool requiresMasking = (scaling && targetSize != Vector2.One)
+ if (customRect != null)
+ {
+ sizableContainer.RelativePositionAxes = customRectIsRelativePosition ? Axes.Both : Axes.None;
+
+ targetRect = customRect.Value;
+ }
+ else if (targetMode == null || scalingMode.Value == targetMode)
+ {
+ sizableContainer.RelativePositionAxes = Axes.Both;
+
+ Vector2 scale = new Vector2(sizeX.Value, sizeY.Value);
+ Vector2 pos = new Vector2(posX.Value, posY.Value) * (Vector2.One - scale);
+
+ targetRect = new RectangleF(pos, scale);
+ }
+
+ bool requiresMasking = targetRect.Size != Vector2.One
// For the top level scaling container, for now we apply masking if safe areas are in use.
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|| (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
@@ -174,8 +189,14 @@ namespace osu.Game.Graphics.Containers
if (requiresMasking)
sizableContainer.Masking = true;
- sizableContainer.MoveTo(targetPosition, 500, Easing.OutQuart);
- sizableContainer.ResizeTo(targetSize, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
+ sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart);
+ sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart);
+
+ // Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius.
+ // Masking and corner radius should likely only be applied at one point in the full game stack to fix this.
+ // An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything".
+ sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None)
+ .OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
}
private class ScalingBackgroundScreen : BackgroundScreenDefault
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs
index 0cc751ea21..03fad00e41 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursor.cs
@@ -140,6 +140,7 @@ namespace osu.Game.Graphics.Cursor
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
+ channel.Volume.Value = baseFrequency;
channel.Play();
}
diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs
new file mode 100644
index 0000000000..7b2a9e50b2
--- /dev/null
+++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class BeatmapOffsetControlStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl";
+
+ ///
+ /// "Beatmap offset"
+ ///
+ public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset");
+
+ ///
+ /// "Previous play:"
+ ///
+ public static LocalisableString PreviousPlay => new TranslatableString(getKey(@"previous_play"), @"Previous play:");
+
+ ///
+ /// "Previous play too short to use for calibration"
+ ///
+ public static LocalisableString PreviousPlayTooShortToUseForCalibration => new TranslatableString(getKey(@"previous_play_too_short_to_use_for_calibration"), @"Previous play too short to use for calibration");
+
+ ///
+ /// "Calibrate using last play"
+ ///
+ public static LocalisableString CalibrateUsingLastPlay => new TranslatableString(getKey(@"calibrate_using_last_play"), @"Calibrate using last play");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
\ No newline at end of file
diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs
new file mode 100644
index 0000000000..8e53f8e88c
--- /dev/null
+++ b/osu.Game/Localisation/LeaderboardStrings.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class LeaderboardStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.Leaderboard";
+
+ ///
+ /// "Couldn't fetch scores!"
+ ///
+ public static LocalisableString CouldntFetchScores => new TranslatableString(getKey(@"couldnt_fetch_scores"), @"Couldn't fetch scores!");
+
+ ///
+ /// "Please select a beatmap!"
+ ///
+ public static LocalisableString PleaseSelectABeatmap => new TranslatableString(getKey(@"please_select_a_beatmap"), @"Please select a beatmap!");
+
+ ///
+ /// "Leaderboards are not available for this ruleset!"
+ ///
+ public static LocalisableString LeaderboardsAreNotAvailableForThisRuleset => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_ruleset"), @"Leaderboards are not available for this ruleset!");
+
+ ///
+ /// "Leaderboards are not available for this beatmap!"
+ ///
+ public static LocalisableString LeaderboardsAreNotAvailableForThisBeatmap => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_beatmap"), @"Leaderboards are not available for this beatmap!");
+
+ ///
+ /// "No records yet!"
+ ///
+ public static LocalisableString NoRecordsYet => new TranslatableString(getKey(@"no_records_yet"), @"No records yet!");
+
+ ///
+ /// "Please sign in to view online leaderboards!"
+ ///
+ public static LocalisableString PleaseSignInToViewOnlineLeaderboards => new TranslatableString(getKey(@"please_sign_in_to_view_online_leaderboards"), @"Please sign in to view online leaderboards!");
+
+ ///
+ /// "Please invest in an osu!supporter tag to view this leaderboard!"
+ ///
+ public static LocalisableString PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard => new TranslatableString(getKey(@"please_invest_in_an_osu_supporter_tag_to_view_this_leaderboard"), @"Please invest in an osu!supporter tag to view this leaderboard!");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetWikiRequest.cs b/osu.Game/Online/API/Requests/GetWikiRequest.cs
index 248fcc03e3..09571ab0a8 100644
--- a/osu.Game/Online/API/Requests/GetWikiRequest.cs
+++ b/osu.Game/Online/API/Requests/GetWikiRequest.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Extensions;
+using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
@@ -8,14 +10,14 @@ namespace osu.Game.Online.API.Requests
public class GetWikiRequest : APIRequest
{
private readonly string path;
- private readonly string locale;
+ private readonly Language language;
- public GetWikiRequest(string path, string locale = "en")
+ public GetWikiRequest(string path, Language language = Language.en)
{
this.path = path;
- this.locale = locale;
+ this.language = language;
}
- protected override string Target => $"wiki/{locale}/{path}";
+ protected override string Target => $"wiki/{language.ToCultureCode()}/{path}";
}
}
diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs
index 5dd3e46b4a..c94a6d3361 100644
--- a/osu.Game/Online/Leaderboards/Leaderboard.cs
+++ b/osu.Game/Online/Leaderboards/Leaderboard.cs
@@ -22,6 +22,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osuTK;
using osuTK.Graphics;
+using osu.Game.Localisation;
namespace osu.Game.Online.Leaderboards
{
@@ -311,25 +312,28 @@ namespace osu.Game.Online.Leaderboards
switch (state)
{
case LeaderboardState.NetworkFailure:
- return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
+ return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync)
{
Action = RefetchScores
};
case LeaderboardState.NoneSelected:
- return new MessagePlaceholder(@"Please select a beatmap!");
+ return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap);
- case LeaderboardState.Unavailable:
- return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!");
+ case LeaderboardState.RulesetUnavailable:
+ return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset);
+
+ case LeaderboardState.BeatmapUnavailable:
+ return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap);
case LeaderboardState.NoScores:
- return new MessagePlaceholder(@"No records yet!");
+ return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet);
case LeaderboardState.NotLoggedIn:
- return new LoginPlaceholder(@"Please sign in to view online leaderboards!");
+ return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards);
case LeaderboardState.NotSupporter:
- return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!");
+ return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard);
case LeaderboardState.Retrieving:
return null;
diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs
index 75e2c6e6db..6b07500a98 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardState.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs
@@ -8,7 +8,8 @@ namespace osu.Game.Online.Leaderboards
Success,
Retrieving,
NetworkFailure,
- Unavailable,
+ BeatmapUnavailable,
+ RulesetUnavailable,
NoneSelected,
NoScores,
NotLoggedIn,
diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs
index f8a326a52e..d03b3d8ffc 100644
--- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs
+++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Online.Placeholders
@@ -12,7 +13,7 @@ namespace osu.Game.Online.Placeholders
[Resolved(CanBeNull = true)]
private LoginOverlay login { get; set; }
- public LoginPlaceholder(string actionMessage)
+ public LoginPlaceholder(LocalisableString actionMessage)
: base(actionMessage, FontAwesome.Solid.UserLock)
{
Action = () => login?.Show();
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
new file mode 100644
index 0000000000..736a0205e2
--- /dev/null
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -0,0 +1,426 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Humanizer;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Utils;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+#nullable enable
+
+namespace osu.Game.Overlays.Mods
+{
+ public class ModColumn : CompositeDrawable
+ {
+ private Func? filter;
+
+ ///
+ /// Function determining whether each mod in the column should be displayed.
+ /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed.
+ /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden.
+ ///
+ public Func? Filter
+ {
+ get => filter;
+ set
+ {
+ filter = value;
+ updateFilter();
+ }
+ }
+
+ private readonly ModType modType;
+ private readonly Key[]? toggleKeys;
+
+ private readonly Bindable>> availableMods = new Bindable>>();
+
+ private readonly TextFlowContainer headerText;
+ private readonly Box headerBackground;
+ private readonly Container contentContainer;
+ private readonly Box contentBackground;
+ private readonly FillFlowContainer panelFlow;
+ private readonly ToggleAllCheckbox? toggleAllCheckbox;
+
+ private Colour4 accentColour;
+
+ private Task? latestLoadTask;
+ internal bool ItemsLoaded => latestLoadTask == null;
+
+ private const float header_height = 42;
+
+ public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null)
+ {
+ this.modType = modType;
+ this.toggleKeys = toggleKeys;
+
+ Width = 320;
+ RelativeSizeAxes = Axes.Y;
+ Shear = new Vector2(ModPanel.SHEAR_X, 0);
+ CornerRadius = ModPanel.CORNER_RADIUS;
+ Masking = true;
+
+ Container controlContainer;
+ InternalChildren = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = header_height + ModPanel.CORNER_RADIUS,
+ Children = new Drawable[]
+ {
+ headerBackground = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = header_height + ModPanel.CORNER_RADIUS
+ },
+ headerText = new OsuTextFlowContainer(t =>
+ {
+ t.Font = OsuFont.TorusAlternate.With(size: 17);
+ t.Shadow = false;
+ t.Colour = Colour4.Black;
+ })
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Shear = new Vector2(-ModPanel.SHEAR_X, 0),
+ Padding = new MarginPadding
+ {
+ Horizontal = 17,
+ Bottom = ModPanel.CORNER_RADIUS
+ }
+ }
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = header_height },
+ Child = contentContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = ModPanel.CORNER_RADIUS,
+ BorderThickness = 3,
+ Children = new Drawable[]
+ {
+ contentBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ controlContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding { Horizontal = 14 }
+ }
+ },
+ new Drawable[]
+ {
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarOverlapsContent = false,
+ Child = panelFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 7),
+ Padding = new MarginPadding(7)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ createHeaderText();
+
+ if (allowBulkSelection)
+ {
+ controlContainer.Height = 35;
+ controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Scale = new Vector2(0.8f),
+ RelativeSizeAxes = Axes.X,
+ LabelText = "Enable All",
+ Shear = new Vector2(-ModPanel.SHEAR_X, 0)
+ });
+ panelFlow.Padding = new MarginPadding
+ {
+ Top = 0,
+ Bottom = 7,
+ Horizontal = 7
+ };
+ }
+ }
+
+ private void createHeaderText()
+ {
+ IEnumerable headerTextWords = modType.Humanize(LetterCasing.Title).Split(' ');
+
+ if (headerTextWords.Count() > 1)
+ {
+ headerText.AddText($"{headerTextWords.First()} ", t => t.Font = t.Font.With(weight: FontWeight.SemiBold));
+ headerTextWords = headerTextWords.Skip(1);
+ }
+
+ headerText.AddText(string.Join(' ', headerTextWords));
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours)
+ {
+ availableMods.BindTo(game.AvailableMods);
+
+ headerBackground.Colour = accentColour = colours.ForModType(modType);
+
+ if (toggleAllCheckbox != null)
+ {
+ toggleAllCheckbox.AccentColour = accentColour;
+ toggleAllCheckbox.AccentHoverColour = accentColour.Lighten(0.3f);
+ }
+
+ contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3);
+ contentBackground.Colour = colourProvider.Background4;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods));
+ updateMods();
+ }
+
+ private CancellationTokenSource? cancellationTokenSource;
+
+ private void updateMods()
+ {
+ var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(modType) ?? Array.Empty()).ToList();
+
+ if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod)))
+ return;
+
+ cancellationTokenSource?.Cancel();
+
+ var panels = newMods.Select(mod => new ModPanel(mod)
+ {
+ Shear = new Vector2(-ModPanel.SHEAR_X, 0)
+ });
+
+ Task? loadTask;
+
+ latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
+ {
+ panelFlow.ChildrenEnumerable = loaded;
+
+ foreach (var panel in panelFlow)
+ panel.Active.BindValueChanged(_ => updateToggleState());
+ updateToggleState();
+
+ updateFilter();
+ }, (cancellationTokenSource = new CancellationTokenSource()).Token);
+ loadTask.ContinueWith(_ =>
+ {
+ if (loadTask == latestLoadTask)
+ latestLoadTask = null;
+ });
+ }
+
+ #region Bulk select / deselect
+
+ private const double initial_multiple_selection_delay = 120;
+
+ private double selectionDelay = initial_multiple_selection_delay;
+ private double lastSelection;
+
+ private readonly Queue pendingSelectionOperations = new Queue();
+
+ protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay)
+ {
+ if (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
+ {
+ dequeuedAction();
+
+ // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
+ selectionDelay = Math.Max(30, selectionDelay * 0.8f);
+ lastSelection = Time.Current;
+ }
+ else
+ {
+ // reset the selection delay after all animations have been completed.
+ // this will cause the next action to be immediately performed.
+ selectionDelay = initial_multiple_selection_delay;
+ }
+ }
+ }
+
+ private void updateToggleState()
+ {
+ if (toggleAllCheckbox != null && !SelectionAnimationRunning)
+ {
+ toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0;
+ toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
+ }
+ }
+
+ ///
+ /// Selects all mods.
+ ///
+ public void SelectAll()
+ {
+ pendingSelectionOperations.Clear();
+
+ foreach (var button in panelFlow.Where(b => !b.Active.Value && !b.Filtered.Value))
+ pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
+ }
+
+ ///
+ /// Deselects all mods.
+ ///
+ public void DeselectAll()
+ {
+ pendingSelectionOperations.Clear();
+
+ foreach (var button in panelFlow.Where(b => b.Active.Value && !b.Filtered.Value))
+ pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
+ }
+
+ private class ToggleAllCheckbox : OsuCheckbox
+ {
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+ updateState();
+ }
+ }
+
+ private Color4 accentHoverColour;
+
+ public Color4 AccentHoverColour
+ {
+ get => accentHoverColour;
+ set
+ {
+ accentHoverColour = value;
+ updateState();
+ }
+ }
+
+ private readonly ModColumn column;
+
+ public ToggleAllCheckbox(ModColumn column)
+ : base(false)
+ {
+ this.column = column;
+ }
+
+ protected override void ApplyLabelParameters(SpriteText text)
+ {
+ base.ApplyLabelParameters(text);
+ text.Font = text.Font.With(weight: FontWeight.SemiBold);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ updateState();
+ }
+
+ private void updateState()
+ {
+ Nub.AccentColour = AccentColour;
+ Nub.GlowingAccentColour = AccentHoverColour;
+ Nub.GlowColour = AccentHoverColour.Opacity(0.2f);
+ }
+
+ protected override void OnUserChange(bool value)
+ {
+ if (value)
+ column.SelectAll();
+ else
+ column.DeselectAll();
+ }
+ }
+
+ #endregion
+
+ #region Filtering support
+
+ private void updateFilter()
+ {
+ foreach (var modPanel in panelFlow)
+ modPanel.ApplyFilter(Filter);
+
+ updateToggleState();
+ }
+
+ #endregion
+
+ #region Keyboard selection support
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.ControlPressed || e.AltPressed) return false;
+ if (toggleKeys == null) return false;
+
+ int index = Array.IndexOf(toggleKeys, e.Key);
+ if (index < 0) return false;
+
+ var panel = panelFlow.ElementAtOrDefault(index);
+ if (panel == null || panel.Filtered.Value) return false;
+
+ panel.Active.Toggle();
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs
index af8ad3eb18..312171cf74 100644
--- a/osu.Game/Overlays/Mods/ModPanel.cs
+++ b/osu.Game/Overlays/Mods/ModPanel.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -28,6 +29,7 @@ namespace osu.Game.Overlays.Mods
{
public Mod Mod { get; }
public BindableBool Active { get; } = new BindableBool();
+ public BindableBool Filtered { get; } = new BindableBool();
protected readonly Box Background;
protected readonly Container SwitchContainer;
@@ -40,10 +42,10 @@ namespace osu.Game.Overlays.Mods
protected const double TRANSITION_DURATION = 150;
- protected const float SHEAR_X = 0.2f;
+ public const float SHEAR_X = 0.2f;
+ public const float CORNER_RADIUS = 7;
protected const float HEIGHT = 42;
- protected const float CORNER_RADIUS = 7;
protected const float IDLE_SWITCH_WIDTH = 54;
protected const float EXPANDED_SWITCH_WIDTH = 70;
@@ -157,6 +159,7 @@ namespace osu.Game.Overlays.Mods
playStateChangeSamples();
UpdateState();
});
+ Filtered.BindValueChanged(_ => updateFilterState());
UpdateState();
FinishTransforms(true);
@@ -190,7 +193,7 @@ namespace osu.Game.Overlays.Mods
mouseDown = true;
UpdateState();
- return true;
+ return false;
}
protected override void OnMouseUp(MouseUpEvent e)
@@ -235,5 +238,19 @@ namespace osu.Game.Overlays.Mods
TextBackground.FadeColour(textBackgroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
}
+
+ #region Filtering support
+
+ public void ApplyFilter(Func? filter)
+ {
+ Filtered.Value = filter != null && !filter.Invoke(Mod);
+ }
+
+ private void updateFilterState()
+ {
+ this.FadeTo(Filtered.Value ? 0 : 1);
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
index 5f513582e5..922f3832e4 100644
--- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -63,11 +64,17 @@ namespace osu.Game.Overlays.Profile.Header
};
}
+ private CancellationTokenSource cancellationTokenSource;
+
private void updateDisplay(APIUser user)
{
- var badges = user.Badges;
+ cancellationTokenSource?.Cancel();
+ cancellationTokenSource = new CancellationTokenSource();
+
badgeFlowContainer.Clear();
+ var badges = user.Badges;
+
if (badges?.Length > 0)
{
Show();
@@ -79,7 +86,7 @@ namespace osu.Game.Overlays.Profile.Header
{
// load in stable order regardless of async load order.
badgeFlowContainer.Insert(displayIndex, asyncBadge);
- });
+ }, cancellationTokenSource.Token);
}
}
else
@@ -87,5 +94,11 @@ namespace osu.Game.Overlays.Profile.Header
Hide();
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationTokenSource?.Cancel();
+ base.Dispose(isDisposing);
+ }
}
}
diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs
index 08321f68fe..b4178359a4 100644
--- a/osu.Game/Overlays/SettingsToolboxGroup.cs
+++ b/osu.Game/Overlays/SettingsToolboxGroup.cs
@@ -22,8 +22,9 @@ namespace osu.Game.Overlays
{
public class SettingsToolboxGroup : Container, IExpandable
{
+ public const int CONTAINER_WIDTH = 270;
+
private const float transition_duration = 250;
- private const int container_width = 270;
private const int border_thickness = 2;
private const int header_height = 30;
private const int corner_radius = 5;
@@ -49,7 +50,7 @@ namespace osu.Game.Overlays
public SettingsToolboxGroup(string title)
{
AutoSizeAxes = Axes.Y;
- Width = container_width;
+ Width = CONTAINER_WIDTH;
Masking = true;
CornerRadius = corner_radius;
BorderColour = Color4.Black;
@@ -201,7 +202,5 @@ namespace osu.Game.Overlays
}
protected override Container Content => content;
-
- protected override bool OnMouseDown(MouseDownEvent e) => true;
}
}
diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs
index 44713d637d..4015d8e196 100644
--- a/osu.Game/Overlays/WikiOverlay.cs
+++ b/osu.Game/Overlays/WikiOverlay.cs
@@ -7,6 +7,7 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -100,7 +101,12 @@ namespace osu.Game.Overlays
cancellationToken?.Cancel();
request?.Cancel();
- request = new GetWikiRequest(e.NewValue);
+ string[] values = e.NewValue.Split('/', 2);
+
+ if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var language))
+ request = new GetWikiRequest(values[1], language);
+ else
+ request = new GetWikiRequest(e.NewValue);
Loading.Show();
diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
index 9baa252caf..7cf480a11b 100644
--- a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
+++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
@@ -5,8 +5,19 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
{
+ ///
+ /// An interface for s that are updated every frame by a .
+ ///
public interface IUpdatableByPlayfield : IApplicableMod
{
+ ///
+ /// Update this .
+ ///
+ /// The main
+ ///
+ /// This method is called once per frame during gameplay by the main only.
+ /// To access nested s, use .
+ ///
void Update(Playfield playfield);
}
}
diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
index 637d0a872a..fea13cf4b6 100644
--- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
+++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
@@ -29,8 +29,15 @@ namespace osu.Game.Rulesets.Scoring
/// A non-null value if unstable rate could be calculated,
/// and if unstable rate cannot be calculated due to being empty.
///
- public static double? CalculateAverageHitError(this IEnumerable hitEvents) =>
- hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).Average();
+ public static double? CalculateAverageHitError(this IEnumerable hitEvents)
+ {
+ double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
+
+ if (timeOffsets.Length == 0)
+ return null;
+
+ return timeOffsets.Average();
+ }
private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index d0bbf859af..30e71dde1c 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -79,6 +79,11 @@ namespace osu.Game.Rulesets.UI
private readonly List nestedPlayfields = new List();
+ ///
+ /// Whether this is nested in another .
+ ///
+ public bool IsNested { get; private set; }
+
///
/// Whether judgements should be displayed by this and and all nested s.
///
@@ -206,6 +211,8 @@ namespace osu.Game.Rulesets.UI
/// The to add.
protected void AddNested(Playfield otherPlayfield)
{
+ otherPlayfield.IsNested = true;
+
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
@@ -229,7 +236,7 @@ namespace osu.Game.Rulesets.UI
{
base.Update();
- if (mods != null)
+ if (!IsNested && mods != null)
{
foreach (var mod in mods)
{
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
index 9460ec680c..f0ead05280 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
@@ -7,10 +7,15 @@ using System.Linq;
using System.Text;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
+using osu.Game.Extensions;
using osu.Game.IO.Legacy;
+using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA;
+#nullable enable
+
namespace osu.Game.Scoring.Legacy
{
public class LegacyScoreEncoder
@@ -27,15 +32,24 @@ namespace osu.Game.Scoring.Legacy
public const int FIRST_LAZER_VERSION = 30000000;
private readonly Score score;
- private readonly IBeatmap beatmap;
+ private readonly IBeatmap? beatmap;
- public LegacyScoreEncoder(Score score, IBeatmap beatmap)
+ ///
+ /// Create a new score encoder for a specific score.
+ ///
+ /// The score to be encoded.
+ /// The beatmap used to convert frames for the score. May be null if the frames are already s.
+ ///
+ public LegacyScoreEncoder(Score score, IBeatmap? beatmap)
{
this.score = score;
this.beatmap = beatmap;
- if (score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID > 3)
- throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score));
+ if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame))
+ throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap));
+
+ if (!score.ScoreInfo.Ruleset.IsLegacyRuleset())
+ throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score));
}
public void Encode(Stream stream)
@@ -101,11 +115,13 @@ namespace osu.Game.Scoring.Legacy
{
int lastTime = 0;
- foreach (var f in score.Replay.Frames.OfType().Select(f => f.ToLegacy(beatmap)))
+ foreach (var f in score.Replay.Frames)
{
+ var legacyFrame = getLegacyFrame(f);
+
// Rounding because stable could only parse integral values
- int time = (int)Math.Round(f.Time);
- replayData.Append(FormattableString.Invariant($"{time - lastTime}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},"));
+ int time = (int)Math.Round(legacyFrame.Time);
+ replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},"));
lastTime = time;
}
}
@@ -117,6 +133,21 @@ namespace osu.Game.Scoring.Legacy
}
}
+ private LegacyReplayFrame getLegacyFrame(ReplayFrame replayFrame)
+ {
+ switch (replayFrame)
+ {
+ case LegacyReplayFrame legacyFrame:
+ return legacyFrame;
+
+ case IConvertibleReplayFrame convertibleFrame:
+ return convertibleFrame.ToLegacy(beatmap);
+
+ default:
+ throw new ArgumentException(@"Frame could not be converted to legacy frames", nameof(replayFrame));
+ }
+ }
+
private string getHpGraphFormatted()
{
// todo: implement, maybe?
diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs
index c6787a1fb1..2a8435ff47 100644
--- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs
+++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
{
public class EditorMenuBar : OsuMenu
{
- public readonly Bindable Mode = new Bindable();
-
public EditorMenuBar()
: base(Direction.Horizontal, true)
{
@@ -28,25 +25,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
MaskingContainer.CornerRadius = 0;
ItemsContainer.Padding = new MarginPadding { Left = 100 };
BackgroundColour = Color4Extensions.FromHex("111");
-
- ScreenSelectionTabControl tabControl;
- AddRangeInternal(new Drawable[]
- {
- tabControl = new ScreenSelectionTabControl
- {
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- X = -15
- }
- });
-
- Mode.BindTo(tabControl.Current);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- Mode.TriggerChange();
}
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu();
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index c2775ae101..dcb7e3a282 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -89,6 +89,8 @@ namespace osu.Game.Screens.Edit
[Resolved(canBeNull: true)]
private NotificationOverlay notifications { get; set; }
+ public readonly Bindable Mode = new Bindable();
+
public IBindable SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable samplePlaybackDisabled = new Bindable();
@@ -115,8 +117,6 @@ namespace osu.Game.Screens.Edit
[CanBeNull] // Should be non-null once it can support custom rulesets.
private EditorChangeHandler changeHandler;
- private EditorMenuBar menuBar;
-
private DependencyContainer dependencies;
private TestGameplayButton testGameplayButton;
@@ -239,40 +239,49 @@ namespace osu.Game.Screens.Edit
Name = "Top bar",
RelativeSizeAxes = Axes.X,
Height = 40,
- Child = menuBar = new EditorMenuBar
+ Children = new Drawable[]
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- RelativeSizeAxes = Axes.Both,
- Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose },
- Items = new[]
+ new EditorMenuBar
{
- new MenuItem("File")
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ Items = new[]
{
- Items = createFileMenuItems()
- },
- new MenuItem("Edit")
- {
- Items = new[]
+ new MenuItem("File")
{
- undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
- redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo),
- new EditorMenuItemSpacer(),
- cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
- copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
- pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
- }
- },
- new MenuItem("View")
- {
- Items = new MenuItem[]
+ Items = createFileMenuItems()
+ },
+ new MenuItem("Edit")
{
- new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)),
- new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations))
+ Items = new[]
+ {
+ undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
+ redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo),
+ new EditorMenuItemSpacer(),
+ cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
+ copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
+ pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
+ }
+ },
+ new MenuItem("View")
+ {
+ Items = new MenuItem[]
+ {
+ new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)),
+ new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations))
+ }
}
}
- }
- }
+ },
+ new ScreenSelectionTabControl
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ X = -15,
+ Current = Mode,
+ },
+ },
},
new Container
{
@@ -340,14 +349,15 @@ namespace osu.Game.Screens.Edit
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
-
- menuBar.Mode.ValueChanged += onModeChanged;
}
protected override void LoadComplete()
{
base.LoadComplete();
setUpClipboardActionAvailability();
+
+ Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose;
+ Mode.BindValueChanged(onModeChanged, true);
}
///
@@ -517,23 +527,23 @@ namespace osu.Game.Screens.Edit
return true;
case GlobalAction.EditorComposeMode:
- menuBar.Mode.Value = EditorScreenMode.Compose;
+ Mode.Value = EditorScreenMode.Compose;
return true;
case GlobalAction.EditorDesignMode:
- menuBar.Mode.Value = EditorScreenMode.Design;
+ Mode.Value = EditorScreenMode.Design;
return true;
case GlobalAction.EditorTimingMode:
- menuBar.Mode.Value = EditorScreenMode.Timing;
+ Mode.Value = EditorScreenMode.Timing;
return true;
case GlobalAction.EditorSetupMode:
- menuBar.Mode.Value = EditorScreenMode.SongSetup;
+ Mode.Value = EditorScreenMode.SongSetup;
return true;
case GlobalAction.EditorVerifyMode:
- menuBar.Mode.Value = EditorScreenMode.Verify;
+ Mode.Value = EditorScreenMode.Verify;
return true;
case GlobalAction.EditorTestGameplay:
diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
index 200921680e..2b6db5f59e 100644
--- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
@@ -13,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Database;
namespace osu.Game.Screens.Play
{
@@ -43,7 +45,7 @@ namespace osu.Game.Screens.Play
Precision = 0.1,
};
- private double totalAppliedOffset => userOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
+ private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1);
@@ -52,12 +54,21 @@ namespace osu.Game.Screens.Play
private readonly bool startAtGameplayStart;
private readonly double firstHitObjectTime;
- private HardwareCorrectionOffsetClock userOffsetClock;
+ private HardwareCorrectionOffsetClock userGlobalOffsetClock;
+ private HardwareCorrectionOffsetClock userBeatmapOffsetClock;
private HardwareCorrectionOffsetClock platformOffsetClock;
private MasterGameplayClock masterGameplayClock;
private Bindable userAudioOffset;
private double startOffset;
+ private IDisposable beatmapOffsetSubscription;
+
+ [Resolved]
+ private RealmAccess realm { get; set; }
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false)
: base(beatmap.Track)
{
@@ -68,11 +79,33 @@ namespace osu.Game.Screens.Play
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
}
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
+ protected override void LoadComplete()
{
+ base.LoadComplete();
+
userAudioOffset = config.GetBindable(OsuSetting.AudioOffset);
- userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true);
+ userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
+
+ beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
+ {
+ var userSettings = r.Find(beatmap.BeatmapInfo.ID)?.UserSettings;
+
+ if (userSettings == null) // only the case for tests.
+ return null;
+
+ void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
+ {
+ if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
+ updateOffset();
+ }
+
+ updateOffset();
+ userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
+
+ return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
+
+ void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset;
+ });
// sane default provided by ruleset.
startOffset = gameplayStartTime;
@@ -161,9 +194,10 @@ namespace osu.Game.Screens.Play
platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
// the final usable gameplay clock with user-set offsets applied.
- userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust);
+ userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust);
+ userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust);
- return masterGameplayClock = new MasterGameplayClock(userOffsetClock);
+ return masterGameplayClock = new MasterGameplayClock(userBeatmapOffsetClock);
}
///
@@ -209,6 +243,7 @@ namespace osu.Game.Screens.Play
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+ beatmapOffsetSubscription?.Dispose();
removeSourceClockAdjustments();
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index d4b02622d3..86ea412488 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -136,7 +136,11 @@ namespace osu.Game.Screens.Play
public readonly PlayerConfiguration Configuration;
- protected Score Score { get; private set; }
+ ///
+ /// The score for the current play session.
+ /// Available only after the player is loaded.
+ ///
+ public Score Score { get; private set; }
///
/// Create a new player instance.
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 6009c85583..41eb822e39 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -61,6 +61,8 @@ namespace osu.Game.Screens.Play
protected VisualSettings VisualSettings { get; private set; }
+ protected AudioSettings AudioSettings { get; private set; }
+
protected Task LoadTask { get; private set; }
protected Task DisposalTask { get; private set; }
@@ -141,6 +143,8 @@ namespace osu.Game.Screens.Play
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
+ const float padding = 25;
+
InternalChildren = new Drawable[]
{
(content = new LogoTrackingContainer
@@ -156,19 +160,27 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- PlayerSettings = new FillFlowContainer
+ new OsuScrollContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Margin = new MarginPadding(25),
- Children = new PlayerSettingsGroup[]
+ RelativeSizeAxes = Axes.Y,
+ Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
+ Padding = new MarginPadding { Vertical = padding },
+ Masking = false,
+ Child = PlayerSettings = new FillFlowContainer
{
- VisualSettings = new VisualSettings(),
- new InputSettings()
- }
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Padding = new MarginPadding { Horizontal = padding },
+ Children = new PlayerSettingsGroup[]
+ {
+ VisualSettings = new VisualSettings(),
+ AudioSettings = new AudioSettings(),
+ new InputSettings()
+ }
+ },
},
idleTracker = new IdleTracker(750),
}),
@@ -225,6 +237,10 @@ namespace osu.Game.Screens.Play
{
base.OnResuming(last);
+ var lastScore = player.Score;
+
+ AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo;
+
// prepare for a retry.
player = null;
playerConsumed = false;
diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs
new file mode 100644
index 0000000000..32de5333e1
--- /dev/null
+++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Configuration;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play.PlayerSettings
+{
+ public class AudioSettings : PlayerSettingsGroup
+ {
+ public Bindable ReferenceScore { get; } = new Bindable();
+
+ private readonly PlayerCheckbox beatmapHitsoundsToggle;
+
+ public AudioSettings()
+ : base("Audio Settings")
+ {
+ Children = new Drawable[]
+ {
+ beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" },
+ new BeatmapOffsetControl
+ {
+ ReferenceScore = { BindTarget = ReferenceScore },
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
new file mode 100644
index 0000000000..8253c2e38e
--- /dev/null
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -0,0 +1,215 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Localisation;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
+using osuTK;
+
+#nullable enable
+
+namespace osu.Game.Screens.Play.PlayerSettings
+{
+ public class BeatmapOffsetControl : CompositeDrawable
+ {
+ public Bindable ReferenceScore { get; } = new Bindable();
+
+ public BindableDouble Current { get; } = new BindableDouble
+ {
+ Default = 0,
+ Value = 0,
+ MinValue = -50,
+ MaxValue = 50,
+ Precision = 0.1,
+ };
+
+ private readonly FillFlowContainer referenceScoreContainer;
+
+ [Resolved]
+ private RealmAccess realm { get; set; } = null!;
+
+ [Resolved]
+ private IBindable beatmap { get; set; } = null!;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ private double lastPlayAverage;
+ private double lastPlayBeatmapOffset;
+
+ private SettingsButton? useAverageButton;
+
+ private IDisposable? beatmapOffsetSubscription;
+
+ private Task? realmWriteTask;
+
+ public BeatmapOffsetControl()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(10),
+ Children = new Drawable[]
+ {
+ new PlayerSliderBar
+ {
+ KeyboardStep = 5,
+ LabelText = BeatmapOffsetControlStrings.BeatmapOffset,
+ Current = Current,
+ },
+ referenceScoreContainer = new FillFlowContainer
+ {
+ Spacing = new Vector2(10),
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ ReferenceScore.BindValueChanged(scoreChanged, true);
+
+ beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
+ {
+ var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings;
+
+ if (userSettings == null) // only the case for tests.
+ return null;
+
+ Current.Value = userSettings.Offset;
+ userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
+
+ return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
+
+ void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
+ {
+ if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
+ Current.Value = userSettings.Offset;
+ }
+ });
+
+ Current.BindValueChanged(currentChanged);
+ }
+
+ private void currentChanged(ValueChangedEvent offset)
+ {
+ Scheduler.AddOnce(updateOffset);
+
+ void updateOffset()
+ {
+ // ensure the previous write has completed. ignoring performance concerns, if we don't do this, the async writes could be out of sequence.
+ if (realmWriteTask?.IsCompleted == false)
+ {
+ Scheduler.AddOnce(updateOffset);
+ return;
+ }
+
+ if (useAverageButton != null)
+ useAverageButton.Enabled.Value = !Precision.AlmostEquals(Current.Value, lastPlayBeatmapOffset - lastPlayAverage, Current.Precision / 2);
+
+ realmWriteTask = realm.WriteAsync(r =>
+ {
+ var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings;
+
+ if (settings == null) // only the case for tests.
+ return;
+
+ if (settings.Offset == Current.Value)
+ return;
+
+ settings.Offset = Current.Value;
+ });
+ }
+ }
+
+ private void scoreChanged(ValueChangedEvent score)
+ {
+ referenceScoreContainer.Clear();
+
+ if (score.NewValue == null)
+ return;
+
+ if (score.NewValue.Mods.Any(m => !m.UserPlayable))
+ return;
+
+ var hitEvents = score.NewValue.HitEvents;
+
+ if (!(hitEvents.CalculateAverageHitError() is double average))
+ return;
+
+ referenceScoreContainer.Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = BeatmapOffsetControlStrings.PreviousPlay
+ },
+ };
+
+ if (hitEvents.Count < 10)
+ {
+ referenceScoreContainer.AddRange(new Drawable[]
+ {
+ new OsuTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Colour = colours.Red1,
+ Text = BeatmapOffsetControlStrings.PreviousPlayTooShortToUseForCalibration
+ },
+ });
+
+ return;
+ }
+
+ lastPlayAverage = average;
+ lastPlayBeatmapOffset = Current.Value;
+
+ referenceScoreContainer.AddRange(new Drawable[]
+ {
+ new HitEventTimingDistributionGraph(hitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 50,
+ },
+ new AverageHitError(hitEvents),
+ useAverageButton = new SettingsButton
+ {
+ Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
+ Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage
+ },
+ });
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ beatmapOffsetSubscription?.Dispose();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs
index a97078c461..81950efa9e 100644
--- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs
@@ -15,7 +15,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
private readonly PlayerCheckbox showStoryboardToggle;
private readonly PlayerCheckbox beatmapSkinsToggle;
private readonly PlayerCheckbox beatmapColorsToggle;
- private readonly PlayerCheckbox beatmapHitsoundsToggle;
public VisualSettings()
: base("Visual Settings")
@@ -45,7 +44,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" },
beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" },
beatmapColorsToggle = new PlayerCheckbox { LabelText = "Beatmap colours" },
- beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" }
};
}
@@ -57,7 +55,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard);
beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins);
beatmapColorsToggle.Current = config.GetBindable(OsuSetting.BeatmapColours);
- beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds);
}
}
}
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index 824c0072e3..a935ce49eb 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -4,10 +4,10 @@
using System;
using System.Diagnostics;
using osu.Game.Beatmaps;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
-using osu.Game.Rulesets;
using osu.Game.Scoring;
namespace osu.Game.Screens.Play
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play
if (beatmapId <= 0)
return null;
- if (rulesetId < 0 || rulesetId > ILegacyRuleset.MAX_LEGACY_RULESET_ID)
+ if (!Ruleset.Value.IsLegacyRuleset())
return null;
return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash);
diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
index 93885b6e02..b32b11c028 100644
--- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
+++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
@@ -64,10 +64,22 @@ namespace osu.Game.Screens.Ranking.Statistics
// Prevent div-by-0 by enforcing a minimum bin size
binSize = Math.Max(1, binSize);
+ bool roundUp = true;
+
foreach (var e in hitEvents)
{
- int binOffset = (int)Math.Round(e.TimeOffset / binSize, MidpointRounding.AwayFromZero);
- bins[timing_distribution_centre_bin_index + binOffset]++;
+ double binOffset = e.TimeOffset / binSize;
+
+ // .NET's round midpoint handling doesn't provide a behaviour that works amazingly for display
+ // purposes here. We want midpoint rounding to roughly distribute evenly to each adjacent bucket
+ // so the easiest way is to cycle between downwards and upwards rounding as we process events.
+ if (Math.Abs(binOffset - (int)binOffset) == 0.5)
+ {
+ binOffset = (int)binOffset + Math.Sign(binOffset) * (roundUp ? 1 : 0);
+ roundUp = !roundUp;
+ }
+
+ bins[timing_distribution_centre_bin_index + (int)Math.Round(binOffset, MidpointRounding.AwayFromZero)]++;
}
int maxCount = bins.Max();
@@ -160,8 +172,6 @@ namespace osu.Game.Screens.Ranking.Statistics
RelativeSizeAxes = Axes.Both;
- Padding = new MarginPadding { Horizontal = 1 };
-
InternalChild = new Circle
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 907a2c9bda..eb0addd377 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Leaderboards;
@@ -98,6 +99,7 @@ namespace osu.Game.Screens.Select.Leaderboards
protected override APIRequest FetchScores(CancellationToken cancellationToken)
{
var fetchBeatmapInfo = BeatmapInfo;
+ var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
if (fetchBeatmapInfo == null)
{
@@ -117,9 +119,15 @@ namespace osu.Game.Screens.Select.Leaderboards
return null;
}
+ if (!fetchRuleset.IsLegacyRuleset())
+ {
+ SetErrorState(LeaderboardState.RulesetUnavailable);
+ return null;
+ }
+
if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
{
- SetErrorState(LeaderboardState.Unavailable);
+ SetErrorState(LeaderboardState.BeatmapUnavailable);
return null;
}
@@ -137,7 +145,7 @@ namespace osu.Game.Screens.Select.Leaderboards
else if (filterMods)
requestMods = mods.Value;
- var req = new GetScoresRequest(fetchBeatmapInfo, ruleset.Value ?? fetchBeatmapInfo.Ruleset, Scope, requestMods);
+ var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
req.Success += r =>
{
diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
index 935d2756fb..ce9afd650a 100644
--- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
+++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Skinning.Editor
{
public class SkinComponentToolbox : ScrollingToolboxGroup
{
+ public const float WIDTH = 200;
+
public Action RequestPlacement;
private const float component_display_scale = 0.8f;
@@ -41,7 +43,7 @@ namespace osu.Game.Skinning.Editor
: base("Components", height)
{
RelativeSizeAxes = Axes.None;
- Width = 200;
+ Width = WIDTH;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs
index 8052f82c93..ae5cbc95f0 100644
--- a/osu.Game/Skinning/Editor/SkinEditor.cs
+++ b/osu.Game/Skinning/Editor/SkinEditor.cs
@@ -8,14 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Resources.Localisation.Web;
-using osuTK;
+using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Skinning.Editor
{
@@ -57,13 +57,43 @@ namespace osu.Game.Skinning.Editor
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- headerText = new OsuTextFlowContainer
+ new Container
{
- TextAnchor = Anchor.TopCentre,
- Padding = new MarginPadding(20),
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.X
+ Name = "Top bar",
+ RelativeSizeAxes = Axes.X,
+ Depth = float.MinValue,
+ Height = 40,
+ Children = new Drawable[]
+ {
+ new EditorMenuBar
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ Items = new[]
+ {
+ new MenuItem("File")
+ {
+ Items = new[]
+ {
+ new EditorMenuItem("Save", MenuItemType.Standard, Save),
+ new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert),
+ new EditorMenuItemSpacer(),
+ new EditorMenuItem("Exit", MenuItemType.Standard, Hide),
+ },
+ },
+ }
+ },
+ headerText = new OsuTextFlowContainer
+ {
+ TextAnchor = Anchor.TopRight,
+ Padding = new MarginPadding(5),
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ },
+ },
},
new GridContainer
{
@@ -89,46 +119,6 @@ namespace osu.Game.Skinning.Editor
Children = new Drawable[]
{
new SkinBlueprintContainer(targetScreen),
- new TriangleButton
- {
- Margin = new MarginPadding(10),
- Text = CommonStrings.ButtonsClose,
- Width = 100,
- Action = Hide,
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- Spacing = new Vector2(5),
- Padding = new MarginPadding
- {
- Top = 10,
- Left = 10,
- },
- Margin = new MarginPadding
- {
- Right = 10,
- Bottom = 10,
- },
- Children = new Drawable[]
- {
- new TriangleButton
- {
- Text = "Save Changes",
- Width = 140,
- Action = Save,
- },
- new DangerousTriangleButton
- {
- Text = "Revert to default",
- Width = 140,
- Action = revert,
- },
- }
- },
}
},
}
@@ -161,7 +151,7 @@ namespace osu.Game.Skinning.Editor
{
headerText.Clear();
- headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 24));
+ headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 16));
headerText.NewParagraph();
headerText.AddText("Currently editing ", cp =>
{
diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs
index 86854ab6ff..61c363b019 100644
--- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs
+++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs
@@ -5,6 +5,7 @@ using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
@@ -100,30 +101,14 @@ namespace osu.Game.Skinning.Editor
{
if (visibility.NewValue == Visibility.Visible)
{
- updateMasking();
- target.AllowScaling = false;
- target.RelativePositionAxes = Axes.Both;
-
- target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
- target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
+ target.SetCustomRect(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true);
}
else
{
- target.AllowScaling = true;
-
- target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => updateMasking());
- target.MoveToX(0f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
+ target.SetCustomRect(null);
}
}
- private void updateMasking()
- {
- if (skinEditor == null)
- return;
-
- target.Masking = skinEditor.State.Value == Visibility.Visible;
- }
-
public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index a2739c527b..d86fbc693e 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 14824a5af6..c37692f0d8 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -62,7 +62,7 @@
-
+