mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:07:52 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
624bb43eaf
2
.github/workflows/sentry-release.yml
vendored
2
.github/workflows/sentry-release.yml
vendored
@ -23,4 +23,4 @@ jobs:
|
|||||||
SENTRY_URL: https://sentry.ppy.sh/
|
SENTRY_URL: https://sentry.ppy.sh/
|
||||||
with:
|
with:
|
||||||
environment: production
|
environment: production
|
||||||
version: ${{ github.ref }}
|
version: osu@${{ github.ref_name }}
|
||||||
|
@ -77,6 +77,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
|
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoop()
|
||||||
|
{
|
||||||
|
AddStep("do nothing", () => { });
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTapThenReset()
|
public void TestTapThenReset()
|
||||||
{
|
{
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Timing;
|
using osu.Game.Screens.Edit.Timing;
|
||||||
|
using osu.Game.Screens.Edit.Timing.RowAttributes;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||||
|
|
||||||
|
private TimingScreen timingScreen;
|
||||||
|
|
||||||
protected override bool ScrollUsingMouseWheel => false;
|
protected override bool ScrollUsingMouseWheel => false;
|
||||||
|
|
||||||
public TestSceneTimingScreen()
|
public TestSceneTimingScreen()
|
||||||
@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||||
Beatmap.Disabled = true;
|
Beatmap.Disabled = true;
|
||||||
|
|
||||||
Child = new TimingScreen
|
Child = timingScreen = new TimingScreen
|
||||||
{
|
{
|
||||||
State = { Value = Visibility.Visible },
|
State = { Value = Visibility.Visible },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("Stop clock", () => Clock.Stop());
|
||||||
|
|
||||||
|
AddUntilStep("wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTrackingCurrentTimeWhileRunning()
|
||||||
|
{
|
||||||
|
AddStep("Select first effect point", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||||
|
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
|
||||||
|
|
||||||
|
AddStep("Seek to just before next point", () => Clock.Seek(69000));
|
||||||
|
AddStep("Start clock", () => Clock.Start());
|
||||||
|
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTrackingCurrentTimeWhilePaused()
|
||||||
|
{
|
||||||
|
AddStep("Select first effect point", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||||
|
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
|
||||||
|
|
||||||
|
AddStep("Seek to later", () => Clock.Seek(80000));
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
Beatmap.Disabled = false;
|
Beatmap.Disabled = false;
|
||||||
|
@ -469,6 +469,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
chatOverlay.Show();
|
chatOverlay.Show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||||
|
|
||||||
waitForChannel1Visible();
|
waitForChannel1Visible();
|
||||||
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
|
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
|
||||||
waitForChannel2Visible();
|
waitForChannel2Visible();
|
||||||
|
@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
AddUntilStep("wait for scores loaded", () =>
|
AddUntilStep("wait for scores loaded", () =>
|
||||||
requestComplete
|
requestComplete
|
||||||
|
// request handler may need to fire more than once to get scores.
|
||||||
|
&& totalCount > 0
|
||||||
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
||||||
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
||||||
AddWaitStep("wait for display", 5);
|
AddWaitStep("wait for display", 5);
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performLookup()
|
private async Task performLookup()
|
||||||
{
|
{
|
||||||
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
|
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
|
||||||
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
|
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
|
||||||
@ -127,7 +127,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
// rather than queueing, we maintain our own single-threaded request stream.
|
// rather than queueing, we maintain our own single-threaded request stream.
|
||||||
// todo: we probably want retry logic here.
|
// todo: we probably want retry logic here.
|
||||||
api.Perform(request);
|
await api.PerformAsync(request).ConfigureAwait(false);
|
||||||
|
|
||||||
finishPendingTask();
|
finishPendingTask();
|
||||||
|
|
||||||
|
@ -63,12 +63,13 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public virtual void Queue(APIRequest request)
|
public virtual void Queue(APIRequest request)
|
||||||
{
|
{
|
||||||
if (HandleRequest?.Invoke(request) != true)
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
// this will fail due to not receiving an APIAccess, and trigger a failure on the request.
|
if (HandleRequest?.Invoke(request) != true)
|
||||||
// this is intended - any request in testing that needs non-failures should use HandleRequest.
|
{
|
||||||
request.Perform(this);
|
request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request."));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
|
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
|
||||||
|
@ -2,13 +2,16 @@
|
|||||||
// 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.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Effects;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit
|
namespace osu.Game.Screens.Edit
|
||||||
{
|
{
|
||||||
@ -26,6 +29,14 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
Height = 60;
|
Height = 60;
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Colour = Color4.Black.Opacity(0.2f),
|
||||||
|
Type = EdgeEffectType.Shadow,
|
||||||
|
Radius = 10f,
|
||||||
|
};
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
new Box
|
new Box
|
||||||
|
@ -99,6 +99,15 @@ namespace osu.Game.Screens.Edit
|
|||||||
colourSelected = colours.Colour3;
|
colourSelected = colours.Colour3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// Reduce flicker of rows when offset is being changed rapidly.
|
||||||
|
// Probably need to reconsider this.
|
||||||
|
FinishTransforms(true);
|
||||||
|
}
|
||||||
|
|
||||||
private bool selected;
|
private bool selected;
|
||||||
|
|
||||||
public bool Selected
|
public bool Selected
|
||||||
|
@ -61,6 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
selectedGroup.BindValueChanged(group =>
|
selectedGroup.BindValueChanged(group =>
|
||||||
{
|
{
|
||||||
|
// TODO: This should scroll the selected row into view.
|
||||||
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
|
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
@ -31,18 +31,33 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
kiai.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
omitBarLine.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
|
||||||
|
void saveChanges()
|
||||||
|
{
|
||||||
|
if (!isRebinding) ChangeHandler?.SaveState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isRebinding;
|
||||||
|
|
||||||
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
|
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
|
||||||
{
|
{
|
||||||
if (point.NewValue != null)
|
if (point.NewValue != null)
|
||||||
{
|
{
|
||||||
|
isRebinding = true;
|
||||||
|
|
||||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||||
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
|
||||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||||
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
|
||||||
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
|
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
|
||||||
scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
isRebinding = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.LocalisationExtensions;
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
@ -31,12 +33,16 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private IAdjustableClock metronomeClock;
|
private IAdjustableClock metronomeClock;
|
||||||
|
|
||||||
|
private Sample clunk;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OverlayColourProvider overlayColourProvider { get; set; }
|
private OverlayColourProvider overlayColourProvider { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(AudioManager audio)
|
||||||
{
|
{
|
||||||
|
clunk = audio.Samples.Get(@"Multiplayer/countdown-tick");
|
||||||
|
|
||||||
const float taper = 25;
|
const float taper = 25;
|
||||||
const float swing_vertical_offset = -23;
|
const float swing_vertical_offset = -23;
|
||||||
const float lower_cover_height = 32;
|
const float lower_cover_height = 32;
|
||||||
@ -269,8 +275,21 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
|
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
|
||||||
{
|
{
|
||||||
using (stick.BeginDelayedSequence(beatLength / 2))
|
using (BeginDelayedSequence(beatLength / 2))
|
||||||
|
{
|
||||||
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
var channel = clunk?.GetChannel();
|
||||||
|
|
||||||
|
if (channel != null)
|
||||||
|
{
|
||||||
|
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
|
||||||
|
channel.Play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private EditorClock editorClock { get; set; }
|
private EditorClock editorClock { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap beatmap { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
||||||
|
|
||||||
@ -45,6 +49,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
{
|
{
|
||||||
new Dimension(GridSizeMode.Absolute, 200),
|
new Dimension(GridSizeMode.Absolute, 200),
|
||||||
new Dimension(GridSizeMode.Absolute, 60),
|
new Dimension(GridSizeMode.Absolute, 60),
|
||||||
|
new Dimension(GridSizeMode.Absolute, 60),
|
||||||
},
|
},
|
||||||
Content = new[]
|
Content = new[]
|
||||||
{
|
{
|
||||||
@ -77,7 +82,36 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding(10),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new TimingAdjustButton(1)
|
||||||
|
{
|
||||||
|
Text = "Offset",
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.48f,
|
||||||
|
Height = 50,
|
||||||
|
Action = adjustOffset,
|
||||||
|
},
|
||||||
|
new TimingAdjustButton(0.1)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Text = "BPM",
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.48f,
|
||||||
|
Height = 50,
|
||||||
|
Action = adjustBpm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
@ -113,6 +147,35 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void adjustOffset(double adjust)
|
||||||
|
{
|
||||||
|
// VERY TEMPORARY
|
||||||
|
var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
|
||||||
|
|
||||||
|
beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
|
||||||
|
|
||||||
|
double newOffset = selectedGroup.Value.Time + adjust;
|
||||||
|
|
||||||
|
foreach (var cp in currentGroupItems)
|
||||||
|
beatmap.ControlPointInfo.Add(newOffset, cp);
|
||||||
|
|
||||||
|
// the control point might not necessarily exist yet, if currentGroupItems was empty.
|
||||||
|
selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
|
||||||
|
|
||||||
|
if (!editorClock.IsRunning)
|
||||||
|
editorClock.Seek(newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustBpm(double adjust)
|
||||||
|
{
|
||||||
|
var timing = selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (timing == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
timing.BeatLength = 60000 / (timing.BPM + adjust);
|
||||||
|
}
|
||||||
|
|
||||||
private void tap()
|
private void tap()
|
||||||
{
|
{
|
||||||
editorClock.Seek(selectedGroup.Value.Time);
|
editorClock.Seek(selectedGroup.Value.Time);
|
||||||
|
254
osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs
Normal file
254
osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Timing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A button with variable constant output based on hold position and length.
|
||||||
|
/// </summary>
|
||||||
|
public class TimingAdjustButton : CompositeDrawable
|
||||||
|
{
|
||||||
|
public Action<double> Action;
|
||||||
|
|
||||||
|
private readonly double adjustAmount;
|
||||||
|
private ScheduledDelegate adjustDelegate;
|
||||||
|
|
||||||
|
private const int max_multiplier = 10;
|
||||||
|
|
||||||
|
private const int adjust_levels = 4;
|
||||||
|
|
||||||
|
private const double initial_delay = 300;
|
||||||
|
|
||||||
|
private const double minimum_delay = 80;
|
||||||
|
|
||||||
|
public Container Content { get; set; }
|
||||||
|
|
||||||
|
private double adjustDelay = initial_delay;
|
||||||
|
|
||||||
|
private readonly Box background;
|
||||||
|
|
||||||
|
private readonly OsuSpriteText text;
|
||||||
|
|
||||||
|
private Sample sample;
|
||||||
|
|
||||||
|
public LocalisableString Text
|
||||||
|
{
|
||||||
|
get => text.Text;
|
||||||
|
set => text.Text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; }
|
||||||
|
|
||||||
|
public TimingAdjustButton(double adjustAmount)
|
||||||
|
{
|
||||||
|
this.adjustAmount = adjustAmount;
|
||||||
|
|
||||||
|
CornerRadius = 5;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
AddInternal(Content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Depth = float.MaxValue
|
||||||
|
},
|
||||||
|
text = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
|
||||||
|
Padding = new MarginPadding(5),
|
||||||
|
Depth = float.MinValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
sample = audio.Samples.Get(@"UI/notch-tick");
|
||||||
|
|
||||||
|
background.Colour = colourProvider.Background3;
|
||||||
|
|
||||||
|
for (int i = 1; i <= adjust_levels; i++)
|
||||||
|
{
|
||||||
|
Content.Add(new IncrementBox(i, adjustAmount));
|
||||||
|
Content.Add(new IncrementBox(-i, adjustAmount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
beginRepeat();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
|
{
|
||||||
|
adjustDelegate?.Cancel();
|
||||||
|
base.OnMouseUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginRepeat()
|
||||||
|
{
|
||||||
|
adjustDelegate?.Cancel();
|
||||||
|
|
||||||
|
adjustDelay = initial_delay;
|
||||||
|
adjustNext();
|
||||||
|
|
||||||
|
void adjustNext()
|
||||||
|
{
|
||||||
|
var hoveredBox = Content.OfType<IncrementBox>().FirstOrDefault(d => d.IsHovered);
|
||||||
|
|
||||||
|
if (hoveredBox != null)
|
||||||
|
{
|
||||||
|
Action(adjustAmount * hoveredBox.Multiplier);
|
||||||
|
|
||||||
|
adjustDelay = Math.Max(minimum_delay, adjustDelay * 0.9f);
|
||||||
|
|
||||||
|
hoveredBox.Flash();
|
||||||
|
|
||||||
|
var channel = sample?.GetChannel();
|
||||||
|
|
||||||
|
if (channel != null)
|
||||||
|
{
|
||||||
|
double repeatModifier = 0.05f * (Math.Abs(adjustDelay - initial_delay) / minimum_delay);
|
||||||
|
double multiplierModifier = (hoveredBox.Multiplier / max_multiplier) * 0.2f;
|
||||||
|
|
||||||
|
channel.Frequency.Value = 1 + multiplierModifier + repeatModifier;
|
||||||
|
channel.Play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
adjustDelay = initial_delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustDelegate = Scheduler.AddDelayed(adjustNext, adjustDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IncrementBox : CompositeDrawable
|
||||||
|
{
|
||||||
|
public readonly float Multiplier;
|
||||||
|
|
||||||
|
private readonly Box box;
|
||||||
|
private readonly OsuSpriteText text;
|
||||||
|
|
||||||
|
public IncrementBox(int index, double amount)
|
||||||
|
{
|
||||||
|
Multiplier = Math.Sign(index) * convertMultiplier(index);
|
||||||
|
|
||||||
|
float ratio = (float)index / adjust_levels;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
Width = 0.5f * Math.Abs(ratio);
|
||||||
|
|
||||||
|
Anchor direction = index < 0 ? Anchor.x2 : Anchor.x0;
|
||||||
|
|
||||||
|
Origin |= direction;
|
||||||
|
|
||||||
|
Depth = Math.Abs(index);
|
||||||
|
|
||||||
|
Anchor = Anchor.TopCentre;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
box = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive
|
||||||
|
},
|
||||||
|
text = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = direction,
|
||||||
|
Origin = direction,
|
||||||
|
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
|
||||||
|
Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}",
|
||||||
|
Padding = new MarginPadding(5),
|
||||||
|
Alpha = 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; }
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
box.Colour = colourProvider.Background1;
|
||||||
|
box.Alpha = 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float convertMultiplier(int m)
|
||||||
|
{
|
||||||
|
switch (Math.Abs(m))
|
||||||
|
{
|
||||||
|
default: return 1;
|
||||||
|
|
||||||
|
case 2: return 2;
|
||||||
|
|
||||||
|
case 3: return 5;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return max_multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
box.Colour = colourProvider.Colour0;
|
||||||
|
|
||||||
|
box.FadeTo(0.2f, 100, Easing.OutQuint);
|
||||||
|
text.FadeIn(100, Easing.OutQuint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
box.Colour = colourProvider.Background1;
|
||||||
|
|
||||||
|
box.FadeTo(0.1f, 500, Easing.OutQuint);
|
||||||
|
text.FadeOut(100, Easing.OutQuint);
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Flash()
|
||||||
|
{
|
||||||
|
box
|
||||||
|
.FadeTo(0.4f, 20, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.FadeTo(0.2f, 400, Easing.OutQuint);
|
||||||
|
|
||||||
|
text
|
||||||
|
.MoveToY(-5, 20, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.MoveToY(0, 400, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
public class TimingScreen : EditorScreenWithTimeline
|
public class TimingScreen : EditorScreenWithTimeline
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
|
public readonly Bindable<ControlPointGroup> SelectedGroup = new Bindable<ControlPointGroup>();
|
||||||
|
|
||||||
public TimingScreen()
|
public TimingScreen()
|
||||||
: base(EditorScreenMode.Timing)
|
: base(EditorScreenMode.Timing)
|
||||||
@ -132,6 +132,40 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
trackActivePoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given the user has selected a control point group, we want to track any group which is
|
||||||
|
/// active at the current point in time which matches the type the user has selected.
|
||||||
|
///
|
||||||
|
/// So if the user is currently looking at a timing point and seeks into the future, a
|
||||||
|
/// future timing point would be automatically selected if it is now the new "current" point.
|
||||||
|
/// </summary>
|
||||||
|
private void trackActivePoint()
|
||||||
|
{
|
||||||
|
// For simplicity only match on the first type of the active control point.
|
||||||
|
var selectedPointType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
|
||||||
|
|
||||||
|
if (selectedPointType != null)
|
||||||
|
{
|
||||||
|
// We don't have an efficient way of looking up groups currently, only individual point types.
|
||||||
|
// To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
|
||||||
|
|
||||||
|
// Find the next group which has the same type as the selected one.
|
||||||
|
var found = Beatmap.ControlPointInfo.Groups
|
||||||
|
.Where(g => g.ControlPoints.Any(cp => cp.GetType() == selectedPointType))
|
||||||
|
.LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
selectedGroup.Value = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void delete()
|
private void delete()
|
||||||
{
|
{
|
||||||
if (selectedGroup.Value == null)
|
if (selectedGroup.Value == null)
|
||||||
@ -144,7 +178,27 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private void addNew()
|
private void addNew()
|
||||||
{
|
{
|
||||||
selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
|
bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
|
||||||
|
|
||||||
|
var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
|
||||||
|
|
||||||
|
if (isFirstControlPoint)
|
||||||
|
group.Add(new TimingControlPoint());
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Try and create matching types from the currently selected control point.
|
||||||
|
var selected = selectedGroup.Value;
|
||||||
|
|
||||||
|
if (selected != null)
|
||||||
|
{
|
||||||
|
foreach (var controlPoint in selected.ControlPoints)
|
||||||
|
{
|
||||||
|
group.Add(controlPoint.DeepClone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGroup.Value = group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,31 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
bpmTextEntry.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
timeSignature.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
|
||||||
|
void saveChanges()
|
||||||
|
{
|
||||||
|
if (!isRebinding) ChangeHandler?.SaveState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isRebinding;
|
||||||
|
|
||||||
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
|
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
|
||||||
{
|
{
|
||||||
if (point.NewValue != null)
|
if (point.NewValue != null)
|
||||||
{
|
{
|
||||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
isRebinding = true;
|
||||||
bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
|
||||||
|
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||||
timeSignature.Current = point.NewValue.TimeSignatureBindable;
|
timeSignature.Current = point.NewValue.TimeSignatureBindable;
|
||||||
timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
isRebinding = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
{
|
{
|
||||||
private const int total_waveforms = 8;
|
private const int total_waveforms = 8;
|
||||||
|
|
||||||
|
private const float corner_radius = LabelledDrawable<Drawable>.CORNER_RADIUS;
|
||||||
|
|
||||||
private readonly BindableNumber<double> beatLength = new BindableDouble();
|
private readonly BindableNumber<double> beatLength = new BindableDouble();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -42,18 +45,22 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
||||||
|
|
||||||
private int lastDisplayedBeatIndex;
|
private double displayedTime;
|
||||||
|
|
||||||
private double selectedGroupStartTime;
|
private double selectedGroupStartTime;
|
||||||
private double selectedGroupEndTime;
|
private double selectedGroupEndTime;
|
||||||
|
|
||||||
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
||||||
|
|
||||||
|
private readonly BindableBool displayLocked = new BindableBool();
|
||||||
|
|
||||||
|
private LockedOverlay lockedOverlay = null!;
|
||||||
|
|
||||||
public WaveformComparisonDisplay()
|
public WaveformComparisonDisplay()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
CornerRadius = LabelledDrawable<Drawable>.CORNER_RADIUS;
|
CornerRadius = corner_radius;
|
||||||
Masking = true;
|
Masking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,72 +88,112 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
Width = 3,
|
Width = 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddInternal(lockedOverlay = new LockedOverlay());
|
||||||
|
|
||||||
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
|
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
|
||||||
|
|
||||||
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
||||||
controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
|
controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
|
||||||
|
|
||||||
beatLength.BindValueChanged(_ => showFrom(lastDisplayedBeatIndex), true);
|
beatLength.BindValueChanged(_ => regenerateDisplay(true), true);
|
||||||
|
|
||||||
|
displayLocked.BindValueChanged(locked =>
|
||||||
|
{
|
||||||
|
if (locked.NewValue)
|
||||||
|
lockedOverlay.Show();
|
||||||
|
else
|
||||||
|
lockedOverlay.Hide();
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTimingGroup()
|
private void updateTimingGroup()
|
||||||
{
|
{
|
||||||
beatLength.UnbindBindings();
|
beatLength.UnbindBindings();
|
||||||
|
|
||||||
selectedGroupStartTime = 0;
|
|
||||||
selectedGroupEndTime = beatmap.Value.Track.Length;
|
|
||||||
|
|
||||||
var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
||||||
|
|
||||||
if (tcp == null)
|
if (tcp == null)
|
||||||
{
|
{
|
||||||
timingPoint = new TimingControlPoint();
|
timingPoint = new TimingControlPoint();
|
||||||
|
// During movement of a control point's offset, this clause can be hit momentarily,
|
||||||
|
// as moving a control point is implemented by removing it and inserting it at the new time.
|
||||||
|
// We don't want to reset the `selectedGroupStartTime` here as we rely on having the
|
||||||
|
// last value to update the waveform display below.
|
||||||
|
selectedGroupEndTime = beatmap.Value.Track.Length;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
timingPoint = tcp;
|
timingPoint = tcp;
|
||||||
beatLength.BindTo(timingPoint.BeatLengthBindable);
|
beatLength.BindTo(timingPoint.BeatLengthBindable);
|
||||||
|
|
||||||
selectedGroupStartTime = selectedGroup.Value?.Time ?? 0;
|
double? newStartTime = selectedGroup.Value?.Time;
|
||||||
|
double? offsetChange = newStartTime - selectedGroupStartTime;
|
||||||
|
|
||||||
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
|
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
|
||||||
.SkipWhile(g => g != tcp)
|
.SkipWhile(g => g != tcp)
|
||||||
.Skip(1)
|
.Skip(1)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (nextGroup != null)
|
selectedGroupStartTime = newStartTime ?? 0;
|
||||||
selectedGroupEndTime = nextGroup.Time;
|
selectedGroupEndTime = nextGroup?.Time ?? beatmap.Value.Track.Length;
|
||||||
|
|
||||||
|
if (newStartTime.HasValue && offsetChange.HasValue)
|
||||||
|
{
|
||||||
|
// The offset of the selected point may have changed.
|
||||||
|
// This handles the case the user has locked the view and expects the display to update with this change.
|
||||||
|
showFromTime(displayedTime + offsetChange.Value, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e) => true;
|
protected override bool OnHover(HoverEvent e) => true;
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
{
|
{
|
||||||
float trackLength = (float)beatmap.Value.Track.Length;
|
if (!displayLocked.Value)
|
||||||
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
|
{
|
||||||
|
float trackLength = (float)beatmap.Value.Track.Length;
|
||||||
|
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
|
||||||
|
|
||||||
Scheduler.AddOnce(showFrom, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
|
Scheduler.AddOnce(showFromBeat, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
|
||||||
|
}
|
||||||
|
|
||||||
return base.OnMouseMove(e);
|
return base.OnMouseMove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
displayLocked.Toggle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (!IsHovered)
|
if (!IsHovered && !displayLocked.Value)
|
||||||
{
|
{
|
||||||
int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
|
int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
|
||||||
|
|
||||||
showFrom(currentBeat);
|
showFromBeat(currentBeat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFrom(int beatIndex)
|
private void showFromBeat(int beatIndex) =>
|
||||||
|
showFromTime(selectedGroupStartTime + beatIndex * timingPoint.BeatLength, false);
|
||||||
|
|
||||||
|
private void showFromTime(double time, bool animated)
|
||||||
{
|
{
|
||||||
if (lastDisplayedBeatIndex == beatIndex)
|
if (displayedTime == time)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
displayedTime = time;
|
||||||
|
regenerateDisplay(animated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void regenerateDisplay(bool animated)
|
||||||
|
{
|
||||||
|
double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
|
||||||
|
|
||||||
// Chosen as a pretty usable number across all BPMs.
|
// Chosen as a pretty usable number across all BPMs.
|
||||||
// Optimally we'd want this to scale with the BPM in question, but performing
|
// Optimally we'd want this to scale with the BPM in question, but performing
|
||||||
// scaling of the display is both expensive in resampling, and decreases usability
|
// scaling of the display is both expensive in resampling, and decreases usability
|
||||||
@ -156,23 +203,88 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
float trackLength = (float)beatmap.Value.Track.Length;
|
float trackLength = (float)beatmap.Value.Track.Length;
|
||||||
float scale = trackLength / visible_width;
|
float scale = trackLength / visible_width;
|
||||||
|
|
||||||
|
const int start_offset = total_waveforms / 2;
|
||||||
|
|
||||||
// Start displaying from before the current beat
|
// Start displaying from before the current beat
|
||||||
beatIndex -= total_waveforms / 2;
|
index -= start_offset;
|
||||||
|
|
||||||
foreach (var row in InternalChildren.OfType<WaveformRow>())
|
foreach (var row in InternalChildren.OfType<WaveformRow>())
|
||||||
{
|
{
|
||||||
// offset to the required beat index.
|
// offset to the required beat index.
|
||||||
double time = selectedGroupStartTime + beatIndex * timingPoint.BeatLength;
|
double time = selectedGroupStartTime + index * timingPoint.BeatLength;
|
||||||
|
|
||||||
float offset = (float)(time - visible_width / 2) / trackLength * scale;
|
float offset = (float)(time - visible_width / 2) / trackLength * scale;
|
||||||
|
|
||||||
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
|
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
|
||||||
row.WaveformOffset = -offset;
|
row.WaveformOffsetTo(-offset, animated);
|
||||||
row.WaveformScale = new Vector2(scale, 1);
|
row.WaveformScale = new Vector2(scale, 1);
|
||||||
row.BeatIndex = beatIndex++;
|
row.BeatIndex = (int)Math.Floor(index);
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LockedOverlay : CompositeDrawable
|
||||||
|
{
|
||||||
|
private OsuSpriteText text = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Masking = true;
|
||||||
|
CornerRadius = corner_radius;
|
||||||
|
BorderColour = colours.Red;
|
||||||
|
BorderThickness = 3;
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
AlwaysPresent = true,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0,
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colours.Red,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
text = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Colour = colours.GrayF,
|
||||||
|
Text = "Locked",
|
||||||
|
Margin = new MarginPadding(5),
|
||||||
|
Shadow = false,
|
||||||
|
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDisplayedBeatIndex = beatIndex;
|
public override void Show()
|
||||||
|
{
|
||||||
|
this.FadeIn(100, Easing.OutQuint);
|
||||||
|
|
||||||
|
text
|
||||||
|
.FadeIn().Then().Delay(600)
|
||||||
|
.FadeOut().Then().Delay(600)
|
||||||
|
.Loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Hide()
|
||||||
|
{
|
||||||
|
this.FadeOut(100, Easing.OutQuint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class WaveformRow : CompositeDrawable
|
internal class WaveformRow : CompositeDrawable
|
||||||
@ -224,7 +336,15 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
|
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
|
||||||
public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
|
public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
|
||||||
public float WaveformOffset { set => waveformGraph.X = value; }
|
|
||||||
|
public void WaveformOffsetTo(float value, bool animated) =>
|
||||||
|
this.TransformTo(nameof(waveformOffset), value, animated ? 300 : 0, Easing.OutQuint);
|
||||||
|
|
||||||
|
private float waveformOffset
|
||||||
|
{
|
||||||
|
get => waveformGraph.X;
|
||||||
|
set => waveformGraph.X = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -67,7 +68,25 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
|||||||
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
|
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
|
||||||
var beatmapManager = dependencies.Get<BeatmapManager>();
|
var beatmapManager = dependencies.Get<BeatmapManager>();
|
||||||
|
|
||||||
((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
|
((DummyAPIAccess)API).HandleRequest = request =>
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
// Because some of the handlers use realm, we need to ensure the game is still alive when firing.
|
||||||
|
// If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late.
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
|
||||||
|
tcs.SetResult(result);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
#pragma warning disable RS0030
|
||||||
|
// We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to
|
||||||
|
// the task being a TaskCompletionSource.
|
||||||
|
// Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument).
|
||||||
|
return tcs.Task.Result;
|
||||||
|
#pragma warning restore RS0030
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -48,9 +48,8 @@ namespace osu.Game.Utils
|
|||||||
|
|
||||||
options.AutoSessionTracking = true;
|
options.AutoSessionTracking = true;
|
||||||
options.IsEnvironmentUser = false;
|
options.IsEnvironmentUser = false;
|
||||||
// The reported release needs to match release tags on github in order for sentry
|
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
|
||||||
// to automatically associate and track against releases.
|
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
|
||||||
options.Release = game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger.NewEntry += processLogEntry;
|
Logger.NewEntry += processLogEntry;
|
||||||
|
Loading…
Reference in New Issue
Block a user