mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge branch 'master' into fix-dummmy-api-request-firing-2
This commit is contained in:
commit
24ce10ed6f
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/
|
||||
with:
|
||||
environment: production
|
||||
version: ${{ github.ref }}
|
||||
version: osu@${{ github.ref_name }}
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
if (positionInfo == positionInfos.First())
|
||||
{
|
||||
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
|
||||
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
|
||||
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
||||
}
|
||||
else
|
||||
|
@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
if (!(osuObject is Slider slider))
|
||||
return;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
|
||||
@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
if (!(osuObject is Slider slider))
|
||||
return;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
|
||||
@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a slider about its start position by the specified angle.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to be rotated.</param>
|
||||
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
||||
public static void RotateSlider(Slider slider, float rotation)
|
||||
{
|
||||
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(rotateNestedObject);
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(rotateNestedObject);
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = rotateVector(point.Position, rotation);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a vector by the specified angle.
|
||||
/// </summary>
|
||||
/// <param name="vector">The vector to be rotated.</param>
|
||||
/// <param name="rotation">The angle, measured in radians, to rotate the vector by.</param>
|
||||
/// <returns>The rotated vector.</returns>
|
||||
private static Vector2 rotateVector(Vector2 vector, float rotation)
|
||||
{
|
||||
float angle = MathF.Atan2(vector.Y, vector.X) + rotation;
|
||||
float length = vector.Length;
|
||||
return new Vector2(
|
||||
length * MathF.Cos(angle),
|
||||
length * MathF.Sin(angle)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
foreach (OsuHitObject hitObject in hitObjects)
|
||||
{
|
||||
Vector2 relativePosition = hitObject.Position - previousPosition;
|
||||
float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
||||
float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
|
||||
float relativeAngle = absoluteAngle - previousAngle;
|
||||
|
||||
positionInfos.Add(new ObjectPositionInfo(hitObject)
|
||||
ObjectPositionInfo positionInfo;
|
||||
positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject)
|
||||
{
|
||||
RelativeAngle = relativeAngle,
|
||||
DistanceFromPrevious = relativePosition.Length
|
||||
});
|
||||
|
||||
if (hitObject is Slider slider)
|
||||
{
|
||||
float absoluteRotation = getSliderRotation(slider);
|
||||
positionInfo.Rotation = absoluteRotation - absoluteAngle;
|
||||
absoluteAngle = absoluteRotation;
|
||||
}
|
||||
|
||||
previousPosition = hitObject.EndPosition;
|
||||
previousAngle = absoluteAngle;
|
||||
}
|
||||
@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
if (hitObject is Spinner)
|
||||
{
|
||||
previous = null;
|
||||
previous = current;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
||||
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
||||
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
||||
if (previous.HitObject is Slider s)
|
||||
{
|
||||
previousAbsoluteAngle = getSliderRotation(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
||||
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
||||
previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
|
||||
}
|
||||
}
|
||||
|
||||
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
|
||||
|
||||
var posRelativeToPrev = new Vector2(
|
||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
|
||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
|
||||
current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle),
|
||||
current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle)
|
||||
);
|
||||
|
||||
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
|
||||
@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
||||
|
||||
current.PositionModified = lastEndPosition + posRelativeToPrev;
|
||||
|
||||
if (!(current.HitObject is Slider slider))
|
||||
return;
|
||||
|
||||
absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
|
||||
|
||||
Vector2 centreOfMassOriginal = calculateCentreOfMass(slider);
|
||||
Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider));
|
||||
centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified);
|
||||
|
||||
float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X);
|
||||
if (!Precision.AlmostEquals(relativeRotation, 0))
|
||||
RotateSlider(slider, relativeRotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
var previousPosition = workingObject.PositionModified;
|
||||
|
||||
// Clamp slider position to the placement area
|
||||
// If the slider is larger than the playfield, force it to stay at the original position
|
||||
// If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield
|
||||
float newX = possibleMovementBounds.Width < 0
|
||||
? workingObject.PositionOriginal.X
|
||||
? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X)
|
||||
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
|
||||
|
||||
float newY = possibleMovementBounds.Height < 0
|
||||
? workingObject.PositionOriginal.Y
|
||||
? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y)
|
||||
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
|
||||
|
||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||
@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the centre of mass of a slider relative to its start position.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to process.</param>
|
||||
/// <returns>The centre of mass of the slider.</returns>
|
||||
private static Vector2 calculateCentreOfMass(Slider slider)
|
||||
{
|
||||
const double sample_step = 50;
|
||||
|
||||
// just sample the start and end positions if the slider is too short
|
||||
if (slider.Distance <= sample_step)
|
||||
{
|
||||
return Vector2.Divide(slider.Path.PositionAt(1), 2);
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
Vector2 sum = Vector2.Zero;
|
||||
double pathDistance = slider.Distance;
|
||||
|
||||
for (double i = 0; i < pathDistance; i += sample_step)
|
||||
{
|
||||
sum += slider.Path.PositionAt(i / pathDistance);
|
||||
count++;
|
||||
}
|
||||
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to process.</param>
|
||||
/// <returns>The angle in radians.</returns>
|
||||
private static float getSliderRotation(Slider slider)
|
||||
{
|
||||
var endPositionVector = slider.Path.PositionAt(1);
|
||||
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
|
||||
}
|
||||
|
||||
public class ObjectPositionInfo
|
||||
{
|
||||
/// <summary>
|
||||
@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
/// </remarks>
|
||||
public float DistanceFromPrevious { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rotation of the hit object, relative to its jump angle.
|
||||
/// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle.
|
||||
/// For hit circles and spinners, this property is ignored.
|
||||
/// </summary>
|
||||
public float Rotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
|
||||
/// </summary>
|
||||
|
@ -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}";
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoop()
|
||||
{
|
||||
AddStep("do nothing", () => { });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTapThenReset()
|
||||
{
|
||||
|
@ -1,14 +1,18 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osu.Game.Screens.Edit.Timing.RowAttributes;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private TimingScreen timingScreen;
|
||||
|
||||
protected override bool ScrollUsingMouseWheel => false;
|
||||
|
||||
public TestSceneTimingScreen()
|
||||
@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||
Beatmap.Disabled = true;
|
||||
|
||||
Child = new TimingScreen
|
||||
Child = timingScreen = new TimingScreen
|
||||
{
|
||||
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)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
|
@ -2,13 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
@ -26,6 +29,14 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
Height = 60;
|
||||
|
||||
Masking = true;
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 10f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
|
@ -99,6 +99,15 @@ namespace osu.Game.Screens.Edit
|
||||
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;
|
||||
|
||||
public bool Selected
|
||||
|
@ -61,6 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
selectedGroup.BindValueChanged(group =>
|
||||
{
|
||||
// TODO: This should scroll the selected row into view.
|
||||
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
|
||||
}, 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)
|
||||
{
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
isRebinding = true;
|
||||
|
||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
|
||||
scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
isRebinding = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@ -31,12 +33,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private IAdjustableClock metronomeClock;
|
||||
|
||||
private Sample clunk;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider overlayColourProvider { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
clunk = audio.Samples.Get(@"Multiplayer/countdown-tick");
|
||||
|
||||
const float taper = 25;
|
||||
const float swing_vertical_offset = -23;
|
||||
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)
|
||||
{
|
||||
using (stick.BeginDelayedSequence(beatLength / 2))
|
||||
using (BeginDelayedSequence(beatLength / 2))
|
||||
{
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
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, 60),
|
||||
new Dimension(GridSizeMode.Absolute, 60),
|
||||
},
|
||||
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[]
|
||||
{
|
||||
@ -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()
|
||||
{
|
||||
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
|
||||
{
|
||||
[Cached]
|
||||
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
|
||||
public readonly Bindable<ControlPointGroup> SelectedGroup = new Bindable<ControlPointGroup>();
|
||||
|
||||
public TimingScreen()
|
||||
: base(EditorScreenMode.Timing)
|
||||
@ -132,6 +132,40 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
}, 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()
|
||||
{
|
||||
if (selectedGroup.Value == null)
|
||||
@ -144,7 +178,27 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
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)
|
||||
{
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||
bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
isRebinding = true;
|
||||
|
||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||
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.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
private const int total_waveforms = 8;
|
||||
|
||||
private const float corner_radius = LabelledDrawable<Drawable>.CORNER_RADIUS;
|
||||
|
||||
private readonly BindableNumber<double> beatLength = new BindableDouble();
|
||||
|
||||
[Resolved]
|
||||
@ -42,18 +45,22 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
||||
|
||||
private int lastDisplayedBeatIndex;
|
||||
private double displayedTime;
|
||||
|
||||
private double selectedGroupStartTime;
|
||||
private double selectedGroupEndTime;
|
||||
|
||||
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
||||
|
||||
private readonly BindableBool displayLocked = new BindableBool();
|
||||
|
||||
private LockedOverlay lockedOverlay = null!;
|
||||
|
||||
public WaveformComparisonDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
CornerRadius = LabelledDrawable<Drawable>.CORNER_RADIUS;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
@ -63,7 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
for (int i = 0; i < total_waveforms; i++)
|
||||
{
|
||||
AddInternal(new WaveformRow
|
||||
AddInternal(new WaveformRow(i == total_waveforms / 2)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
@ -81,72 +88,112 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Width = 3,
|
||||
});
|
||||
|
||||
AddInternal(lockedOverlay = new LockedOverlay());
|
||||
|
||||
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
|
||||
|
||||
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
||||
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()
|
||||
{
|
||||
beatLength.UnbindBindings();
|
||||
|
||||
selectedGroupStartTime = 0;
|
||||
selectedGroupEndTime = beatmap.Value.Track.Length;
|
||||
|
||||
var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
||||
|
||||
if (tcp == null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
timingPoint = tcp;
|
||||
beatLength.BindTo(timingPoint.BeatLengthBindable);
|
||||
|
||||
selectedGroupStartTime = selectedGroup.Value?.Time ?? 0;
|
||||
double? newStartTime = selectedGroup.Value?.Time;
|
||||
double? offsetChange = newStartTime - selectedGroupStartTime;
|
||||
|
||||
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
|
||||
.SkipWhile(g => g != tcp)
|
||||
.Skip(1)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (nextGroup != null)
|
||||
selectedGroupEndTime = nextGroup.Time;
|
||||
selectedGroupStartTime = newStartTime ?? 0;
|
||||
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 OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
float trackLength = (float)beatmap.Value.Track.Length;
|
||||
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
|
||||
if (!displayLocked.Value)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
displayLocked.Toggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!IsHovered)
|
||||
if (!IsHovered && !displayLocked.Value)
|
||||
{
|
||||
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;
|
||||
|
||||
displayedTime = time;
|
||||
regenerateDisplay(animated);
|
||||
}
|
||||
|
||||
private void regenerateDisplay(bool animated)
|
||||
{
|
||||
double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
|
||||
|
||||
// Chosen as a pretty usable number across all BPMs.
|
||||
// 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
|
||||
@ -156,38 +203,115 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
float trackLength = (float)beatmap.Value.Track.Length;
|
||||
float scale = trackLength / visible_width;
|
||||
|
||||
const int start_offset = total_waveforms / 2;
|
||||
|
||||
// Start displaying from before the current beat
|
||||
beatIndex -= total_waveforms / 2;
|
||||
index -= start_offset;
|
||||
|
||||
foreach (var row in InternalChildren.OfType<WaveformRow>())
|
||||
{
|
||||
// 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;
|
||||
|
||||
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
|
||||
row.WaveformOffset = -offset;
|
||||
row.WaveformOffsetTo(-offset, animated);
|
||||
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
|
||||
{
|
||||
private readonly bool isMainRow;
|
||||
private OsuSpriteText beatIndexText = null!;
|
||||
private WaveformGraph waveformGraph = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public WaveformRow(bool isMainRow)
|
||||
{
|
||||
this.isMainRow = isMainRow;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background3,
|
||||
Alpha = isMainRow ? 1 : 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
waveformGraph = new WaveformGraph
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -212,7 +336,15 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,9 +48,8 @@ namespace osu.Game.Utils
|
||||
|
||||
options.AutoSessionTracking = true;
|
||||
options.IsEnvironmentUser = false;
|
||||
// The reported release needs to match release tags on github in order for sentry
|
||||
// to automatically associate and track against releases.
|
||||
options.Release = game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty);
|
||||
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
|
||||
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
|
||||
});
|
||||
|
||||
Logger.NewEntry += processLogEntry;
|
||||
|
Loading…
Reference in New Issue
Block a user