mirror of
https://github.com/ppy/osu.git
synced 2025-01-18 05:42:56 +08:00
Merge branch 'master' into mania-hp-bar-position
This commit is contained in:
commit
216c9040c8
@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(X, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(X, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
||||
|
||||
float IHasXPosition.X => OriginalX;
|
||||
float IHasXPosition.X
|
||||
{
|
||||
get => OriginalX;
|
||||
set => OriginalX = value;
|
||||
}
|
||||
|
||||
float IHasYPosition.Y => LegacyConvertedY;
|
||||
float IHasYPosition.Y
|
||||
{
|
||||
get => LegacyConvertedY;
|
||||
set => LegacyConvertedY = value;
|
||||
}
|
||||
|
||||
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
|
||||
Vector2 IHasPosition.Position
|
||||
{
|
||||
get => new Vector2(OriginalX, LegacyConvertedY);
|
||||
set
|
||||
{
|
||||
((IHasXPosition)this).X = value.X;
|
||||
((IHasYPosition)this).Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
float IHasXPosition.X => Column;
|
||||
float IHasXPosition.X
|
||||
{
|
||||
get => Column;
|
||||
set => Column = (int)value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@ -261,6 +263,163 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(100)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("also select third node", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("quick-delete fourth node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(3));
|
||||
InputManager.Click(MouseButton.Middle);
|
||||
});
|
||||
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(100)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("also select third node", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("quick-delete second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Middle);
|
||||
});
|
||||
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(50, 100)),
|
||||
new PathControlPoint(new Vector2(145, 100)),
|
||||
},
|
||||
ExpectedDistance = { Value = 162.62 }
|
||||
},
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select last node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("right click node", () => InputManager.Click(MouseButton.Right));
|
||||
AddUntilStep("context menu open", () => this.ChildrenOfType<ContextMenuContainer>().Single().ChildrenOfType<Menu>().All(m => m.State == MenuState.Open));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(10, 50),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = new Vector2(200, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(-100, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider));
|
||||
|
||||
AddStep("move to marker", () =>
|
||||
{
|
||||
var marker = this.ChildrenOfType<SliderEndDragMarker>().First();
|
||||
var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
|
||||
InputManager.MoveMouseTo(position);
|
||||
});
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider));
|
||||
}
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
|
100
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs
Normal file
100
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs
Normal file
@ -0,0 +1,100 @@
|
||||
// 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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModRelax : OsuModTestScene
|
||||
{
|
||||
private readonly HitCircle hitObject;
|
||||
private readonly HitWindows hitWindows = new OsuHitWindows();
|
||||
|
||||
public TestSceneOsuModRelax()
|
||||
{
|
||||
hitWindows.SetDifficulty(9);
|
||||
|
||||
hitObject = new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(100, 100),
|
||||
HitWindows = hitWindows
|
||||
};
|
||||
}
|
||||
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
|
||||
|
||||
[Test]
|
||||
public void TestRelax() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModRelax(),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject> { hitObject }
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2()),
|
||||
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestRelaxLeniency() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModRelax(),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject> { hitObject }
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
|
||||
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
|
||||
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
protected partial class ModRelaxTestPlayer : ModTestPlayer
|
||||
{
|
||||
private readonly ModTestData currentTestData;
|
||||
|
||||
public ModRelaxTestPlayer(ModTestData data, bool allowFail)
|
||||
: base(data, allowFail)
|
||||
{
|
||||
currentTestData = data;
|
||||
}
|
||||
|
||||
protected override void PrepareReplay()
|
||||
{
|
||||
// We need to set IsLegacyScore to true otherwise the mod assumes that presses are already embedded into the replay
|
||||
DrawableRuleset?.SetReplayScore(new Score
|
||||
{
|
||||
Replay = new Replay { Frames = currentTestData.ReplayFrames! },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" }, IsLegacyScore = true, Mods = new Mod[] { new OsuModRelax() } },
|
||||
});
|
||||
|
||||
DrawableRuleset?.SetRecordTarget(Score);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -137,11 +137,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <summary>
|
||||
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>Whether any change actually took place.</returns>
|
||||
public bool DeleteSelected()
|
||||
{
|
||||
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
||||
|
||||
if (!Delete(toRemove))
|
||||
return false;
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the specified <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <returns>Whether any change actually took place.</returns>
|
||||
public bool Delete(List<PathControlPoint> toRemove)
|
||||
{
|
||||
// Ensure that there are any points to be deleted
|
||||
if (toRemove.Count == 0)
|
||||
return false;
|
||||
@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
changeHandler?.BeginChange();
|
||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||
changeHandler?.EndChange();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left;
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
|
@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (hoveredControlPoint == null)
|
||||
return false;
|
||||
|
||||
hoveredControlPoint.IsSelected.Value = true;
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
if (hoveredControlPoint.IsSelected.Value)
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
else
|
||||
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
/// <summary>
|
||||
/// How early before a hitobject's start time to trigger a hit.
|
||||
/// </summary>
|
||||
private const float relax_leniency = 3;
|
||||
public const float RELAX_LENIENCY = 12;
|
||||
|
||||
private bool isDownState;
|
||||
private bool wasLeft;
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
||||
{
|
||||
// we are not yet close enough to the object.
|
||||
if (time < h.HitObject.StartTime - relax_leniency)
|
||||
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
|
||||
break;
|
||||
|
||||
// already hit or beyond the hittable end time.
|
||||
|
@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set => position.Value = value;
|
||||
}
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Position.Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(Position.X, value);
|
||||
}
|
||||
|
||||
public Vector2 StackedPosition => Position + StackOffset;
|
||||
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
using MemoryStream = System.IO.MemoryStream;
|
||||
@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFractionalObjectCoordinatesRounded()
|
||||
{
|
||||
IWorkingBeatmap beatmap = null!;
|
||||
MemoryStream outStream = null!;
|
||||
|
||||
// Ensure importer encoding is correct
|
||||
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
|
||||
AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
|
||||
|
||||
// Ensure exporter legacy conversion is correct
|
||||
AddStep("export", () =>
|
||||
{
|
||||
outStream = new MemoryStream();
|
||||
|
||||
new LegacyBeatmapExporter(LocalStorage)
|
||||
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||
});
|
||||
|
||||
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||
AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExportStability()
|
||||
{
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/fractional-coordinates.olz
Normal file
BIN
osu.Game.Tests/Resources/Archives/fractional-coordinates.olz
Normal file
Binary file not shown.
@ -177,6 +177,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
// bit of a hack to ensure this test can be ran multiple times without running into UNIQUE constraint failures
|
||||
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
|
||||
|
||||
AddStep("start playing track", () => InputManager.Key(Key.Space));
|
||||
AddStep("click test gameplay button", () =>
|
||||
{
|
||||
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
||||
@ -185,11 +186,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
|
||||
|
||||
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
|
||||
EditorPlayer editorPlayer = null;
|
||||
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
||||
AddUntilStep("track playing", () => Beatmap.Value.Track.IsRunning);
|
||||
AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1);
|
||||
|
||||
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
|
||||
|
@ -42,7 +42,10 @@ namespace osu.Game.Database
|
||||
return null;
|
||||
|
||||
using var contentStreamReader = new LineBufferedReader(contentStream);
|
||||
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
|
||||
|
||||
// FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts.
|
||||
// we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating)
|
||||
var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader);
|
||||
|
||||
var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
|
||||
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
|
||||
@ -93,6 +96,12 @@ namespace osu.Game.Database
|
||||
|
||||
hitObject.StartTime = Math.Floor(hitObject.StartTime);
|
||||
|
||||
if (hitObject is IHasXPosition hasXPosition)
|
||||
hasXPosition.X = MathF.Round(hasXPosition.X);
|
||||
|
||||
if (hitObject is IHasYPosition hasYPosition)
|
||||
hasYPosition.Y = MathF.Round(hasYPosition.Y);
|
||||
|
||||
if (hitObject is not IHasPath hasPath) continue;
|
||||
|
||||
// stable's hit object parsing expects the entire slider to use only one type of curve,
|
||||
|
@ -96,8 +96,9 @@ namespace osu.Game.Database
|
||||
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
|
||||
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
|
||||
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
|
||||
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
|
||||
/// </summary>
|
||||
private const int schema_version = 45;
|
||||
private const int schema_version = 46;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -1222,6 +1223,22 @@ namespace osu.Game.Database
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 46:
|
||||
{
|
||||
// Stable direction didn't match.
|
||||
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
||||
|
||||
var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor);
|
||||
if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelDown }))
|
||||
migration.NewRealm.Remove(nextBeatSnapBinding);
|
||||
|
||||
var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor);
|
||||
if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelUp }))
|
||||
migration.NewRealm.Remove(previousBeatSnapBinding);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
@ -142,8 +142,8 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
||||
|
@ -1428,24 +1428,25 @@ namespace osu.Game
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
if (introScreen == null) return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.DecreaseVolume:
|
||||
case GlobalAction.IncreaseVolume:
|
||||
return volume.Adjust(e.Action);
|
||||
}
|
||||
|
||||
// All actions below this point don't allow key repeat.
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
// Wait until we're loaded at least to the intro before allowing various interactions.
|
||||
if (introScreen == null) return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.ToggleMute:
|
||||
case GlobalAction.NextVolumeMeter:
|
||||
case GlobalAction.PreviousVolumeMeter:
|
||||
|
||||
if (e.Repeat)
|
||||
return true;
|
||||
|
||||
return volume.Adjust(e.Action);
|
||||
|
||||
case GlobalAction.ToggleFPSDisplay:
|
||||
|
@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
codeTextBox.Current.BindValueChanged(code =>
|
||||
{
|
||||
if (code.NewValue.Length == 8)
|
||||
string trimmedCode = code.NewValue.Trim();
|
||||
|
||||
if (trimmedCode.Length == 8)
|
||||
{
|
||||
api.AuthenticateSecondFactor(code.NewValue);
|
||||
api.AuthenticateSecondFactor(trimmedCode);
|
||||
codeTextBox.Current.Disabled = true;
|
||||
}
|
||||
});
|
||||
|
@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
|
||||
public int ComboOffset { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Position.Y);
|
||||
}
|
||||
|
||||
public float Y => Position.Y;
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(Position.X, value);
|
||||
}
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
|
@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The starting position of the HitObject.
|
||||
/// </summary>
|
||||
Vector2 Position { get; }
|
||||
Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The starting X-position of this HitObject.
|
||||
/// </summary>
|
||||
float X { get; }
|
||||
float X { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The starting Y-position of this HitObject.
|
||||
/// </summary>
|
||||
float Y { get; }
|
||||
float Y { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -131,7 +132,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private void updateSamplePointContractedState()
|
||||
{
|
||||
const double minimum_gap = 28;
|
||||
const double absolute_minimum_gap = 31; // assumes single letter bank name for default banks
|
||||
double minimumGap = absolute_minimum_gap;
|
||||
|
||||
if (timeline == null || editorClock == null)
|
||||
return;
|
||||
@ -153,9 +155,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
|
||||
break;
|
||||
|
||||
foreach (var sample in hitObject.Samples)
|
||||
{
|
||||
if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
|
||||
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
|
||||
}
|
||||
|
||||
if (hitObject is IHasRepeats hasRepeats)
|
||||
{
|
||||
smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
|
||||
|
||||
foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s))
|
||||
{
|
||||
if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
|
||||
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
|
||||
}
|
||||
}
|
||||
|
||||
double gap = lastTime - hitObject.GetEndTime();
|
||||
|
||||
// If the gap is less than 1ms, we can assume that the objects are stacked on top of each other
|
||||
@ -167,7 +183,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
|
||||
double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap;
|
||||
SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap;
|
||||
SamplePointContracted.Value = smallestAbsoluteGap < minimumGap;
|
||||
}
|
||||
|
||||
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
|
||||
|
@ -523,6 +523,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public void TestGameplay()
|
||||
{
|
||||
clock.Stop();
|
||||
|
||||
if (HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||
|
@ -28,33 +28,31 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(SetupScreen? setupScreen)
|
||||
{
|
||||
var metadata = Beatmap.Metadata;
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist,
|
||||
!string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist),
|
||||
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist,
|
||||
!string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
|
||||
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title,
|
||||
!string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title),
|
||||
RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle,
|
||||
!string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
|
||||
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator, metadata.Author.Username),
|
||||
difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName),
|
||||
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
|
||||
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
|
||||
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist),
|
||||
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist),
|
||||
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title),
|
||||
RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle),
|
||||
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator),
|
||||
difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName),
|
||||
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource),
|
||||
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoTags)
|
||||
};
|
||||
|
||||
if (setupScreen != null)
|
||||
setupScreen.MetadataChanged += reloadMetadata;
|
||||
|
||||
reloadMetadata();
|
||||
}
|
||||
|
||||
private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue)
|
||||
private TTextBox createTextBox<TTextBox>(LocalisableString label)
|
||||
where TTextBox : FormTextBox, new()
|
||||
=> new TTextBox
|
||||
{
|
||||
Caption = label,
|
||||
Current = { Value = initialValue },
|
||||
TabbableContentContainer = this
|
||||
};
|
||||
|
||||
@ -94,10 +92,29 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
|
||||
// for now, update on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
updateMetadata();
|
||||
setMetadata();
|
||||
}
|
||||
|
||||
private void updateMetadata()
|
||||
private void reloadMetadata()
|
||||
{
|
||||
var metadata = Beatmap.Metadata;
|
||||
|
||||
RomanisedArtistTextBox.ReadOnly = false;
|
||||
RomanisedTitleTextBox.ReadOnly = false;
|
||||
|
||||
ArtistTextBox.Current.Value = !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist;
|
||||
RomanisedArtistTextBox.Current.Value = !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
|
||||
TitleTextBox.Current.Value = !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title;
|
||||
RomanisedTitleTextBox.Current.Value = !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
|
||||
creatorTextBox.Current.Value = metadata.Author.Username;
|
||||
difficultyTextBox.Current.Value = Beatmap.BeatmapInfo.DifficultyName;
|
||||
sourceTextBox.Current.Value = metadata.Source;
|
||||
tagsTextBox.Current.Value = metadata.Tags;
|
||||
|
||||
updateReadOnlyState();
|
||||
}
|
||||
|
||||
private void setMetadata()
|
||||
{
|
||||
Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value;
|
||||
Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value;
|
||||
|
@ -35,6 +35,9 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private SetupScreen setupScreen { get; set; } = null!;
|
||||
|
||||
private SetupScreenHeaderBackground headerBackground = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -93,15 +96,37 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
if (!source.Exists)
|
||||
return false;
|
||||
|
||||
var tagSource = TagLib.File.Create(source.FullName);
|
||||
|
||||
changeResource(source, applyToAllDifficulties, @"audio",
|
||||
metadata => metadata.AudioFile,
|
||||
(metadata, name) => metadata.AudioFile = name);
|
||||
(metadata, name) =>
|
||||
{
|
||||
metadata.AudioFile = name;
|
||||
|
||||
string artist = tagSource.Tag.JoinedAlbumArtists;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
metadata.ArtistUnicode = artist;
|
||||
metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
|
||||
}
|
||||
|
||||
string title = tagSource.Tag.Title;
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
metadata.TitleUnicode = title;
|
||||
metadata.Title = MetadataUtils.StripNonRomanisedCharacters(metadata.TitleUnicode);
|
||||
}
|
||||
});
|
||||
|
||||
music.ReloadCurrentTrack();
|
||||
setupScreen.MetadataChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func<BeatmapMetadata, string> readFilename, Action<BeatmapMetadata, string> writeFilename)
|
||||
private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func<BeatmapMetadata, string> readFilename, Action<BeatmapMetadata, string> writeMetadata)
|
||||
{
|
||||
var set = working.Value.BeatmapSetInfo;
|
||||
var beatmap = working.Value.BeatmapInfo;
|
||||
@ -148,10 +173,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
foreach (var b in otherBeatmaps)
|
||||
{
|
||||
// This operation is quite expensive, so only perform it if required.
|
||||
if (readFilename(b.Metadata) == newFilename) continue;
|
||||
|
||||
writeFilename(b.Metadata, newFilename);
|
||||
writeMetadata(b.Metadata, newFilename);
|
||||
|
||||
// save the difficulty to re-encode the .osu file, updating any reference of the old filename.
|
||||
//
|
||||
@ -162,7 +184,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
}
|
||||
}
|
||||
|
||||
writeFilename(beatmap.Metadata, newFilename);
|
||||
writeMetadata(beatmap.Metadata, newFilename);
|
||||
|
||||
// editor change handler cannot be aware of any file changes or other difficulties having their metadata modified.
|
||||
// for simplicity's sake, trigger a save when changing any resource to ensure the change is correctly saved.
|
||||
|
@ -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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,12 +14,15 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
[Cached]
|
||||
public partial class SetupScreen : EditorScreen
|
||||
{
|
||||
public const float COLUMN_WIDTH = 450;
|
||||
public const float SPACING = 28;
|
||||
public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING;
|
||||
|
||||
public Action? MetadataChanged { get; set; }
|
||||
|
||||
public SetupScreen()
|
||||
: base(EditorScreenMode.SongSetup)
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
@ -41,6 +42,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; } = null!;
|
||||
|
||||
public bool EnableClicking
|
||||
{
|
||||
get => metronomeTick.EnableClicking;
|
||||
@ -222,7 +226,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Clock = new FramedClock(metronomeClock = new StopwatchClock(true));
|
||||
}
|
||||
|
||||
private double beatLength;
|
||||
private double effectiveBeatLength;
|
||||
|
||||
private TimingControlPoint timingPoint = null!;
|
||||
|
||||
@ -232,11 +236,26 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private ScheduledDelegate? latchDelegate;
|
||||
|
||||
private bool spedUp;
|
||||
|
||||
private int computeSpedUpDivisor()
|
||||
{
|
||||
if (!spedUp)
|
||||
return 1;
|
||||
|
||||
if (beatDivisor.Value % 3 == 0)
|
||||
return 3;
|
||||
if (beatDivisor.Value % 2 == 0)
|
||||
return 2;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString());
|
||||
interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString());
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -250,16 +269,20 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime);
|
||||
|
||||
if (beatLength != timingPoint.BeatLength)
|
||||
Divisor = metronomeTick.Divisor = computeSpedUpDivisor();
|
||||
|
||||
if (effectiveBeatLength != timingPoint.BeatLength / Divisor)
|
||||
{
|
||||
beatLength = timingPoint.BeatLength;
|
||||
effectiveBeatLength = timingPoint.BeatLength / Divisor;
|
||||
|
||||
EarlyActivationMilliseconds = timingPoint.BeatLength / 2;
|
||||
|
||||
float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480, 0, 1));
|
||||
double effectiveBpm = 60000 / effectiveBeatLength;
|
||||
|
||||
float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1));
|
||||
|
||||
weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint);
|
||||
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint);
|
||||
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint);
|
||||
}
|
||||
|
||||
if (!BeatSyncSource.Clock.IsRunning && isSwinging)
|
||||
@ -305,7 +328,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
float currentAngle = swing.Rotation;
|
||||
float targetAngle = currentAngle > 0 ? -angle : angle;
|
||||
|
||||
swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
|
||||
swing.RotateTo(targetAngle, effectiveBeatLength, Easing.InOutQuad);
|
||||
}
|
||||
|
||||
private void onTickPlayed()
|
||||
@ -313,9 +336,25 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
// Originally, this flash only occurred when the pendulum correctly passess the centre.
|
||||
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
|
||||
// so now this matches the actual tick sample.
|
||||
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
||||
stick.FlashColour(overlayColourProvider.Content1, effectiveBeatLength, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
updateDivisorFromKey(e);
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
updateDivisorFromKey(e);
|
||||
}
|
||||
|
||||
private void updateDivisorFromKey(UIEvent e) => spedUp = e.ControlPressed;
|
||||
|
||||
private partial class MetronomeTick : BeatSyncedContainer
|
||||
{
|
||||
public bool EnableClicking;
|
||||
|
@ -157,5 +157,9 @@
|
||||
<string>public.app-category.music-games</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<false/>
|
||||
<!-- Game mode is supposed to be automatically enabled by the app category specification above,
|
||||
but for some reason it needs to be explicitly enabled with this key. -->
|
||||
<key>GCSupportsGameMode</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Loading…
Reference in New Issue
Block a user