mirror of
https://github.com/ppy/osu.git
synced 2025-01-18 08:32:54 +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 Vector2 Position { get; set; }
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
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 Vector2 Position { get; set; }
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
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>
|
/// </summary>
|
||||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
|
|
||||||
#region LegacyBeatmapEncoder
|
#region LegacyBeatmapEncoder
|
||||||
|
|
||||||
float IHasXPosition.X => Column;
|
float IHasXPosition.X
|
||||||
|
{
|
||||||
|
get => Column;
|
||||||
|
set => Column = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,9 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Objects;
|
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.HitCircles.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
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));
|
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
|
private ComposeBlueprintContainer blueprintContainer
|
||||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
=> 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>
|
/// <summary>
|
||||||
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns>Whether any change actually took place.</returns>
|
||||||
public bool DeleteSelected()
|
public bool DeleteSelected()
|
||||||
{
|
{
|
||||||
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
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
|
// Ensure that there are any points to be deleted
|
||||||
if (toRemove.Count == 0)
|
if (toRemove.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||||
changeHandler?.EndChange();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||||
{
|
{
|
||||||
@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
base.OnDragEnd(e);
|
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()
|
private void updateState()
|
||||||
{
|
{
|
||||||
|
@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
if (hoveredControlPoint == null)
|
if (hoveredControlPoint == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
hoveredControlPoint.IsSelected.Value = true;
|
if (hoveredControlPoint.IsSelected.Value)
|
||||||
ControlPointVisualiser?.DeleteSelected();
|
ControlPointVisualiser?.DeleteSelected();
|
||||||
|
else
|
||||||
|
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// How early before a hitobject's start time to trigger a hit.
|
/// How early before a hitobject's start time to trigger a hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const float relax_leniency = 3;
|
public const float RELAX_LENIENCY = 12;
|
||||||
|
|
||||||
private bool isDownState;
|
private bool isDownState;
|
||||||
private bool wasLeft;
|
private bool wasLeft;
|
||||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
||||||
{
|
{
|
||||||
// we are not yet close enough to the object.
|
// we are not yet close enough to the object.
|
||||||
if (time < h.HitObject.StartTime - relax_leniency)
|
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// already hit or beyond the hittable end time.
|
// already hit or beyond the hittable end time.
|
||||||
|
@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set => position.Value = value;
|
set => position.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
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;
|
public Vector2 StackedPosition => Position + StackOffset;
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using MemoryStream = System.IO.MemoryStream;
|
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));
|
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]
|
[Test]
|
||||||
public void TestExportStability()
|
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
|
// 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("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
|
||||||
|
|
||||||
|
AddStep("start playing track", () => InputManager.Key(Key.Space));
|
||||||
AddStep("click test gameplay button", () =>
|
AddStep("click test gameplay button", () =>
|
||||||
{
|
{
|
||||||
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
||||||
@ -185,11 +186,13 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||||
|
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
|
||||||
|
|
||||||
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||||
|
|
||||||
EditorPlayer editorPlayer = null;
|
EditorPlayer editorPlayer = null;
|
||||||
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as 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);
|
AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1);
|
||||||
|
|
||||||
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
|
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
|
||||||
|
@ -42,7 +42,10 @@ namespace osu.Game.Database
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
using var contentStreamReader = new LineBufferedReader(contentStream);
|
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 workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
|
||||||
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
|
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
|
||||||
@ -93,6 +96,12 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
hitObject.StartTime = Math.Floor(hitObject.StartTime);
|
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;
|
if (hitObject is not IHasPath hasPath) continue;
|
||||||
|
|
||||||
// stable's hit object parsing expects the entire slider to use only one type of curve,
|
// 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.
|
/// 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.
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
private const int schema_version = 45;
|
private const int schema_version = 46;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// 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;
|
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");
|
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.J }, GlobalAction.EditorFlipVertically),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
|
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.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
||||||
|
@ -1428,24 +1428,25 @@ namespace osu.Game
|
|||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (e.Repeat)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (introScreen == null) return false;
|
|
||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.DecreaseVolume:
|
case GlobalAction.DecreaseVolume:
|
||||||
case GlobalAction.IncreaseVolume:
|
case GlobalAction.IncreaseVolume:
|
||||||
return volume.Adjust(e.Action);
|
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.ToggleMute:
|
||||||
case GlobalAction.NextVolumeMeter:
|
case GlobalAction.NextVolumeMeter:
|
||||||
case GlobalAction.PreviousVolumeMeter:
|
case GlobalAction.PreviousVolumeMeter:
|
||||||
|
|
||||||
if (e.Repeat)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return volume.Adjust(e.Action);
|
return volume.Adjust(e.Action);
|
||||||
|
|
||||||
case GlobalAction.ToggleFPSDisplay:
|
case GlobalAction.ToggleFPSDisplay:
|
||||||
|
@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login
|
|||||||
|
|
||||||
codeTextBox.Current.BindValueChanged(code =>
|
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;
|
codeTextBox.Current.Disabled = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
|
|
||||||
public int ComboOffset { get; set; }
|
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; }
|
public Vector2 Position { get; set; }
|
||||||
|
|
||||||
|
@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The starting position of the HitObject.
|
/// The starting position of the HitObject.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Vector2 Position { get; }
|
Vector2 Position { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The starting X-position of this HitObject.
|
/// The starting X-position of this HitObject.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
float X { get; }
|
float X { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The starting Y-position of this HitObject.
|
/// The starting Y-position of this HitObject.
|
||||||
/// </summary>
|
/// </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.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Audio;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -131,7 +132,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
private void updateSamplePointContractedState()
|
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)
|
if (timeline == null || editorClock == null)
|
||||||
return;
|
return;
|
||||||
@ -153,9 +155,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
|
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
|
||||||
break;
|
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)
|
if (hitObject is IHasRepeats hasRepeats)
|
||||||
|
{
|
||||||
smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
|
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();
|
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
|
// 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;
|
double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap;
|
||||||
SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap;
|
SamplePointContracted.Value = smallestAbsoluteGap < minimumGap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
|
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
|
||||||
|
@ -523,6 +523,8 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
public void TestGameplay()
|
public void TestGameplay()
|
||||||
{
|
{
|
||||||
|
clock.Stop();
|
||||||
|
|
||||||
if (HasUnsavedChanges)
|
if (HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||||
|
@ -28,33 +28,31 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
|
public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(SetupScreen? setupScreen)
|
||||||
{
|
{
|
||||||
var metadata = Beatmap.Metadata;
|
|
||||||
|
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist,
|
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist),
|
||||||
!string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist),
|
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist),
|
||||||
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist,
|
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title),
|
||||||
!string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
|
RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle),
|
||||||
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title,
|
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator),
|
||||||
!string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title),
|
difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName),
|
||||||
RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle,
|
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource),
|
||||||
!string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
|
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoTags)
|
||||||
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)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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()
|
where TTextBox : FormTextBox, new()
|
||||||
=> new TTextBox
|
=> new TTextBox
|
||||||
{
|
{
|
||||||
Caption = label,
|
Caption = label,
|
||||||
Current = { Value = initialValue },
|
|
||||||
TabbableContentContainer = this
|
TabbableContentContainer = this
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,10 +92,29 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
|
|
||||||
// for now, update on commit rather than making BeatmapMetadata bindables.
|
// 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.
|
// 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.ArtistUnicode = ArtistTextBox.Current.Value;
|
||||||
Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value;
|
Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value;
|
||||||
|
@ -35,6 +35,9 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private Editor? editor { get; set; }
|
private Editor? editor { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SetupScreen setupScreen { get; set; } = null!;
|
||||||
|
|
||||||
private SetupScreenHeaderBackground headerBackground = null!;
|
private SetupScreenHeaderBackground headerBackground = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -93,15 +96,37 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
if (!source.Exists)
|
if (!source.Exists)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
var tagSource = TagLib.File.Create(source.FullName);
|
||||||
|
|
||||||
changeResource(source, applyToAllDifficulties, @"audio",
|
changeResource(source, applyToAllDifficulties, @"audio",
|
||||||
metadata => metadata.AudioFile,
|
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();
|
music.ReloadCurrentTrack();
|
||||||
|
setupScreen.MetadataChanged?.Invoke();
|
||||||
return true;
|
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 set = working.Value.BeatmapSetInfo;
|
||||||
var beatmap = working.Value.BeatmapInfo;
|
var beatmap = working.Value.BeatmapInfo;
|
||||||
@ -148,10 +173,7 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
{
|
{
|
||||||
foreach (var b in otherBeatmaps)
|
foreach (var b in otherBeatmaps)
|
||||||
{
|
{
|
||||||
// This operation is quite expensive, so only perform it if required.
|
writeMetadata(b.Metadata, newFilename);
|
||||||
if (readFilename(b.Metadata) == newFilename) continue;
|
|
||||||
|
|
||||||
writeFilename(b.Metadata, newFilename);
|
|
||||||
|
|
||||||
// save the difficulty to re-encode the .osu file, updating any reference of the old filename.
|
// 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.
|
// 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.
|
// 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.
|
// 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;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -13,12 +14,15 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Setup
|
namespace osu.Game.Screens.Edit.Setup
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class SetupScreen : EditorScreen
|
public partial class SetupScreen : EditorScreen
|
||||||
{
|
{
|
||||||
public const float COLUMN_WIDTH = 450;
|
public const float COLUMN_WIDTH = 450;
|
||||||
public const float SPACING = 28;
|
public const float SPACING = 28;
|
||||||
public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING;
|
public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING;
|
||||||
|
|
||||||
|
public Action? MetadataChanged { get; set; }
|
||||||
|
|
||||||
public SetupScreen()
|
public SetupScreen()
|
||||||
: base(EditorScreenMode.SongSetup)
|
: base(EditorScreenMode.SongSetup)
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
@ -41,6 +42,9 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
|
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BindableBeatDivisor beatDivisor { get; set; } = null!;
|
||||||
|
|
||||||
public bool EnableClicking
|
public bool EnableClicking
|
||||||
{
|
{
|
||||||
get => metronomeTick.EnableClicking;
|
get => metronomeTick.EnableClicking;
|
||||||
@ -222,7 +226,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
Clock = new FramedClock(metronomeClock = new StopwatchClock(true));
|
Clock = new FramedClock(metronomeClock = new StopwatchClock(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private double beatLength;
|
private double effectiveBeatLength;
|
||||||
|
|
||||||
private TimingControlPoint timingPoint = null!;
|
private TimingControlPoint timingPoint = null!;
|
||||||
|
|
||||||
@ -232,11 +236,26 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private ScheduledDelegate? latchDelegate;
|
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()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString());
|
interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -250,16 +269,20 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime);
|
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;
|
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);
|
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)
|
if (!BeatSyncSource.Clock.IsRunning && isSwinging)
|
||||||
@ -305,7 +328,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
float currentAngle = swing.Rotation;
|
float currentAngle = swing.Rotation;
|
||||||
float targetAngle = currentAngle > 0 ? -angle : angle;
|
float targetAngle = currentAngle > 0 ? -angle : angle;
|
||||||
|
|
||||||
swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
|
swing.RotateTo(targetAngle, effectiveBeatLength, Easing.InOutQuad);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTickPlayed()
|
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.
|
// 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
|
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
|
||||||
// so now this matches the actual tick sample.
|
// 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
|
private partial class MetronomeTick : BeatSyncedContainer
|
||||||
{
|
{
|
||||||
public bool EnableClicking;
|
public bool EnableClicking;
|
||||||
|
@ -157,5 +157,9 @@
|
|||||||
<string>public.app-category.music-games</string>
|
<string>public.app-category.music-games</string>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<false/>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
Loading…
Reference in New Issue
Block a user