1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 10:53:21 +08:00

Merge branch 'master' into editor-storyboard-display-2

This commit is contained in:
Bartłomiej Dach 2025-01-08 15:46:08 +01:00
commit bcd35c8899
No known key found for this signature in database
11 changed files with 284 additions and 73 deletions

View File

@ -56,14 +56,13 @@ namespace osu.Desktop.Windows
/// Installs file and URI associations. /// Installs file and URI associations.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised. /// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks> /// </remarks>
public static void InstallAssociations() public static void InstallAssociations()
{ {
try try
{ {
updateAssociations(); updateAssociations();
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@ -76,17 +75,13 @@ namespace osu.Desktop.Windows
/// Updates associations with latest definitions. /// Updates associations with latest definitions.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised. /// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks> /// </remarks>
public static void UpdateAssociations() public static void UpdateAssociations()
{ {
try try
{ {
updateAssociations(); 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(); NotifyShellUpdate();
} }
catch (Exception e) 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 try
{ {
updateDescriptions(localisationManager); foreach (var association in file_associations)
association.LocaliseDescription(localisationManager);
foreach (var association in uri_associations)
association.LocaliseDescription(localisationManager);
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@ -140,17 +141,6 @@ namespace osu.Desktop.Windows
association.Install(); 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 #region Native interop
[DllImport("Shell32.dll")] [DllImport("Shell32.dll")]
@ -174,9 +164,20 @@ namespace osu.Desktop.Windows
#endregion #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;
}
/// <summary> /// <summary>
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key /// 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 // register a program id for the given extension
using (var programKey = classes.CreateSubKey(programId)) using (var programKey = classes.CreateSubKey(programId))
{ {
programKey.SetValue(null, description.ToString());
using (var defaultIconKey = programKey.CreateSubKey(default_icon)) using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath); defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND)) using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
} }
using (var extensionKey = classes.CreateSubKey(Extension)) using (var extensionKey = classes.CreateSubKey(extension))
{ {
// set ourselves as the default program // set ourselves as the default program
extensionKey.SetValue(null, programId); 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); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; if (classes == null) return;
using (var programKey = classes.OpenSubKey(programId, true)) using (var programKey = classes.OpenSubKey(programId, true))
programKey?.SetValue(null, description); programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
} }
/// <summary> /// <summary>
@ -225,7 +228,7 @@ namespace osu.Desktop.Windows
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; 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 // 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 // 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
{ {
/// <summary> /// <summary>
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler." /// "The <c>URL Protocol</c> 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). /// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
/// </summary> /// </summary>
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;
}
/// <summary> /// <summary>
/// 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). /// 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); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; 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)) using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath); defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND)) using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); 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); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; if (classes == null) return;
using (var protocolKey = classes.OpenSubKey(Protocol, true)) using (var protocolKey = classes.OpenSubKey(protocol, true))
protocolKey?.SetValue(null, $@"URL:{description}"); protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
} }
public void Uninstall() public void Uninstall()
{ {
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false); classes?.DeleteSubKeyTree(protocol, throwOnMissingSubKey: false);
} }
} }
} }

View File

@ -539,5 +539,85 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX)); 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));
});
}
} }
} }

View File

