diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs
index 6f53c65ca9..43c3e5a947 100644
--- a/osu.Desktop/Windows/WindowsAssociationManager.cs
+++ b/osu.Desktop/Windows/WindowsAssociationManager.cs
@@ -56,14 +56,13 @@ namespace osu.Desktop.Windows
/// Installs file and URI associations.
///
///
- /// Call in a timely fashion to keep descriptions up-to-date and localised.
+ /// Call in a timely fashion to keep descriptions up-to-date and localised.
///
public static void InstallAssociations()
{
try
{
updateAssociations();
- updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate();
}
catch (Exception e)
@@ -76,17 +75,13 @@ namespace osu.Desktop.Windows
/// Updates associations with latest definitions.
///
///
- /// Call in a timely fashion to keep descriptions up-to-date and localised.
+ /// Call in a timely fashion to keep descriptions up-to-date and localised.
///
public static void UpdateAssociations()
{
try
{
updateAssociations();
-
- // TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
- updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
-
NotifyShellUpdate();
}
catch (Exception e)
@@ -95,11 +90,17 @@ namespace osu.Desktop.Windows
}
}
- public static void UpdateDescriptions(LocalisationManager localisationManager)
+ // TODO: call this sometime.
+ public static void LocaliseDescriptions(LocalisationManager localisationManager)
{
try
{
- updateDescriptions(localisationManager);
+ foreach (var association in file_associations)
+ association.LocaliseDescription(localisationManager);
+
+ foreach (var association in uri_associations)
+ association.LocaliseDescription(localisationManager);
+
NotifyShellUpdate();
}
catch (Exception e)
@@ -140,17 +141,6 @@ namespace osu.Desktop.Windows
association.Install();
}
- private static void updateDescriptions(LocalisationManager? localisation)
- {
- foreach (var association in file_associations)
- association.UpdateDescription(getLocalisedString(association.Description));
-
- foreach (var association in uri_associations)
- association.UpdateDescription(getLocalisedString(association.Description));
-
- string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
- }
-
#region Native interop
[DllImport("Shell32.dll")]
@@ -174,9 +164,20 @@ namespace osu.Desktop.Windows
#endregion
- private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
+ private class FileAssociation
{
- private string programId => $@"{program_id_prefix}{Extension}";
+ private string programId => $@"{program_id_prefix}{extension}";
+
+ private string extension { get; }
+ private LocalisableString description { get; }
+ private string iconPath { get; }
+
+ public FileAssociation(string extension, LocalisableString description, string iconPath)
+ {
+ this.extension = extension;
+ this.description = description;
+ this.iconPath = iconPath;
+ }
///
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
@@ -189,14 +190,16 @@ namespace osu.Desktop.Windows
// register a program id for the given extension
using (var programKey = classes.CreateSubKey(programId))
{
+ programKey.SetValue(null, description.ToString());
+
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
- defaultIconKey.SetValue(null, IconPath);
+ defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
}
- using (var extensionKey = classes.CreateSubKey(Extension))
+ using (var extensionKey = classes.CreateSubKey(extension))
{
// set ourselves as the default program
extensionKey.SetValue(null, programId);
@@ -208,13 +211,13 @@ namespace osu.Desktop.Windows
}
}
- public void UpdateDescription(string description)
+ public void LocaliseDescription(LocalisationManager localisationManager)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var programKey = classes.OpenSubKey(programId, true))
- programKey?.SetValue(null, description);
+ programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
}
///
@@ -225,7 +228,7 @@ namespace osu.Desktop.Windows
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
- using (var extensionKey = classes.OpenSubKey(Extension, true))
+ using (var extensionKey = classes.OpenSubKey(extension, true))
{
// clear our default association so that Explorer doesn't show the raw programId to users
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
@@ -240,13 +243,24 @@ namespace osu.Desktop.Windows
}
}
- private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
+ private class UriAssociation
{
///
/// "The URL Protocol string value indicates that this key declares a custom pluggable protocol handler."
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
///
- public const string URL_PROTOCOL = @"URL Protocol";
+ private const string url_protocol = @"URL Protocol";
+
+ private string protocol { get; }
+ private LocalisableString description { get; }
+ private string iconPath { get; }
+
+ public UriAssociation(string protocol, LocalisableString description, string iconPath)
+ {
+ this.protocol = protocol;
+ this.description = description;
+ this.iconPath = iconPath;
+ }
///
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
@@ -256,31 +270,32 @@ namespace osu.Desktop.Windows
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
- using (var protocolKey = classes.CreateSubKey(Protocol))
+ using (var protocolKey = classes.CreateSubKey(protocol))
{
- protocolKey.SetValue(URL_PROTOCOL, string.Empty);
+ protocolKey.SetValue(null, $@"URL:{description}");
+ protocolKey.SetValue(url_protocol, string.Empty);
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
- defaultIconKey.SetValue(null, IconPath);
+ defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
}
}
- public void UpdateDescription(string description)
+ public void LocaliseDescription(LocalisationManager localisationManager)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
- using (var protocolKey = classes.OpenSubKey(Protocol, true))
- protocolKey?.SetValue(null, $@"URL:{description}");
+ using (var protocolKey = classes.OpenSubKey(protocol, true))
+ protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
}
public void Uninstall()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
- classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
+ classes?.DeleteSubKeyTree(protocol, throwOnMissingSubKey: false);
}
}
}
diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
index bbcf6aac2c..c625346645 100644
--- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
+++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
@@ -539,5 +539,85 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
});
}
+
+ [Test]
+ public void TestPuttingObjectBetweenBreakEndAndAnotherObjectForcesNewCombo()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ Difficulty =
+ {
+ ApproachRate = 10,
+ },
+ HitObjects =
+ {
+ new HitCircle { StartTime = 1000, NewCombo = true },
+ new HitCircle { StartTime = 4500 },
+ new HitCircle { StartTime = 5000, NewCombo = true },
+ },
+ Breaks =
+ {
+ new BreakPeriod(2000, 4000),
+ }
+ });
+
+ foreach (var ho in beatmap.HitObjects)
+ ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
+ Assert.That(((HitCircle)beatmap.HitObjects[2]).NewCombo, Is.True);
+
+ Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
+ Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
+ Assert.That(((HitCircle)beatmap.HitObjects[2]).ComboIndex, Is.EqualTo(3));
+ });
+ }
+
+ [Test]
+ public void TestAutomaticallyInsertedBreakForcesNewCombo()
+ {
+ var controlPoints = new ControlPointInfo();
+ controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
+ var beatmap = new EditorBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
+ Difficulty =
+ {
+ ApproachRate = 10,
+ },
+ HitObjects =
+ {
+ new HitCircle { StartTime = 1000, NewCombo = true },
+ new HitCircle { StartTime = 5000 },
+ },
+ });
+
+ foreach (var ho in beatmap.HitObjects)
+ ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
+
+ var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
+ beatmapProcessor.PreProcess();
+ beatmapProcessor.PostProcess();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
+ Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
+
+ Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
+ Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
+ });
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index 60781d6f0a..f65a3e67e8 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -18,6 +18,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Play;
@@ -102,6 +103,35 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
}
+ [Test]
+ public void TestGameplayTestResetsPlaybackSpeedAdjustment()
+ {
+ AddStep("start track", () => EditorClock.Start());
+ AddStep("change playback speed", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
+
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+ AddAssert("editor track stopped", () => !EditorClock.IsRunning);
+ AddAssert("track playback rate is 1x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1));
+
+ AddStep("exit player", () => editorPlayer.Exit());
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
+ }
+
[TestCase(2000)] // chosen to be after last object in the map
[TestCase(22000)] // chosen to be in the middle of the last spinner
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)
diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
index da71457004..37337bc79f 100644
--- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
+++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components
protected readonly IBindable Beatmap = new Bindable();
- protected readonly IBindable
public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
- public IBindable Track => track;
+ [CanBeNull]
+ public event Action TrackChanged;
private readonly Bindable track = new Bindable();
public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000;
+ public AudioAdjustments AudioAdjustments { get; } = new AudioAdjustments();
+
public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo;
public IBeatmap Beatmap { get; set; }
@@ -56,6 +61,8 @@ namespace osu.Game.Screens.Edit
underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true);
AddInternal(underlyingClock);
+
+ track.BindValueChanged(_ => TrackChanged?.Invoke());
}
///
@@ -208,7 +215,16 @@ namespace osu.Game.Screens.Edit
}
}
- public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments();
+ public void BindAdjustments() => track.Value?.BindAdjustments(AudioAdjustments);
+
+ public void UnbindAdjustments() => track.Value?.UnbindAdjustments(AudioAdjustments);
+
+ public void ResetSpeedAdjustments()
+ {
+ AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Frequency);
+ AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Tempo);
+ underlyingClock.ResetSpeedAdjustments();
+ }
double IAdjustableClock.Rate
{
@@ -231,8 +247,12 @@ namespace osu.Game.Screens.Edit
public void ChangeSource(IClock source)
{
+ UnbindAdjustments();
+
track.Value = source as Track;
underlyingClock.ChangeSource(source);
+
+ BindAdjustments();
}
public IClock Source => underlyingClock.Source;
diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
index 45213b7bdb..2df2dd7c5b 100644
--- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
+++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
@@ -4,8 +4,8 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
@@ -305,7 +305,8 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private IBindable beatmap { get; set; } = null!;
- private readonly IBindable track = new Bindable();
+ [Resolved]
+ private EditorClock editorClock { get; set; } = null!;
public WaveformRow(bool isMainRow)
{
@@ -313,7 +314,7 @@ namespace osu.Game.Screens.Edit.Timing
}
[BackgroundDependencyLoader]
- private void load(EditorClock clock)
+ private void load()
{
InternalChildren = new Drawable[]
{
@@ -343,13 +344,16 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Content2
}
};
-
- track.BindTo(clock.Track);
}
protected override void LoadComplete()
{
- track.ValueChanged += _ => waveformGraph.Waveform = beatmap.Value.Waveform;
+ editorClock.TrackChanged += updateWaveform;
+ }
+
+ private void updateWaveform()
+ {
+ waveformGraph.Waveform = beatmap.Value.Waveform;
}
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
@@ -363,6 +367,14 @@ namespace osu.Game.Screens.Edit.Timing
get => waveformGraph.X;
set => waveformGraph.X = value;
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (editorClock.IsNotNull())
+ editorClock.TrackChanged -= updateWaveform;
+ }
}
}
}