1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 08:27:23 +08:00

Merge branch 'ppy:master' into master

This commit is contained in:
Unili 2024-12-31 11:35:55 +08:00 committed by GitHub
commit acd51c8e9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 314 additions and 23 deletions

View File

@ -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();

View 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);
}
}
}
}

View File

@ -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;
}

View File

@ -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()
{

View File

@ -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;
}

View File

@ -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.

View File

@ -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:

View File

@ -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>();