@ -18,6 +18,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -102,6 +103,35 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); 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<PlaybackControl.PlaybackTabControl.PlaybackTabItem>().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<TestGameplayButton>().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(2000)] // chosen to be after last object in the map
[TestCase(22000)] // chosen to be in the middle of the last spinner [TestCase(22000)] // chosen to be in the middle of the last spinner
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd) public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
public readonly Drawable Background; public readonly Drawable Background;
private readonly Container content; private readonly Container content;
@ -45,10 +42,9 @@ namespace osu.Game.Screens.Edit.Components
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
Beatmap.BindTo(beatmap); Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
} }
} }
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
@ -75,7 +76,7 @@ namespace osu.Game.Screens.Edit.Components
} }
}; };
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true); editorClock.AudioAdjustments.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment);
if (editor != null) if (editor != null)
currentScreenMode.BindTo(editor.Mode); currentScreenMode.BindTo(editor.Mode);
@ -105,7 +106,8 @@ namespace osu.Game.Screens.Edit.Components
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment); if (editorClock.IsNotNull())
editorClock.AudioAdjustments.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment);
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
@ -148,7 +150,7 @@ namespace osu.Game.Screens.Edit.Components
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
} }
private partial class PlaybackTabControl : OsuTabControl<double> public partial class PlaybackTabControl : OsuTabControl<double>
{ {
private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };

View File

@ -3,8 +3,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osuTK; using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -26,7 +26,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
[Resolved] [Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } = null!; protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
protected readonly IBindable<Track> Track = new Bindable<Track>(); [Resolved]
private EditorClock editorClock { get; set; } = null!;
private readonly Container<T> content; private readonly Container<T> content;
@ -35,22 +36,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
public TimelinePart(Container<T>? content = null) public TimelinePart(Container<T>? content = null)
{ {
AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both }); AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both });
beatmap.ValueChanged += _ =>
{
updateRelativeChildSize();
};
Track.ValueChanged += _ => updateRelativeChildSize();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
this.beatmap.BindTo(beatmap); this.beatmap.BindTo(beatmap);
LoadBeatmap(EditorBeatmap); LoadBeatmap(EditorBeatmap);
Track.BindTo(clock.Track); this.beatmap.ValueChanged += _ => updateRelativeChildSize();
editorClock.TrackChanged += updateRelativeChildSize;
updateRelativeChildSize();
} }
private void updateRelativeChildSize() private void updateRelativeChildSize()
@ -68,5 +64,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
content.Clear(); content.Clear();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorClock.IsNotNull())
editorClock.TrackChanged -= updateRelativeChildSize;
}
} }
} }

View File

@ -3,9 +3,9 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -49,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!; private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
/// <summary> /// <summary>
/// The timeline's scroll position in the last frame. /// The timeline's scroll position in the last frame.
/// </summary> /// </summary>
@ -86,8 +89,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private double trackLengthForZoom; private double trackLengthForZoom;
private readonly IBindable<Track> track = new Bindable<Track>();
public Timeline(Drawable userContent) public Timeline(Drawable userContent)
{ {
this.userContent = userContent; this.userContent = userContent;
@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) private void load(OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config)
{ {
CentreMarker centreMarker; CentreMarker centreMarker;
@ -150,16 +151,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
controlPointsVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTimingChanges); controlPointsVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTimingChanges);
ticksVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks); ticksVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks);
track.BindTo(editorClock.Track); editorClock.TrackChanged += updateWaveform;
track.BindValueChanged(_ => updateWaveform();
{
waveform.Waveform = beatmap.Value.Waveform;
Scheduler.AddOnce(applyVisualOffset, beatmap);
}, true);
Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom); Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom);
} }
private void updateWaveform()
{
waveform.Waveform = beatmap.Value.Waveform;
Scheduler.AddOnce(applyVisualOffset, beatmap);
}
private void applyVisualOffset(IBindable<WorkingBeatmap> beatmap) private void applyVisualOffset(IBindable<WorkingBeatmap> beatmap)
{ {
waveform.RelativePositionAxes = Axes.X; waveform.RelativePositionAxes = Axes.X;
@ -334,5 +337,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorClock.IsNotNull())
editorClock.TrackChanged -= updateWaveform;
}
} }
} }

View File

@ -871,6 +871,7 @@ namespace osu.Game.Screens.Edit
{ {
base.OnResuming(e); base.OnResuming(e);
setUpBackground(); setUpBackground();
clock.BindAdjustments();
} }
private void setUpBackground() private void setUpBackground()
@ -929,6 +930,10 @@ namespace osu.Game.Screens.Edit
base.OnSuspending(e); base.OnSuspending(e);
clock.Stop(); clock.Stop();
refetchBeatmap(); refetchBeatmap();
// unfortunately ordering matters here.
// this unbind MUST happen after `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change,
// which causes `EditorClock` to reload the track and automatically reapply adjustments to it.
clock.UnbindAdjustments();
} }
private void refetchBeatmap() private void refetchBeatmap()

View File

@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit
rulesetBeatmapProcessor?.PostProcess(); rulesetBeatmapProcessor?.PostProcess();
autoGenerateBreaks(); autoGenerateBreaks();
ensureNewComboAfterBreaks();
} }
private void autoGenerateBreaks() private void autoGenerateBreaks()
@ -100,5 +101,40 @@ namespace osu.Game.Screens.Edit
Beatmap.Breaks.Add(breakPeriod); Beatmap.Breaks.Add(breakPeriod);
} }
} }
private void ensureNewComboAfterBreaks()
{
var breakEnds = Beatmap.Breaks.Select(b => b.EndTime).OrderBy(t => t).ToList();
if (breakEnds.Count == 0)
return;
int currentBreak = 0;
IHasComboInformation? lastObj = null;
bool comboInformationUpdateRequired = false;
foreach (var hitObject in Beatmap.HitObjects)
{
if (hitObject is not IHasComboInformation hasCombo)
continue;
if (currentBreak < breakEnds.Count && hitObject.StartTime >= breakEnds[currentBreak])
{
if (!hasCombo.NewCombo)
{
hasCombo.NewCombo = true;
comboInformationUpdateRequired = true;
}
currentBreak += 1;
}
if (comboInformationUpdateRequired)
hasCombo.UpdateComboInformation(lastObj);
lastObj = hasCombo;
}
}
} }
} }

View File

@ -6,6 +6,8 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -23,12 +25,15 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{ {
public IBindable<Track> Track => track; [CanBeNull]
public event Action TrackChanged;
private readonly Bindable<Track> track = new Bindable<Track>(); private readonly Bindable<Track> track = new Bindable<Track>();
public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000; public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000;
public AudioAdjustments AudioAdjustments { get; } = new AudioAdjustments();
public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo; public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo;
public IBeatmap Beatmap { get; set; } public IBeatmap Beatmap { get; set; }
@ -56,6 +61,8 @@ namespace osu.Game.Screens.Edit
underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true); underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true);
AddInternal(underlyingClock); AddInternal(underlyingClock);
track.BindValueChanged(_ => TrackChanged?.Invoke());
} }
/// <summary> /// <summary>
@ -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 double IAdjustableClock.Rate
{ {
@ -231,8 +247,12 @@ namespace osu.Game.Screens.Edit
public void ChangeSource(IClock source) public void ChangeSource(IClock source)
{ {
UnbindAdjustments();
track.Value = source as Track; track.Value = source as Track;
underlyingClock.ChangeSource(source); underlyingClock.ChangeSource(source);
BindAdjustments();
} }
public IClock Source => underlyingClock.Source; public IClock Source => underlyingClock.Source;

View File

@ -4,8 +4,8 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -305,7 +305,8 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!; private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private readonly IBindable<Track> track = new Bindable<Track>(); [Resolved]
private EditorClock editorClock { get; set; } = null!;
public WaveformRow(bool isMainRow) public WaveformRow(bool isMainRow)
{ {
@ -313,7 +314,7 @@ namespace osu.Game.Screens.Edit.Timing
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(EditorClock clock) private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -343,13 +344,16 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Content2 Colour = colourProvider.Content2
} }
}; };
track.BindTo(clock.Track);
} }
protected override void LoadComplete() 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(); } public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
@ -363,6 +367,14 @@ namespace osu.Game.Screens.Edit.Timing
get => waveformGraph.X; get => waveformGraph.X;
set => waveformGraph.X = value; set => waveformGraph.X = value;
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorClock.IsNotNull())
editorClock.TrackChanged -= updateWaveform;
}
} }
} }
} }