1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 02:32:55 +08:00

Merge branch 'master' into daily-challenge/better-results

This commit is contained in:
Dean Herbert 2024-07-04 23:56:09 +09:00 committed by GitHub
commit f0ad7a97cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
100 changed files with 3207 additions and 475 deletions

View File

@ -7,7 +7,6 @@ T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> ins
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.627.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.702.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public partial class TestSceneCatchEditorSaving : EditorSavingTestScene
{
protected override Ruleset CreateRuleset() => new CatchRuleset();
[Test]
public void TestCatchJuiceStreamTickCorrect()
{
AddStep("enter timing mode", () => InputManager.Key(Key.F3));
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
Vector2 startPoint = Vector2.Zero;
float increment = 0;
AddUntilStep("wait for playfield", () => this.ChildrenOfType<CatchPlayfield>().FirstOrDefault()?.IsLoaded == true);
AddStep("move to centre", () =>
{
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3);
increment = playfield.ScreenSpaceDrawQuad.Height / 10;
InputManager.MoveMouseTo(startPoint);
});
AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment)));
AddStep("add node", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment)));
AddStep("add node", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment)));
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
int largeDropletCount = 0, tinyDropletCount = 0;
AddStep("store droplet count", () =>
{
largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet));
tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet));
});
SaveEditor();
ReloadEditorToSameBeatmap();
AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount));
AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount));
}
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene
{
[Test]
public void TestReplayDetach()
{
DrawableCatchRuleset drawableRuleset = null!;
float catcherPosition = 0;
AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), []));
AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score()));
AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType<Catcher>().Single().X);
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(catcherPosition));
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
AddStep("detach replay", () => drawableRuleset.SetReplayScore(null));
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.Not.EqualTo(catcherPosition));
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
}
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration. // this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1 : ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity, TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1 SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield(); }.Yield();

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -62,43 +61,43 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new CatchModNightcore(); yield return new CatchModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime(); yield return new CatchModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new CatchModPerfect(); yield return new CatchModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new CatchModSuddenDeath(); yield return new CatchModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new CatchModCinema(); yield return new CatchModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new CatchModAutoplay(); yield return new CatchModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new CatchModEasy(); yield return new CatchModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new CatchModFlashlight(); yield return new CatchModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new CatchModHalfTime(); yield return new CatchModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new CatchModHardRock(); yield return new CatchModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new CatchModHidden(); yield return new CatchModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new CatchModNoFail(); yield return new CatchModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new CatchModRelax(); yield return new CatchModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit
result.ScreenSpacePosition.X = screenSpacePosition.X; result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlagFast(SnapType.RelativeGrids)) if (snapType.HasFlag(SnapType.RelativeGrids))
{ {
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
@ -46,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TickDistanceMultiplier = 1; public double TickDistanceMultiplier = 1;
[JsonIgnore] [JsonIgnore]
private double velocityFactor; public double Velocity { get; private set; }
[JsonIgnore] [JsonIgnore]
private double tickDistanceFactor; public double TickDistance { get; private set; }
[JsonIgnore]
public double Velocity => velocityFactor * SliderVelocityMultiplier;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.
@ -68,8 +63,13 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
double scoringDistance = Velocity * timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -2,6 +2,8 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = new ManiaModInvert(), Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
}); });
[Test]
public void TestBreaksPreservedOnOriginalBeatmap()
{
var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo);
beatmap.Breaks.Clear();
beatmap.Breaks.Add(new BreakPeriod(0, 1000));
var workingBeatmap = new FlatWorkingBeatmap(beatmap);
var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() });
Assert.That(playableWithInvert.Breaks.Count, Is.Zero);
var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo);
Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero);
Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000)));
}
} }
} }

View File

@ -0,0 +1,643 @@
// 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 System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModNoRelease : RateAdjustedBeatmapTestScene
{
private const double time_before_head = 250;
private const double time_head = 1500;
private const double time_during_hold_1 = 2500;
private const double time_tail = 4000;
private const double time_after_tail = 5250;
private List<JudgementResult> judgementResults = new List<JudgementResult>();
/// <summary>
/// -----[ ]-----
/// o o
/// </summary>
[Test]
public void TestNoInput()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestCorrectInput()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
assertNoteJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestLateRelease()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
assertNoteJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestPressTooEarlyAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail, ManiaAction.Key1),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestPressTooEarlyAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// xo x o
/// </summary>
[Test]
public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_before_head + 10),
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
}
/// <summary>
/// -----[ ]-----
/// xo x o
/// </summary>
[Test]
public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_before_head + 10),
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
}
/// <summary>
/// -----[ ]-----
/// xo o
/// </summary>
[Test]
public void TestPressAtStartAndBreak()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 10),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// xox o
/// </summary>
[Test]
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 1),
new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertComboAtJudgement(0, 1);
assertTailJudgement(HitResult.Meh);
assertComboAtJudgement(1, 0);
assertComboAtJudgement(3, 1);
}
/// <summary>
/// -----[ ]-----
/// xo x o
/// </summary>
[Test]
public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 10),
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-----
/// xo x o o
/// </summary>
[Test]
public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 10),
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestPressDuringNoteAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-----
/// x o o
/// </summary>
[Test]
public void TestPressDuringNoteAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]--------------
/// xo
/// </summary>
[Test]
public void TestPressAndReleaseAfterTailWithCloseByHead()
{
const int duration = 30;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
// hold note is very short, to make the head still in range
new HoldNote
{
StartTime = time_head,
Duration = duration,
Column = 0,
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head + duration + 60, ManiaAction.Key1),
new ManiaReplayFrame(time_head + duration + 70),
}, beatmap);
assertHeadJudgement(HitResult.Ok);
assertTailJudgement(HitResult.Perfect);
}
/// <summary>
/// -----[ ]-O-------------
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead()
{
Note note;
const int duration = 50;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
// hold note is very short, to make the head still in range
new HoldNote
{
StartTime = time_head,
Duration = duration,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_head + duration + 10
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head + duration, ManiaAction.Key1),
new ManiaReplayFrame(time_head + duration + 10),
}, beatmap);
assertHeadJudgement(HitResult.Good);
assertTailJudgement(HitResult.Perfect);
assertHitObjectJudgement(note, HitResult.Miss);
}
/// <summary>
/// -----[ ]--O--
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustBeforeTailWithNearbyNote()
{
Note note;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_tail + 50
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Good);
}
/// <summary>
/// -----[ ]-----
/// xo
/// </summary>
[Test]
public void TestPressAndReleaseJustAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail + 20, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 30),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]--O--
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
{
// Next note within tail lenience
Note note = new Note { StartTime = time_tail + 50 };
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
},
note
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 20),
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Great);
}
/// <summary>
/// -----[ ]-----
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 10),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
[Test]
public void TestMissReleaseAndHitSecondRelease()
{
var windows = new ManiaHitWindows();
windows.SetDifficulty(10);
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = 1000,
Duration = 500,
Column = 0,
},
new HoldNote
{
StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
Duration = 500,
Column = 0,
},
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
SliderTickRate = 4,
OverallDifficulty = 10,
},
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
}, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => !j.Type.IsHit()));
AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit()));
}
[Test]
public void TestZeroLength()
{
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = 1000,
Duration = 0,
Column = 0,
},
},
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1),
new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1),
}, beatmap);
AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit()));
}
private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
=> AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result));
private void assertTailJudgement(HitResult result)
=> AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result));
private void assertNoteJudgement(HitResult result)
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
private void assertComboAtJudgement(int judgementIndex, int combo)
=> AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
private ScoreAccessibleReplayPlayer currentPlayer = null!;
private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject>? beatmap = null)
{
if (beatmap == null)
{
beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo,
},
};
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>
{
SelectedMods.Value = new List<Mod>
{
new ManiaModNoRelease()
};
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -474,8 +474,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => !j.Type.IsHit())); .All(j => !j.Type.IsHit()));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit())); .All(j => j.Type.IsHit()));
} }
[Test] [Test]

View File

@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests
} }
private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor)
=> hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor); => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor);
private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor)
=> verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osuTK; using osuTK;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
else else
convertType |= PatternType.LowProbability; convertType |= PatternType.LowProbability;
if (!convertType.HasFlagFast(PatternType.KeepSingle)) if (!convertType.HasFlag(PatternType.KeepSingle))
{ {
if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8)
convertType |= PatternType.Mirror; convertType |= PatternType.Mirror;
@ -102,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
{ {
// Generate a new pattern by copying the last hit objects in reverse-column order // Generate a new pattern by copying the last hit objects in reverse-column order
for (int i = RandomStart; i < TotalColumns; i++) for (int i = RandomStart; i < TotalColumns; i++)
@ -114,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key // If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0) && (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column // Make sure the last column was not the centre column
@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
{ {
// Generate a new pattern by placing on the already filled columns // Generate a new pattern by placing on the already filled columns
for (int i = RandomStart; i < TotalColumns; i++) for (int i = RandomStart; i < TotalColumns; i++)
@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (PreviousPattern.HitObjects.Count() == 1) if (PreviousPattern.HitObjects.Count() == 1)
{ {
if (convertType.HasFlagFast(PatternType.Stair)) if (convertType.HasFlag(PatternType.Stair))
{ {
// Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next"
int targetColumn = lastColumn + 1; int targetColumn = lastColumn + 1;
@ -152,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (convertType.HasFlagFast(PatternType.ReverseStair)) if (convertType.HasFlag(PatternType.ReverseStair))
{ {
// Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous"
int targetColumn = lastColumn - 1; int targetColumn = lastColumn - 1;
@ -164,10 +163,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
} }
if (convertType.HasFlagFast(PatternType.KeepSingle)) if (convertType.HasFlag(PatternType.KeepSingle))
return generateRandomNotes(1); return generateRandomNotes(1);
if (convertType.HasFlagFast(PatternType.Mirror)) if (convertType.HasFlag(PatternType.Mirror))
{ {
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(0.78, 0.42, 0, 0);
return generateRandomPattern(1, 0.62, 0, 0); return generateRandomPattern(1, 0.62, 0, 0);
@ -187,7 +186,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4) if (ConversionDifficulty > 4)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.35, 0.08, 0, 0);
return generateRandomPattern(0.52, 0.15, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0);
@ -195,7 +194,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2) if (ConversionDifficulty > 2)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.18, 0, 0, 0);
return generateRandomPattern(0.45, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0);
@ -208,9 +207,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in p.HitObjects) foreach (var obj in p.HitObjects)
{ {
if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1) if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair; StairType = PatternType.ReverseStair;
if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart) if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair; StairType = PatternType.Stair;
} }
@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
var pattern = new Pattern(); var pattern = new Pattern();
bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack); bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack);
if (!allowStacking) if (!allowStacking)
noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects);
@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int getNextColumn(int last) int getNextColumn(int last)
{ {
if (convertType.HasFlagFast(PatternType.Gathered)) if (convertType.HasFlag(PatternType.Gathered))
{ {
last++; last++;
if (last == TotalColumns) if (last == TotalColumns)
@ -297,7 +296,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{ {
if (convertType.HasFlagFast(PatternType.ForceNotStack)) if (convertType.HasFlag(PatternType.ForceNotStack))
return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
var pattern = new Pattern(); var pattern = new Pattern();

View File

@ -7,7 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
@ -147,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4) if (ConversionDifficulty > 4)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(StartTime, 0.56, 0.18, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
@ -155,13 +154,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2.5) if (ConversionDifficulty > 2.5)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.3, 0, 0);
return generateNRandomNotes(StartTime, 0.37, 0.08, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
} }
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.17, 0, 0);
return generateNRandomNotes(StartTime, 0.27, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0);
@ -219,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); var pattern = new Pattern();
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn; int lastColumn = nextColumn;
@ -371,7 +370,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes) if (canGenerateTwoNotes)
@ -404,7 +403,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int endTime = startTime + SegmentDuration * SpanCount; int endTime = startTime + SegmentDuration * SpanCount;
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++) for (int i = 0; i < columnRepeat; i++)
@ -433,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); var pattern = new Pattern();
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note // Create the hold note

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -89,79 +88,79 @@ namespace osu.Game.Rulesets.Mania
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore(); yield return new ManiaModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime(); yield return new ManiaModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new ManiaModPerfect(); yield return new ManiaModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new ManiaModSuddenDeath(); yield return new ManiaModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new ManiaModCinema(); yield return new ManiaModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new ManiaModAutoplay(); yield return new ManiaModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new ManiaModEasy(); yield return new ManiaModEasy();
if (mods.HasFlagFast(LegacyMods.FadeIn)) if (mods.HasFlag(LegacyMods.FadeIn))
yield return new ManiaModFadeIn(); yield return new ManiaModFadeIn();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new ManiaModFlashlight(); yield return new ManiaModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new ManiaModHalfTime(); yield return new ManiaModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new ManiaModHardRock(); yield return new ManiaModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new ManiaModHidden(); yield return new ManiaModHidden();
if (mods.HasFlagFast(LegacyMods.Key1)) if (mods.HasFlag(LegacyMods.Key1))
yield return new ManiaModKey1(); yield return new ManiaModKey1();
if (mods.HasFlagFast(LegacyMods.Key2)) if (mods.HasFlag(LegacyMods.Key2))
yield return new ManiaModKey2(); yield return new ManiaModKey2();
if (mods.HasFlagFast(LegacyMods.Key3)) if (mods.HasFlag(LegacyMods.Key3))
yield return new ManiaModKey3(); yield return new ManiaModKey3();
if (mods.HasFlagFast(LegacyMods.Key4)) if (mods.HasFlag(LegacyMods.Key4))
yield return new ManiaModKey4(); yield return new ManiaModKey4();
if (mods.HasFlagFast(LegacyMods.Key5)) if (mods.HasFlag(LegacyMods.Key5))
yield return new ManiaModKey5(); yield return new ManiaModKey5();
if (mods.HasFlagFast(LegacyMods.Key6)) if (mods.HasFlag(LegacyMods.Key6))
yield return new ManiaModKey6(); yield return new ManiaModKey6();
if (mods.HasFlagFast(LegacyMods.Key7)) if (mods.HasFlag(LegacyMods.Key7))
yield return new ManiaModKey7(); yield return new ManiaModKey7();
if (mods.HasFlagFast(LegacyMods.Key8)) if (mods.HasFlag(LegacyMods.Key8))
yield return new ManiaModKey8(); yield return new ManiaModKey8();
if (mods.HasFlagFast(LegacyMods.Key9)) if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9(); yield return new ManiaModKey9();
if (mods.HasFlagFast(LegacyMods.KeyCoop)) if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages(); yield return new ManiaModDualStages();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail(); yield return new ManiaModNoFail();
if (mods.HasFlagFast(LegacyMods.Random)) if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom(); yield return new ManiaModRandom();
if (mods.HasFlagFast(LegacyMods.Mirror)) if (mods.HasFlag(LegacyMods.Mirror))
yield return new ManiaModMirror(); yield return new ManiaModMirror();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }
@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModEasy(), new ManiaModEasy(),
new ManiaModNoFail(), new ManiaModNoFail(),
new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()), new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()),
new ManiaModNoRelease(),
}; };
case ModType.DifficultyIncrease: case ModType.DifficultyIncrease:

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public partial class ManiaModNoRelease : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset<ManiaHitObject>
{
public override string Name => "No Release";
public override string Acronym => "NR";
public override LocalisableString Description => "No more timing the end of hold notes.";
public override double ScoreMultiplier => 0.9;
public override ModType Type => ModType.DifficultyReduction;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var hitObjects = maniaBeatmap.HitObjects.Select(obj =>
{
if (obj is HoldNote hold)
return new NoReleaseHoldNote(hold);
return obj;
}).ToList();
maniaBeatmap.HitObjects = hitObjects;
}
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
foreach (var stage in maniaRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
column.RegisterPool<NoReleaseTailNote, NoReleaseDrawableHoldNoteTail>(10, 50);
}
}
}
private partial class NoReleaseDrawableHoldNoteTail : DrawableHoldNoteTail
{
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// apply perfect once the tail is reached
if (HoldNote.HoldStartTime != null && timeOffset >= 0)
ApplyResult(GetCappedResult(HitResult.Perfect));
else
base.CheckForResult(userTriggered, timeOffset);
}
}
private class NoReleaseTailNote : TailNote
{
}
private class NoReleaseHoldNote : HoldNote
{
public NoReleaseHoldNote(HoldNote hold)
{
StartTime = hold.StartTime;
Duration = hold.Duration;
Column = hold.Column;
NodeSamples = hold.NodeSamples;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
AddNested(Head = new HeadNote
{
StartTime = StartTime,
Column = Column,
Samples = GetNodeSamples(0),
});
AddNested(Tail = new NoReleaseTailNote
{
StartTime = EndTime,
Column = Column,
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
AddNested(Body = new HoldNoteBody
{
StartTime = StartTime,
Column = Column
});
}
}
}
}

View File

@ -72,18 +72,18 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary> /// <summary>
/// The head note of the hold. /// The head note of the hold.
/// </summary> /// </summary>
public HeadNote Head { get; private set; } public HeadNote Head { get; protected set; }
/// <summary> /// <summary>
/// The tail note of the hold. /// The tail note of the hold.
/// </summary> /// </summary>
public TailNote Tail { get; private set; } public TailNote Tail { get; protected set; }
/// <summary> /// <summary>
/// The body of the hold. /// The body of the hold.
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>. /// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
/// </summary> /// </summary>
public HoldNoteBody Body { get; private set; } public HoldNoteBody Body { get; protected set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;

View File

@ -192,12 +192,12 @@ namespace osu.Game.Rulesets.Mania.UI
if (press) if (press)
{ {
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value); inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
} }
else else
{ {
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value); inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint); highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
} }
} }

View File

@ -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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -160,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return grid switch return grid switch
{ {
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
_ => Vector2.Zero _ => Vector2.Zero
}; };
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
@ -177,6 +178,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addAssertPointPositionChanged(points, i); addAssertPointPositionChanged(points, i);
} }
[Test]
public void TestChangingControlPointTypeViaTab()
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.LINEAR);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
addControlPointStep(new Vector2(500, 100));
AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertControlPointPathType(0, PathType.BEZIER);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(0, PathType.LINEAR);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(0, PathType.BSpline(4));
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(0, PathType.PERFECT_CURVE);
assertControlPointPathType(2, PathType.BSpline(4));
AddStep("select third last control point", () =>
{
visualiser.Pieces[0].IsSelected.Value = false;
visualiser.Pieces[2].IsSelected.Value = true;
});
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(2, PathType.PERFECT_CURVE);
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(2, null);
AddStep("select first and third control points", () =>
{
visualiser.Pieces[0].IsSelected.Value = true;
visualiser.Pieces[2].IsSelected.Value = true;
});
AddStep("press alt-1", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Number1);
InputManager.ReleaseKey(Key.AltLeft);
});
assertControlPointPathType(0, PathType.LINEAR);
assertControlPointPathType(2, PathType.LINEAR);
}
private void addAssertPointPositionChanged(Vector2[] points, int index) private void addAssertPointPositionChanged(Vector2[] points, int index)
{ {
AddAssert($"Point at {points.ElementAt(index)} changed", AddAssert($"Point at {points.ElementAt(index)} changed",

View File

@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; 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.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(200); assertLength(200);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
} }
[Test] [Test]
@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
} }
[Test] [Test]
@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100)); assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR); assertFinalControlPointType(1, PathType.LINEAR);
} }
[Test] [Test]
@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertLength(100); assertLength(100);
} }
@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR); assertFinalControlPointType(1, PathType.LINEAR);
} }
[Test] [Test]
@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE); assertFinalControlPointType(1, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200)); assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, PathType.PERFECT_CURVE); assertFinalControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestManualPathTypeControlViaKeyboard()
{
addMovementStep(new Vector2(200));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(300, 200));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(300));
assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE);
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
assertControlPointTypeDuringPlacement(0, PathType.LINEAR);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.ShiftLeft);
});
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
AddStep("start new segment via S", () => InputManager.Key(Key.S));
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
addMovementStep(new Vector2(400, 300));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(400));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right); addClickStep(MouseButton.Right);
assertPlaced(true); assertPlaced(true);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(808, tolerance: 10); assertLength(808, tolerance: 10);
assertControlPointCount(5); assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(4)); assertFinalControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null); assertFinalControlPointType(1, null);
assertControlPointType(2, null); assertFinalControlPointType(2, null);
assertControlPointType(3, null); assertFinalControlPointType(3, null);
assertControlPointType(4, null); assertFinalControlPointType(4, null);
} }
[Test] [Test]
@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(600, tolerance: 10); assertLength(600, tolerance: 10);
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointType(0, PathType.BSpline(4)); assertFinalControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, PathType.BSpline(4)); assertFinalControlPointType(1, PathType.BSpline(4));
assertControlPointType(2, PathType.BSpline(4)); assertFinalControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null); assertFinalControlPointType(3, null);
} }
[Test] [Test]
@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected)); private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}",
() => this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type));
private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointPosition(int index, Vector2 position) => private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath where T : OsuHitObject, IHasPath
{ {
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
internal readonly Container<PathControlPointPiece<T>> Pieces; internal readonly Container<PathControlPointPiece<T>> Pieces;
@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (allowSelection) if (allowSelection)
d.RequestSelection = selectionRequested; d.RequestSelection = selectionRequested;
d.ControlPoint.Changed += controlPointChanged;
d.DragStarted = DragStarted; d.DragStarted = DragStarted;
d.DragInProgress = DragInProgress; d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded; d.DragEnded = DragEnded;
@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
foreach (var point in e.OldItems.Cast<PathControlPoint>()) foreach (var point in e.OldItems.Cast<PathControlPoint>())
{ {
point.Changed -= controlPointChanged;
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately(); piece.RemoveAndDisposeImmediately();
} }
@ -217,6 +220,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
} }
private void controlPointChanged() => updateCurveMenuItems();
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (Pieces.Any(piece => piece.IsHovered)) if (Pieces.Any(piece => piece.IsHovered))
@ -245,6 +250,86 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
} }
// ReSharper disable once StaticMemberInGenericType
private static readonly PathType?[] path_types =
[
PathType.LINEAR,
PathType.BEZIER,
PathType.PERFECT_CURVE,
PathType.BSpline(4),
null,
];
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
switch (e.Key)
{
case Key.Tab:
{
var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray();
if (selectedPieces.Length != 1)
return false;
var selectedPiece = selectedPieces.Single();
var selectedPoint = selectedPiece.ControlPoint;
var validTypes = path_types;
if (selectedPoint == controlPoints[0])
validTypes = validTypes.Where(t => t != null).ToArray();
int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type);
if (currentTypeIndex < 0 && e.ShiftPressed)
currentTypeIndex = 0;
changeHandler?.BeginChange();
do
{
currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length;
updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]);
} while (selectedPoint.Type != validTypes[currentTypeIndex]);
changeHandler?.EndChange();
return true;
}
case Key.Number1:
case Key.Number2:
case Key.Number3:
case Key.Number4:
case Key.Number5:
{
if (!e.AltPressed)
return false;
var type = path_types[e.Key - Key.Number1];
if (Pieces[0].IsSelected.Value && type == null)
return false;
updatePathTypeOfSelectedPieces(type);
return true;
}
default:
return false;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var p in Pieces)
p.ControlPoint.Changed -= controlPointChanged;
}
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e) private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
{ {
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
@ -254,30 +339,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
/// <summary> /// <summary>
/// Attempts to set the given control point piece to the given path type. /// Attempts to set all selected control point pieces to the given path type.
/// If that would fail, try to change the path such that it instead succeeds /// If that fails, try to change the path such that it instead succeeds
/// in a UX-friendly way. /// in a UX-friendly way.
/// </summary> /// </summary>
/// <param name="piece">The control point piece that we want to change the path type of.</param>
/// <param name="type">The path type we want to assign to the given control point piece.</param> /// <param name="type">The path type we want to assign to the given control point piece.</param>
private void updatePathType(PathControlPointPiece<T> piece, PathType? type) private void updatePathTypeOfSelectedPieces(PathType? type)
{ {
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint); changeHandler?.BeginChange();
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
if (type?.Type == SplineType.PerfectCurve) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
{ {
// Can't always create a circular arc out of 4 or more points, var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
// so we split the segment into one 3-point circular arc segment int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (pointsInSegment.Count > thirdPointIndex + 1) if (type?.Type == SplineType.PerfectCurve)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; {
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (pointsInSegment.Count > thirdPointIndex + 1)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
}
hitObject.Path.ExpectedDistance.Value = null;
p.ControlPoint.Type = type;
} }
hitObject.Path.ExpectedDistance.Value = null; EnsureValidPathTypes();
piece.ControlPoint.Type = type;
changeHandler?.EndChange();
} }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -290,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private int draggedControlPointIndex; private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints; private HashSet<PathControlPoint> selectedControlPoints;
private List<MenuItem> curveTypeItems;
public void DragStarted(PathControlPoint controlPoint) public void DragStarted(PathControlPoint controlPoint)
{ {
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
@ -386,22 +481,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
var splittablePieces = selectedPieces.Where(isSplittable).ToList(); var splittablePieces = selectedPieces.Where(isSplittable).ToList();
int splittableCount = splittablePieces.Count; int splittableCount = splittablePieces.Count;
List<MenuItem> curveTypeItems = new List<MenuItem>(); curveTypeItems = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0])) foreach (PathType? type in path_types)
{ {
curveTypeItems.Add(createMenuItemForPathType(null)); // special inherit case
curveTypeItems.Add(new OsuMenuItemSpacer()); if (type == null)
{
if (selectedPieces.Contains(Pieces[0]))
continue;
curveTypeItems.Add(new OsuMenuItemSpacer());
}
curveTypeItems.Add(createMenuItemForPathType(type));
} }
// todo: hide/disable items which aren't valid for selected points
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull)) if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
{
curveTypeItems.Add(new OsuMenuItemSpacer());
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
}
var menuItems = new List<MenuItem> var menuItems = new List<MenuItem>
{ {
@ -424,35 +524,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
() => DeleteSelected()) () => DeleteSelected())
); );
updateCurveMenuItems();
return menuItems.ToArray(); return menuItems.ToArray();
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
} }
} }
private MenuItem createMenuItemForPathType(PathType? type) private void updateCurveMenuItems()
{ {
int totalCount = Pieces.Count(p => p.IsSelected.Value); if (curveTypeItems == null)
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); return;
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => foreach (var item in curveTypeItems.OfType<CurveTypeMenuItem>())
{ {
changeHandler?.BeginChange(); int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == item.PathType);
foreach (var p in Pieces.Where(p => p.IsSelected.Value)) if (countOfState == totalCount)
updatePathType(p, type); item.State.Value = TernaryState.True;
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
}
}
EnsureValidPathTypes(); private class CurveTypeMenuItem : TernaryStateRadioMenuItem
{
public readonly PathType? PathType;
changeHandler?.EndChange(); public CurveTypeMenuItem(PathType? pathType, Action<TernaryState> action)
}); : base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action)
{
if (countOfState == totalCount) PathType = pathType;
item.State.Value = TernaryState.True; }
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
return item;
} }
} }
} }

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private PathControlPoint segmentStart; private PathControlPoint segmentStart;
private PathControlPoint cursor; private PathControlPoint cursor;
private int currentSegmentLength; private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
[CanBeNull] [CanBeNull]
@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.ControlPoints: case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint)) if (canPlaceNewControlPoint(out var lastPoint))
{ placeNewControlPoint();
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
else else
{ beginNewSegment(lastPoint);
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
break; break;
} }
@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true; return true;
} }
private void beginNewSegment(PathControlPoint lastPoint)
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
usingCustomSegmentType = false;
}
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
if (e.Button != MouseButton.Left) if (e.Button != MouseButton.Left)
@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnMouseUp(e); base.OnMouseUp(e);
} }
private static readonly PathType[] path_types =
[
PathType.LINEAR,
PathType.BEZIER,
PathType.PERFECT_CURVE,
PathType.BSpline(4),
];
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
if (state != SliderPlacementState.ControlPoints)
return false;
switch (e.Key)
{
case Key.S:
{
if (!canPlaceNewControlPoint(out _))
return false;
placeNewControlPoint();
var last = HitObject.Path.ControlPoints.Last(p => p != cursor);
beginNewSegment(last);
return true;
}
case Key.Number1:
case Key.Number2:
case Key.Number3:
case Key.Number4:
{
if (!e.AltPressed)
return false;
usingCustomSegmentType = true;
segmentStart.Type = path_types[e.Key - Key.Number1];
controlPointVisualiser.EnsureValidPathTypes();
return true;
}
case Key.Tab:
{
usingCustomSegmentType = true;
int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1;
if (currentTypeIndex < 0 && e.ShiftPressed)
currentTypeIndex = 0;
do
{
currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length;
segmentStart.Type = path_types[currentTypeIndex];
controlPointVisualiser.EnsureValidPathTypes();
} while (segmentStart.Type != path_types[currentTypeIndex]);
return true;
}
}
return false;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePathType() private void updatePathType()
{ {
if (usingCustomSegmentType)
{
controlPointVisualiser.EnsureValidPathTypes();
return;
}
if (state == SliderPlacementState.Drawing) if (state == SliderPlacementState.Drawing)
{ {
segmentStart.Type = PathType.BSpline(4); segmentStart.Type = PathType.BSpline(4);
@ -316,6 +389,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return lastPiece.IsHovered != true; return lastPiece.IsHovered != true;
} }
private void placeNewControlPoint()
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
private void updateSlider() private void updateSlider()
{ {
if (state == SliderPlacementState.Drawing) if (state == SliderPlacementState.Drawing)

View File

@ -55,7 +55,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; } private BindableBeatDivisor beatDivisor { get; set; }
public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad; public override Quad SelectionQuad
{
get
{
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
if (ControlPointVisualiser != null)
{
foreach (var piece in ControlPointVisualiser.Pieces)
result = RectangleF.Union(result, piece.ScreenSpaceDrawQuad.AABBFloat);
}
return result;
}
}
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>(); private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly IBindable<int> pathVersion = new Bindable<int>(); private readonly IBindable<int> pathVersion = new Bindable<int>();

View File

@ -1,17 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
@ -20,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved] [Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!; private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IExpandingContainer? expandingContainer { get; set; }
/// <summary> /// <summary>
/// X position of the grid's origin. /// X position of the grid's origin.
/// </summary> /// </summary>
@ -55,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary> /// </summary>
public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f)
{ {
MinValue = -45f, MinValue = -180f,
MaxValue = 45f, MaxValue = 180f,
Precision = 1f Precision = 1f
}; };
@ -72,10 +82,13 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary> /// </summary>
public Bindable<Vector2> SpacingVector { get; } = new Bindable<Vector2>(); public Bindable<Vector2> SpacingVector { get; } = new Bindable<Vector2>();
public Bindable<PositionSnapGridType> GridType { get; } = new Bindable<PositionSnapGridType>();
private ExpandableSlider<float> startPositionXSlider = null!; private ExpandableSlider<float> startPositionXSlider = null!;
private ExpandableSlider<float> startPositionYSlider = null!; private ExpandableSlider<float> startPositionYSlider = null!;
private ExpandableSlider<float> spacingSlider = null!; private ExpandableSlider<float> spacingSlider = null!;
private ExpandableSlider<float> gridLinesRotationSlider = null!; private ExpandableSlider<float> gridLinesRotationSlider = null!;
private EditorRadioButtonCollection gridTypeButtons = null!;
public OsuGridToolboxGroup() public OsuGridToolboxGroup()
: base("grid") : base("grid")
@ -109,6 +122,31 @@ namespace osu.Game.Rulesets.Osu.Edit
Current = GridLinesRotation, Current = GridLinesRotation,
KeyboardStep = 1, KeyboardStep = 1,
}, },
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Children = new Drawable[]
{
gridTypeButtons = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Square",
() => GridType.Value = PositionSnapGridType.Square,
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Triangle",
() => GridType.Value = PositionSnapGridType.Triangle,
() => new OutlineTriangle(true, 20)),
new RadioButton("Circle",
() => GridType.Value = PositionSnapGridType.Circle,
() => new SpriteIcon { Icon = FontAwesome.Regular.Circle }),
}
},
}
},
}; };
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; Spacing.Value = editorBeatmap.BeatmapInfo.GridSize;
@ -118,6 +156,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
base.LoadComplete(); base.LoadComplete();
gridTypeButtons.Items.First().Select();
StartPositionX.BindValueChanged(x => StartPositionX.BindValueChanged(x =>
{ {
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
@ -145,6 +185,32 @@ namespace osu.Game.Rulesets.Osu.Edit
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}"; gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
}, true); }, true);
expandingContainer?.Expanded.BindValueChanged(v =>
{
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
}, true);
GridType.BindValueChanged(v =>
{
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
switch (v.NewValue)
{
case PositionSnapGridType.Square:
GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45;
GridLinesRotation.MinValue = -45;
GridLinesRotation.MaxValue = 45;
break;
case PositionSnapGridType.Triangle:
GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30;
GridLinesRotation.MinValue = -30;
GridLinesRotation.MaxValue = 30;
break;
}
}, true);
} }
private void nextGridSize() private void nextGridSize()
@ -167,5 +233,42 @@ namespace osu.Game.Rulesets.Osu.Edit
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{ {
} }
public partial class OutlineTriangle : BufferedContainer
{
public OutlineTriangle(bool outlineOnly, float size)
: base(cachedFrameBuffer: true)
{
Size = new Vector2(size);
InternalChildren = new Drawable[]
{
new EquilateralTriangle { RelativeSizeAxes = Axes.Both },
};
if (outlineOnly)
{
AddInternal(new EquilateralTriangle
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Y,
Y = 0.48f,
Colour = Color4.Black,
Size = new Vector2(size - 7),
Blending = BlendingParameters.None,
});
}
Blending = BlendingParameters.Additive;
}
}
}
public enum PositionSnapGridType
{
Square,
Triangle,
Circle,
} }
} }

View File

@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -101,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
updatePositionSnapGrid(); OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true);
RightToolbox.AddRange(new Drawable[] RightToolbox.AddRange(new Drawable[]
{ {
@ -116,18 +115,45 @@ namespace osu.Game.Rulesets.Osu.Edit
); );
} }
private void updatePositionSnapGrid() private void updatePositionSnapGrid(ValueChangedEvent<PositionSnapGridType> obj)
{ {
if (positionSnapGrid != null) if (positionSnapGrid != null)
LayerBelowRuleset.Remove(positionSnapGrid, true); LayerBelowRuleset.Remove(positionSnapGrid, true);
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); switch (obj.NewValue)
{
case PositionSnapGridType.Square:
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = rectangularPositionSnapGrid; positionSnapGrid = rectangularPositionSnapGrid;
break;
case PositionSnapGridType.Triangle:
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = triangularPositionSnapGrid;
break;
case PositionSnapGridType.Circle:
var circularPositionSnapGrid = new CircularPositionSnapGrid();
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
positionSnapGrid = circularPositionSnapGrid;
break;
default:
throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type.");
}
// Bind the start position to the toolbox sliders.
positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
positionSnapGrid.RelativeSizeAxes = Axes.Both; positionSnapGrid.RelativeSizeAxes = Axes.Both;
LayerBelowRuleset.Add(positionSnapGrid); LayerBelowRuleset.Add(positionSnapGrid);
@ -194,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{ {
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{ {
// In the case of snapping to nearby objects, a time value is not provided. // In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
@ -204,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same. // the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
@ -216,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.RelativeGrids)) if (snapType.HasFlag(SnapType.RelativeGrids))
{ {
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
@ -227,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
if (snapType.HasFlagFast(SnapType.GlobalGrids)) if (snapType.HasFlag(SnapType.GlobalGrids))
{ {
if (rectangularGridSnapToggle.Value == TernaryState.True) if (rectangularGridSnapToggle.Value == TernaryState.True)
{ {

View File

@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public SliderCompositionTool() public SliderCompositionTool()
: base(nameof(Slider)) : base(nameof(Slider))
{ {
TooltipText = """
Left click for new point.
Left click twice or S key for new segment.
Tab, Shift-Tab, or Alt-1~4 to change current segment type.
Right click to finish.
Click and drag for drawing mode.
""";
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -70,55 +69,55 @@ namespace osu.Game.Rulesets.Osu
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new OsuModNightcore(); yield return new OsuModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new OsuModDoubleTime(); yield return new OsuModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new OsuModPerfect(); yield return new OsuModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new OsuModSuddenDeath(); yield return new OsuModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Autopilot)) if (mods.HasFlag(LegacyMods.Autopilot))
yield return new OsuModAutopilot(); yield return new OsuModAutopilot();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new OsuModCinema(); yield return new OsuModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new OsuModAutoplay(); yield return new OsuModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new OsuModEasy(); yield return new OsuModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new OsuModFlashlight(); yield return new OsuModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new OsuModHalfTime(); yield return new OsuModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new OsuModHardRock(); yield return new OsuModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new OsuModHidden(); yield return new OsuModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new OsuModNoFail(); yield return new OsuModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new OsuModRelax(); yield return new OsuModRelax();
if (mods.HasFlagFast(LegacyMods.SpunOut)) if (mods.HasFlag(LegacyMods.SpunOut))
yield return new OsuModSpunOut(); yield return new OsuModSpunOut();
if (mods.HasFlagFast(LegacyMods.Target)) if (mods.HasFlag(LegacyMods.Target))
yield return new OsuModTargetPractice(); yield return new OsuModTargetPractice();
if (mods.HasFlagFast(LegacyMods.TouchDevice)) if (mods.HasFlag(LegacyMods.TouchDevice))
yield return new OsuModTouchDevice(); yield return new OsuModTouchDevice();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }

View File

@ -7,7 +7,6 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering;
@ -243,14 +242,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
originPosition = Vector2.Zero; originPosition = Vector2.Zero;
if (Source.TrailOrigin.HasFlagFast(Anchor.x1)) if (Source.TrailOrigin.HasFlag(Anchor.x1))
originPosition.X = 0.5f; originPosition.X = 0.5f;
else if (Source.TrailOrigin.HasFlagFast(Anchor.x2)) else if (Source.TrailOrigin.HasFlag(Anchor.x2))
originPosition.X = 1f; originPosition.X = 1f;
if (Source.TrailOrigin.HasFlagFast(Anchor.y1)) if (Source.TrailOrigin.HasFlag(Anchor.y1))
originPosition.Y = 0.5f; originPosition.Y = 0.5f;
else if (Source.TrailOrigin.HasFlagFast(Anchor.y2)) else if (Source.TrailOrigin.HasFlag(Anchor.y2))
originPosition.Y = 1f; originPosition.Y = 1f;
Source.parts.CopyTo(parts, 0); Source.parts.CopyTo(parts, 0);

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer(); protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer();
public override Quad SkinnableComponentScreenSpaceDrawQuad => playfieldBorder.ScreenSpaceDrawQuad;
private readonly Container judgementAboveHitObjectLayer; private readonly Container judgementAboveHitObjectLayer;
public OsuPlayfield() public OsuPlayfield()

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -79,43 +78,43 @@ namespace osu.Game.Rulesets.Taiko
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new TaikoModNightcore(); yield return new TaikoModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new TaikoModDoubleTime(); yield return new TaikoModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new TaikoModPerfect(); yield return new TaikoModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new TaikoModSuddenDeath(); yield return new TaikoModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new TaikoModCinema(); yield return new TaikoModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new TaikoModAutoplay(); yield return new TaikoModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new TaikoModEasy(); yield return new TaikoModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new TaikoModFlashlight(); yield return new TaikoModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new TaikoModHalfTime(); yield return new TaikoModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new TaikoModHardRock(); yield return new TaikoModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new TaikoModHidden(); yield return new TaikoModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new TaikoModNoFail(); yield return new TaikoModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new TaikoModRelax(); yield return new TaikoModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }

View File

@ -528,8 +528,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
// The control point at the end time of the slider should be applied // The fourth object is a slider.
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); // `Samples` of a slider are presumed to control the volume of sounds that last the entire duration of the slider
// (such as ticks, slider slide sounds, etc.)
// Thus, the point of query of control points used for `Samples` is just beyond the start time of the slider.
Assert.AreEqual("Gameplay/soft-hitnormal11", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
// That said, the `NodeSamples` of the slider are responsible for the sounds of the slider's head / tail / repeats / large ticks etc.
// Therefore, they should be read at the time instant correspondent to the given node.
// This means that the tail should use bank 8 rather than 11.
Assert.AreEqual("Gameplay/soft-hitnormal11", ((ConvertSlider)hitObjects[4]).NodeSamples[0][0].LookupNames.First());
Assert.AreEqual("Gameplay/soft-hitnormal8", ((ConvertSlider)hitObjects[4]).NodeSamples[1][0].LookupNames.First());
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];

View File

@ -0,0 +1,235 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckTitleMarkersTest
{
private CheckTitleMarkers check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckTitleMarkers();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Title = "Egao no Kanata",
TitleUnicode = "エガオノカナタ"
}
}
};
}
[Test]
public void TestNoTitleMarkers()
{
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestTvSizeMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (TV Size)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedTvSizeMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (tv size)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestGameVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Game Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Game Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedGameVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (game ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (game ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestShortVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Short Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Short Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedShortVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (short ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (short ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestSpedUpVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Sped Up Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedSpedUpVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (sped up ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestNightcoreMixMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Nightcore Mix)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore Mix)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedNightcoreMixMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (nightcore mix)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore mix)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestSpedUpCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Sped Up & Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up & Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedSpedUpCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (sped up & cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up & cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestNightcoreCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Nightcore & Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore & Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedNightcoreCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (nightcore & cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore & cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
private BeatmapVerifierContext getContext(IBeatmap beatmap)
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,43 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class EditorTimestampParserTest
{
private static readonly object?[][] test_cases =
{
new object?[] { ":", false, null, null },
new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null },
new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null },
new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null },
new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null },
new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null },
new object?[] { "1:92", false, null, null },
new object?[] { "1:002", false, null, null },
new object?[] { "1:02:3", true, new TimeSpan(0, 0, 1, 2, 3), null },
new object?[] { "1:02:300", true, new TimeSpan(0, 0, 1, 2, 300), null },
new object?[] { "1:02:3000", false, null, null },
new object?[] { "1:02:300 ()", false, null, null },
new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
};
[TestCaseSource(nameof(test_cases))]
public void TestTryParse(string timestamp, bool expectedSuccess, TimeSpan? expectedParsedTime, string? expectedSelection)
{
bool actualSuccess = EditorTimestampParser.TryParse(timestamp, out var actualParsedTime, out string? actualSelection);
Assert.Multiple(() =>
{
Assert.That(actualSuccess, Is.EqualTo(expectedSuccess));
Assert.That(actualParsedTime, Is.EqualTo(expectedParsedTime));
Assert.That(actualSelection, Is.EqualTo(expectedSelection));
});
}
}
}

View File

@ -21,10 +21,11 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
}; BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -38,14 +39,15 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -59,15 +61,16 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 2000 },
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -81,14 +84,15 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HoldNote { StartTime = 1000, Duration = 10000 }, new HoldNote { StartTime = 1000, Duration = 10000 },
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -102,16 +106,17 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HoldNote { StartTime = 1000, Duration = 10000 }, new HoldNote { StartTime = 1000, Duration = 10000 },
new Note { StartTime = 2000 }, new Note { StartTime = 2000 },
new Note { StartTime = 12000 }, new Note { StartTime = 12000 },
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -125,15 +130,16 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 }, new HitCircle { StartTime = 5000 },
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -152,9 +158,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -165,7 +172,7 @@ namespace osu.Game.Tests.Editing
new BreakPeriod(1200, 4000), new BreakPeriod(1200, 4000),
new BreakPeriod(5200, 8000), new BreakPeriod(5200, 8000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -184,9 +191,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -197,7 +205,7 @@ namespace osu.Game.Tests.Editing
{ {
new BreakPeriod(1200, 8000), new BreakPeriod(1200, 8000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -218,9 +226,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1100 }, new HitCircle { StartTime = 1100 },
@ -230,7 +239,7 @@ namespace osu.Game.Tests.Editing
{ {
new BreakPeriod(1200, 8000), new BreakPeriod(1200, 8000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -249,9 +258,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -262,7 +272,7 @@ namespace osu.Game.Tests.Editing
new ManualBreakPeriod(1200, 4000), new ManualBreakPeriod(1200, 4000),
new ManualBreakPeriod(5200, 8000), new ManualBreakPeriod(5200, 8000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -283,9 +293,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -296,7 +307,7 @@ namespace osu.Game.Tests.Editing
{ {
new ManualBreakPeriod(1200, 8000), new ManualBreakPeriod(1200, 8000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -317,9 +328,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -329,7 +341,7 @@ namespace osu.Game.Tests.Editing
{ {
new ManualBreakPeriod(1200, 8800), new ManualBreakPeriod(1200, 8800),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -348,9 +360,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -360,7 +373,7 @@ namespace osu.Game.Tests.Editing
{ {
new BreakPeriod(10000, 15000), new BreakPeriod(10000, 15000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -374,9 +387,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1000 },
@ -386,7 +400,7 @@ namespace osu.Game.Tests.Editing
{ {
new ManualBreakPeriod(10000, 15000), new ManualBreakPeriod(10000, 15000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -400,9 +414,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HoldNote { StartTime = 1000, EndTime = 20000 }, new HoldNote { StartTime = 1000, EndTime = 20000 },
@ -412,7 +427,7 @@ namespace osu.Game.Tests.Editing
{ {
new ManualBreakPeriod(10000, 15000), new ManualBreakPeriod(10000, 15000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -426,9 +441,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 10000 }, new HitCircle { StartTime = 10000 },
@ -438,7 +454,7 @@ namespace osu.Game.Tests.Editing
{ {
new BreakPeriod(0, 9000), new BreakPeriod(0, 9000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();
@ -452,9 +468,10 @@ namespace osu.Game.Tests.Editing
{ {
var controlPoints = new ControlPointInfo(); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap var beatmap = new EditorBeatmap(new Beatmap
{ {
ControlPointInfo = controlPoints, ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 10000 }, new HitCircle { StartTime = 10000 },
@ -464,7 +481,7 @@ namespace osu.Game.Tests.Editing
{ {
new ManualBreakPeriod(0, 9000), new ManualBreakPeriod(0, 9000),
} }
}; });
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess(); beatmapProcessor.PreProcess();

View File

@ -0,0 +1,22 @@
osu file format v128
[General]
SampleSet: Normal
[TimingPoints]
15,1000,4,1,0,100,1,0
2271,-100,4,1,0,5,0,0
6021,-100,4,1,0,100,0,0
8515,-100,4,1,0,5,0,0
12765,-100,4,1,0,100,0,0
14764,-100,4,1,0,5,0,0
14770,-100,4,1,0,50,0,0
17264,-100,4,1,0,5,0,0
17270,-100,4,1,0,50,0,0
22264,-100,4,1,0,100,0,0
[HitObjects]
113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0:
82,206,6015,2,0,L|457:204,1,350,0|0,2:0|2:0,2:0:0:0:
75,310,10265,2,0,L|435:312,1,350,0|0,3:0|3:0,3:0:0:0:
75,310,14764,2,0,L|435:312,3,350,0|0|0|0,3:0|3:0|3:0|3:0,3:0:0:0:

View File

@ -12,7 +12,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("stack empty", () => Stack.CurrentScreen == null); AddAssert("stack empty", () => Stack.CurrentScreen == null);
} }
[Test]
public void TestSwitchToDifficultyOfAnotherRuleset()
{
BeatmapInfo targetDifficulty = null;
AddAssert("ruleset is catch", () => Ruleset.Value.CreateInstance() is CatchRuleset);
AddStep("set taiko difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("ruleset switched to taiko", () => Ruleset.Value.CreateInstance() is TaikoRuleset);
AddStep("exit editor forcefully", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
private void switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke())); private void switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty) private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
@ -224,6 +225,116 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
} }
[Test]
public void TestAutoplayToggle()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Not.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestQuickPause()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.True);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestQuickExitAtInitialPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
AddWaitStep("wait some", 5);
AddStep("exit player", () => InputManager.PressKey(Key.F1));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
[Test]
public void TestQuickExitAtCurrentPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
AddWaitStep("wait some", 5);
AddStep("exit player", () => InputManager.PressKey(Key.F2));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000));
}
public override void TearDownSteps() public override void TearDownSteps()
{ {
base.TearDownSteps(); base.TearDownSteps();

View File

@ -402,6 +402,88 @@ namespace osu.Game.Tests.Visual.Editing
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected)); void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
} }
[Test]
public void TestHotkeysAffectNodeSamples()
{
AddStep("add slider", () =>
{
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 1000,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples = new List<IList<HitSampleInfo>>
{
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
},
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
},
}
});
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("add clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP);
AddStep("remove clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
AddStep("set drum bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LShift);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
}
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{ {
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));

View File

@ -5,11 +5,13 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
@ -102,5 +104,56 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("change tool to circle", () => InputManager.Key(Key.Number2)); AddStep("change tool to circle", () => InputManager.Key(Key.Number2));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
} }
[Test]
public void TestAutomaticBankAssignment()
{
AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
{
StartTime = 0,
Samples =
{
new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT, volume: 70),
}
}));
AddStep("seek to 500", () => EditorClock.Seek(500));
AddStep("enable automatic bank assignment", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
}
[Test]
public void TestVolumeIsInheritedFromLastObject()
{
AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
{
StartTime = 0,
Samples =
{
new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
}
}));
AddStep("seek to 500", () => EditorClock.Seek(500));
AddStep("select drum bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
}
} }
} }

View File

@ -70,6 +70,51 @@ namespace osu.Game.Tests.Visual.Editing
})); }));
} }
[TestCaseSource(nameof(test_cases))]
public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation)
{
TriangularPositionSnapGrid grid = null;
AddStep("create grid", () =>
{
Child = grid = new TriangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing.X;
grid.GridLineRotation.Value = rotation;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
[TestCaseSource(nameof(test_cases))]
public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation)
{
CircularPositionSnapGrid grid = null;
AddStep("create grid", () =>
{
Child = grid = new CircularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing.X;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
private partial class SnappingCursorContainer : CompositeDrawable private partial class SnappingCursorContainer : CompositeDrawable
{ {
public Func<Vector2, Vector2> GetSnapPosition; public Func<Vector2, Vector2> GetSnapPosition;

View File

@ -403,6 +403,28 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2)); AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2));
} }
[Test]
public void TestBreakRemoval()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 5000 },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddAssert("beatmap has one break", () => EditorBeatmap.Breaks, () => Has.Count.EqualTo(1));
AddStep("move mouse to break", () => InputManager.MoveMouseTo(this.ChildrenOfType<TimelineBreak>().Single()));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("move mouse to delete menu item", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuContextMenu>().First().ChildrenOfType<DrawableOsuMenuItem>().First()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("beatmap has no breaks", () => EditorBeatmap.Breaks, () => Is.Empty);
AddAssert("break piece went away", () => this.ChildrenOfType<TimelineBreak>().Count(), () => Is.Zero);
}
private void assertSelectionIs(IEnumerable<HitObject> hitObjects) private void assertSelectionIs(IEnumerable<HitObject> hitObjects)
=> AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
} }

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.IO.Serialization.Converters; using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
@ -62,7 +61,7 @@ namespace osu.Game.Beatmaps
public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo();
public BindableList<BreakPeriod> Breaks { get; set; } = new BindableList<BreakPeriod>(); public List<BreakPeriod> Breaks { get; set; } = new List<BreakPeriod>();
public List<string> UnhandledEventLines { get; set; } = new List<string>(); public List<string> UnhandledEventLines { get; set; } = new List<string>();

View File

@ -49,6 +49,9 @@ namespace osu.Game.Beatmaps
original.BeatmapInfo = original.BeatmapInfo.Clone(); original.BeatmapInfo = original.BeatmapInfo.Clone();
original.ControlPointInfo = original.ControlPointInfo.DeepClone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone();
// Used in osu!mania conversion.
original.Breaks = original.Breaks.ToList();
return ConvertBeatmap(original, cancellationToken); return ConvertBeatmap(original, cancellationToken);
} }

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -33,7 +32,7 @@ namespace osu.Game.Beatmaps.Formats
/// <remarks> /// <remarks>
/// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319 /// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319
/// </remarks> /// </remarks>
private const double control_point_leniency = 5; public const double CONTROL_POINT_LENIENCY = 5;
internal static RulesetStore? RulesetStore; internal static RulesetStore? RulesetStore;
@ -160,20 +159,24 @@ namespace osu.Game.Beatmaps.Formats
private void applySamples(HitObject hitObject) private void applySamples(HitObject hitObject)
{ {
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + control_point_leniency) ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
if (hitObject is IHasRepeats hasRepeats) if (hitObject is IHasRepeats hasRepeats)
{ {
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
{ {
double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + control_point_leniency; double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + CONTROL_POINT_LENIENCY;
var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT; var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT;
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList(); hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList();
} }
} }
else
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
}
} }
/// <summary> /// <summary>
@ -528,8 +531,8 @@ namespace osu.Game.Beatmaps.Formats
if (split.Length >= 8) if (split.Length >= 8)
{ {
LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]);
kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai); kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine); omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine);
} }
string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); string stringSampleSet = sampleSet.ToString().ToLowerInvariant();

View File

@ -282,19 +282,39 @@ namespace osu.Game.Beatmaps.Formats
{ {
foreach (var hitObject in hitObjects) foreach (var hitObject in hitObjects)
{ {
if (hitObject.Samples.Count > 0) if (hitObject is IHasRepeats hasNodeSamples)
{ {
int volume = hitObject.Samples.Max(o => o.Volume); double spanDuration = hasNodeSamples.Duration / hasNodeSamples.SpanCount();
int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
? hitObject.Samples.OfType<ConvertHitObjectParser.LegacyHitSampleInfo>().Max(o => o.CustomSampleBank)
: -1;
yield return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = hitObject.GetEndTime(), SampleVolume = volume, CustomSampleBank = customIndex }; for (int i = 0; i < hasNodeSamples.NodeSamples.Count; ++i)
{
double nodeTime = hitObject.StartTime + i * spanDuration;
if (hasNodeSamples.NodeSamples[i].Count > 0)
yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]);
if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0 && i < hasNodeSamples.NodeSamples.Count - 1)
yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples);
}
}
else if (hitObject.Samples.Count > 0)
{
yield return createSampleControlPointFor(hitObject.GetEndTime(), hitObject.Samples);
} }
foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects))
yield return nested; yield return nested;
} }
SampleControlPoint createSampleControlPointFor(double time, IList<HitSampleInfo> samples)
{
int volume = samples.Max(o => o.Volume);
int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
? samples.OfType<ConvertHitObjectParser.LegacyHitSampleInfo>().Max(o => o.CustomSampleBank)
: -1;
return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex };
}
} }
void extractSampleControlPoints(IEnumerable<HitObject> hitObject) void extractSampleControlPoints(IEnumerable<HitObject> hitObject)

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -41,11 +40,11 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// The breaks in this beatmap. /// The breaks in this beatmap.
/// </summary> /// </summary>
BindableList<BreakPeriod> Breaks { get; } List<BreakPeriod> Breaks { get; set; }
/// <summary> /// <summary>
/// All lines from the [Events] section which aren't handled in the encoding process yet. /// All lines from the [Events] section which aren't handled in the encoding process yet.
/// These lines shoule be written out to the beatmap file on save or export. /// These lines should be written out to the beatmap file on save or export.
/// </summary> /// </summary>
List<string> UnhandledEventLines { get; } List<string> UnhandledEventLines { get; }

View File

@ -10,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -164,13 +163,13 @@ namespace osu.Game.Database
var importTasks = new List<Task>(); var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask; Task beatmapImportTask = Task.CompletedTask;
if (content.HasFlagFast(StableContent.Beatmaps)) if (content.HasFlag(StableContent.Beatmaps))
importTasks.Add(beatmapImportTask = new LegacyBeatmapImporter(beatmaps).ImportFromStableAsync(stableStorage)); importTasks.Add(beatmapImportTask = new LegacyBeatmapImporter(beatmaps).ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Skins)) if (content.HasFlag(StableContent.Skins))
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Collections)) if (content.HasFlag(StableContent.Collections))
{ {
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess) importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess)
{ {
@ -180,7 +179,7 @@ namespace osu.Game.Database
}.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); }.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
} }
if (content.HasFlagFast(StableContent.Scores)) if (content.HasFlag(StableContent.Scores))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);

View File

@ -15,7 +15,6 @@ using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -1035,7 +1034,7 @@ namespace osu.Game.Database
var legacyMods = (LegacyMods)sr.ReadInt32(); var legacyMods = (LegacyMods)sr.ReadInt32();
if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) if (!legacyMods.HasFlag(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2"))
return; return;
score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray();

View File

@ -6,7 +6,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering;
@ -190,9 +189,9 @@ namespace osu.Game.Graphics
float width = Texture.DisplayWidth * scale; float width = Texture.DisplayWidth * scale;
float height = Texture.DisplayHeight * scale; float height = Texture.DisplayHeight * scale;
if (relativePositionAxes.HasFlagFast(Axes.X)) if (relativePositionAxes.HasFlag(Axes.X))
position.X *= sourceSize.X; position.X *= sourceSize.X;
if (relativePositionAxes.HasFlagFast(Axes.Y)) if (relativePositionAxes.HasFlag(Axes.Y))
position.Y *= sourceSize.Y; position.Y *= sourceSize.Y;
return new RectangleF( return new RectangleF(

View File

@ -119,9 +119,14 @@ namespace osu.Game.Graphics.UserInterface
Expanded.BindValueChanged(v => Expanded.BindValueChanged(v =>
{ {
label.Text = v.NewValue ? expandedLabelText : contractedLabelText; label.Text = v.NewValue ? expandedLabelText : contractedLabelText;
slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); slider.FadeTo(v.NewValue ? Current.Disabled ? 0.3f : 1f : 0f, 500, Easing.OutQuint);
slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
}, true); }, true);
Current.BindDisabledChanged(disabled =>
{
slider.Alpha = Expanded.Value ? disabled ? 0.3f : 1 : 0f;
});
} }
} }

View File

@ -12,7 +12,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using System; using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -57,15 +56,15 @@ namespace osu.Game.Graphics.UserInterface
set set
{ {
base.Origin = value; base.Origin = value;
c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight;
c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft;
X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0;
Remove(c1, false); Remove(c1, false);
Remove(c2, false); Remove(c2, false);
c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1; c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1;
c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0; c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0;
Add(c1); Add(c1);
Add(c2); Add(c2);
} }

View File

@ -34,6 +34,7 @@ namespace osu.Game.Input.Bindings
/// </remarks> /// </remarks>
public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings
.Concat(editorKeyBindings) .Concat(editorKeyBindings)
.Concat(editorTestPlayKeyBindings)
.Concat(inGameKeyBindings) .Concat(inGameKeyBindings)
.Concat(replayKeyBindings) .Concat(replayKeyBindings)
.Concat(songSelectKeyBindings) .Concat(songSelectKeyBindings)
@ -68,6 +69,9 @@ namespace osu.Game.Input.Bindings
case GlobalActionCategory.Overlays: case GlobalActionCategory.Overlays:
return overlayKeyBindings; return overlayKeyBindings;
case GlobalActionCategory.EditorTestPlay:
return editorTestPlayKeyBindings;
default: default:
throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}"); throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}");
} }
@ -100,7 +104,6 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
@ -118,6 +121,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
}; };
private static IEnumerable<KeyBinding> editorKeyBindings => new[] private static IEnumerable<KeyBinding> editorKeyBindings => new[]
@ -145,6 +149,14 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
}; };
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause),
new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorTestPlayQuickExitToInitialTime),
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorTestPlayQuickExitToCurrentTime),
};
private static IEnumerable<KeyBinding> inGameKeyBindings => new[] private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
{ {
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
@ -432,6 +444,18 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
EditorToggleScaleControl, EditorToggleScaleControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))]
EditorTestPlayToggleAutoplay,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))]
EditorTestPlayToggleQuickPause,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToInitialTime))]
EditorTestPlayQuickExitToInitialTime,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))]
EditorTestPlayQuickExitToCurrentTime,
} }
public enum GlobalActionCategory public enum GlobalActionCategory
@ -442,6 +466,7 @@ namespace osu.Game.Input.Bindings
Replay, Replay,
SongSelect, SongSelect,
AudioControl, AudioControl,
Overlays Overlays,
EditorTestPlay,
} }
} }

View File

@ -374,6 +374,26 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control");
/// <summary>
/// "Toggle autoplay"
/// </summary>
public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Toggle autoplay");
/// <summary>
/// "Toggle quick pause"
/// </summary>
public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Toggle quick pause");
/// <summary>
/// "Quick exit to initial time"
/// </summary>
public static LocalisableString EditorTestPlayQuickExitToInitialTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_initial_time"), @"Quick exit to initial time");
/// <summary>
/// "Quick exit to current time"
/// </summary>
public static LocalisableString EditorTestPlayQuickExitToCurrentTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_current_time"), @"Quick exit to current time");
/// <summary> /// <summary>
/// "Increase mod speed" /// "Increase mod speed"
/// </summary> /// </summary>

View File

@ -49,6 +49,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor"); public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor");
/// <summary>
/// "Editor: Test play"
/// </summary>
public static LocalisableString EditorTestPlaySection => new TranslatableString(getKey(@"editor_test_play_section"), @"Editor: Test play");
/// <summary> /// <summary>
/// "Reset all bindings in section" /// "Reset all bindings in section"
/// </summary> /// </summary>

View File

@ -271,7 +271,7 @@ namespace osu.Game.Online.Chat
handleAdvanced(advanced_link_regex, result, startIndex); handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times // handle editor times
handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); handleMatches(EditorTimestampParser.TIME_REGEX_STRICT, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels // handle channels
handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel);

View File

@ -596,7 +596,7 @@ namespace osu.Game
return; return;
} }
editor.HandleTimestamp(timestamp); editor.HandleTimestamp(timestamp, notifyOnError: true);
} }
/// <summary> /// <summary>

View File

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
@ -131,7 +130,7 @@ namespace osu.Game.Overlays.Music
filter.Search.HoldFocus = true; filter.Search.HoldFocus = true;
Schedule(() => filter.Search.TakeFocus()); Schedule(() => filter.Search.TakeFocus());
this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlagFast(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlag(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint);
this.FadeIn(transition_duration, Easing.OutQuint); this.FadeIn(transition_duration, Easing.OutQuint);
} }

View File

@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame), new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame),
new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay), new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor), new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorTestPlaySection, GlobalActionCategory.EditorTestPlay),
}); });
} }
} }

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -161,7 +160,7 @@ namespace osu.Game.Overlays
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{ {
if (invalidation.HasFlagFast(Invalidation.DrawSize)) if (invalidation.HasFlag(Invalidation.DrawSize))
headerTextVisibilityCache.Invalidate(); headerTextVisibilityCache.Invalidate();
return base.OnInvalidate(invalidation, source); return base.OnInvalidate(invalidation, source);

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -145,9 +144,9 @@ namespace osu.Game.Overlays.SkinEditor
var blueprintItem = ((Drawable)blueprint.Item); var blueprintItem = ((Drawable)blueprint.Item);
blueprintItem.Scale = Vector2.One; blueprintItem.Scale = Vector2.One;
if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.X)) if (blueprintItem.RelativeSizeAxes.HasFlag(Axes.X))
blueprintItem.Width = 1; blueprintItem.Width = 1;
if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.Y)) if (blueprintItem.RelativeSizeAxes.HasFlag(Axes.Y))
blueprintItem.Height = 1; blueprintItem.Height = 1;
} }
}); });

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -51,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor
CanScaleDiagonally.Value = true; CanScaleDiagonally.Value = true;
} }
private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlag(axis) == false);
private Dictionary<Drawable, OriginalDrawableState>? objectsInScale; private Dictionary<Drawable, OriginalDrawableState>? objectsInScale;
private Vector2? defaultOrigin; private Vector2? defaultOrigin;

View File

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -150,9 +149,9 @@ namespace osu.Game.Overlays.Toolbar
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize
Anchor = TooltipAnchor.HasFlagFast(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight,
Origin = TooltipAnchor, Origin = TooltipAnchor,
Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5), Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),
Alpha = 0, Alpha = 0,
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -3,7 +3,6 @@
using MessagePack; using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osuTK; using osuTK;
@ -32,23 +31,23 @@ namespace osu.Game.Replays.Legacy
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]
public bool MouseLeft1 => ButtonState.HasFlagFast(ReplayButtonState.Left1); public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1);
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]
public bool MouseRight1 => ButtonState.HasFlagFast(ReplayButtonState.Right1); public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1);
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]
public bool MouseLeft2 => ButtonState.HasFlagFast(ReplayButtonState.Left2); public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2);
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]
public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2);
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]
public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke); public bool Smoke => ButtonState.HasFlag(ReplayButtonState.Smoke);
[Key(3)] [Key(3)]
public ReplayButtonState ButtonState; public ReplayButtonState ButtonState;

View File

@ -9,7 +9,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -328,7 +327,12 @@ namespace osu.Game.Rulesets.Difficulty
set => baseBeatmap.Difficulty = value; set => baseBeatmap.Difficulty = value;
} }
public BindableList<BreakPeriod> Breaks => baseBeatmap.Breaks; public List<BreakPeriod> Breaks
{
get => baseBeatmap.Breaks;
set => baseBeatmap.Breaks = value;
}
public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines; public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines;
public double TotalBreakTime => baseBeatmap.TotalBreakTime; public double TotalBreakTime => baseBeatmap.TotalBreakTime;

View File

@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Edit
// Events // Events
new CheckBreaks(), new CheckBreaks(),
// Metadata
new CheckTitleMarkers(),
}; };
public IEnumerable<Issue> Run(BeatmapVerifierContext context) public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -0,0 +1,71 @@
// 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.Collections.Generic;
using System.Text.RegularExpressions;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckTitleMarkers : ICheck
{
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title.");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateIncorrectMarker(this),
};
private readonly IEnumerable<MarkerCheck> markerChecks =
[
new MarkerCheck(@"(TV Size)", @"(?i)(tv (size|ver))"),
new MarkerCheck(@"(Game Ver.)", @"(?i)(game (size|ver))"),
new MarkerCheck(@"(Short Ver.)", @"(?i)(short (size|ver))"),
new MarkerCheck(@"(Cut Ver.)", @"(?i)(?<!& )(cut (size|ver))"),
new MarkerCheck(@"(Sped Up Ver.)", @"(?i)(?<!& )(sped|speed) ?up ver"),
new MarkerCheck(@"(Nightcore Mix)", @"(?i)(?<!& )(nightcore|night core) (ver|mix)"),
new MarkerCheck(@"(Sped Up & Cut Ver.)", @"(?i)(sped|speed) ?up (ver)? ?& cut (size|ver)"),
new MarkerCheck(@"(Nightcore & Cut Ver.)", @"(?i)(nightcore|night core) (ver|mix)? ?& cut (size|ver)"),
];
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
string romanisedTitle = context.Beatmap.Metadata.Title;
string unicodeTitle = context.Beatmap.Metadata.TitleUnicode;
foreach (var check in markerChecks)
{
bool hasRomanisedTitle = unicodeTitle != romanisedTitle;
if (check.AnyRegex.IsMatch(unicodeTitle) && !unicodeTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal))
yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat);
if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !romanisedTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal))
yield return new IssueTemplateIncorrectMarker(this).Create("Romanised title", check.CorrectMarkerFormat);
}
}
private class MarkerCheck
{
public readonly string CorrectMarkerFormat;
public readonly Regex AnyRegex;
public MarkerCheck(string exact, string anyRegex)
{
CorrectMarkerFormat = exact;
AnyRegex = new Regex(anyRegex, RegexOptions.Compiled);
}
}
public class IssueTemplateIncorrectMarker : IssueTemplate
{
public IssueTemplateIncorrectMarker(ICheck check)
: base(check, IssueType.Problem, "{0} field has an incorrect format of marker {1}")
{
}
public Issue Create(string titleField, string correctMarkerFormat) => new Issue(this, titleField, correctMarkerFormat);
}
}
}

View File

@ -9,13 +9,41 @@ namespace osu.Game.Rulesets.Edit
{ {
public static class EditorTimestampParser public static class EditorTimestampParser
{ {
// 00:00:000 (...) - test /// <summary>
// original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 /// Used for parsing in contexts where we don't want e.g. normal times of day to be parsed as timestamps (e.g. chat)
public static readonly Regex TIME_REGEX = new Regex(@"\b(((?<minutes>\d{2,}):(?<seconds>[0-5]\d)[:.](?<milliseconds>\d{3}))(?<selection>\s\([^)]+\))?)", RegexOptions.Compiled); /// Original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
/// </summary>
/// <example>
/// 00:00:000 (...) - test
/// </example>
public static readonly Regex TIME_REGEX_STRICT = new Regex(@"\b(((?<minutes>\d{2,}):(?<seconds>[0-5]\d)[:.](?<milliseconds>\d{3}))(?<selection>\s\([^)]+\))?)", RegexOptions.Compiled);
/// <summary>
/// Used for editor-specific context wherein we want to try as hard as we can to process user input as a timestamp.
/// </summary>
/// <example>
/// <list type="bullet">
/// <item>1 - parses to 00:00:001 (bare numbers are treated as milliseconds)</item>
/// <item>1:2 - parses to 01:02:000</item>
/// <item>1:02 - parses to 01:02:000</item>
/// <item>1:92 - does not parse</item>
/// <item>1:02:3 - parses to 01:02:003</item>
/// <item>1:02:300 - parses to 01:02:300</item>
/// <item>1:02:300 (1,2,3) - parses to 01:02:300 with selection</item>
/// </list>
/// </example>
private static readonly Regex time_regex_lenient = new Regex(@"^(((?<minutes>\d{1,3}):(?<seconds>([0-5]?\d))([:.](?<milliseconds>\d{0,3}))?)(?<selection>\s\([^)]+\))?)$", RegexOptions.Compiled);
public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection)
{ {
Match match = TIME_REGEX.Match(timestamp); if (double.TryParse(timestamp, out double msec))
{
parsedTime = TimeSpan.FromMilliseconds(msec);
parsedSelection = null;
return true;
}
Match match = time_regex_lenient.Match(timestamp);
if (!match.Success) if (!match.Success)
{ {
@ -24,16 +52,14 @@ namespace osu.Game.Rulesets.Edit
return false; return false;
} }
bool result = true; int timeMin, timeSec, timeMsec;
result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin); int.TryParse(match.Groups[@"minutes"].Value, out timeMin);
result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec); int.TryParse(match.Groups[@"seconds"].Value, out timeSec);
result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec); int.TryParse(match.Groups[@"milliseconds"].Value, out timeMsec);
// somewhat sane limit for timestamp duration (10 hours). // somewhat sane limit for timestamp duration (10 hours).
result &= timeMin < 600; if (timeMin >= 600)
if (!result)
{ {
parsedTime = null; parsedTime = null;
parsedSelection = null; parsedSelection = null;
@ -42,8 +68,7 @@ namespace osu.Game.Rulesets.Edit
parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec); parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec);
parsedSelection = match.Groups[@"selection"].Value.Trim(); parsedSelection = match.Groups[@"selection"].Value.Trim();
if (!string.IsNullOrEmpty(parsedSelection)) parsedSelection = !string.IsNullOrEmpty(parsedSelection) ? parsedSelection[1..^1] : null;
parsedSelection = parsedSelection[1..^1];
return true; return true;
} }
} }

View File

@ -10,7 +10,6 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -216,14 +215,14 @@ namespace osu.Game.Rulesets.Edit
toolboxCollection.Items = CompositionTools toolboxCollection.Items = CompositionTools
.Prepend(new SelectTool()) .Prepend(new SelectTool())
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
.ToList(); .ToList();
foreach (var item in toolboxCollection.Items) foreach (var item in toolboxCollection.Items)
{ {
item.Selected.DisabledChanged += isDisabled => item.Selected.DisabledChanged += isDisabled =>
{ {
item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty; item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText;
}; };
} }
@ -505,7 +504,7 @@ namespace osu.Game.Rulesets.Edit
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null; double? targetTime = null;
if (snapType.HasFlagFast(SnapType.GlobalGrids)) if (snapType.HasFlag(SnapType.GlobalGrids))
{ {
if (playfield is ScrollingPlayfield scrollingPlayfield) if (playfield is ScrollingPlayfield scrollingPlayfield)
{ {

View File

@ -0,0 +1,22 @@
// 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 osu.Game.Rulesets.Edit.Tools;
using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Rulesets.Edit
{
public class HitObjectCompositionToolButton : RadioButton
{
public HitObjectCompositionTool Tool { get; }
public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action)
: base(tool.Name, action, tool.CreateIcon)
{
Tool = tool;
TooltipText = tool.TooltipText;
}
}
}

View File

@ -156,12 +156,21 @@ namespace osu.Game.Rulesets.Edit
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
} }
if (AutomaticBankAssignment) var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
{ {
// Take the hitnormal sample of the last hit object if (AutomaticBankAssignment)
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); {
if (lastHitNormal != null) // Take the hitnormal sample of the last hit object
HitObject.Samples[0] = lastHitNormal; HitObject.Samples[0] = lastHitNormal;
}
else
{
// Only inherit the volume from the previous hit object
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
}
} }
} }

View File

@ -1,9 +1,8 @@
// 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.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Edit.Tools namespace osu.Game.Rulesets.Edit.Tools
{ {
@ -11,14 +10,16 @@ namespace osu.Game.Rulesets.Edit.Tools
{ {
public readonly string Name; public readonly string Name;
public LocalisableString TooltipText { get; init; }
protected HitObjectCompositionTool(string name) protected HitObjectCompositionTool(string name)
{ {
Name = name; Name = name;
} }
public abstract PlacementBlueprint CreatePlacementBlueprint(); public abstract PlacementBlueprint? CreatePlacementBlueprint();
public virtual Drawable CreateIcon() => null; public virtual Drawable? CreateIcon() => null;
public override string ToString() => Name; public override string ToString() => Name;
} }

View File

@ -12,7 +12,6 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.Audio; using osu.Game.Audio;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4;
type &= ~LegacyHitObjectType.ComboOffset; type &= ~LegacyHitObjectType.ComboOffset;
bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo); bool combo = type.HasFlag(LegacyHitObjectType.NewCombo);
type &= ~LegacyHitObjectType.NewCombo; type &= ~LegacyHitObjectType.NewCombo;
var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
@ -66,14 +65,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
HitObject result = null; HitObject result = null;
if (type.HasFlagFast(LegacyHitObjectType.Circle)) if (type.HasFlag(LegacyHitObjectType.Circle))
{ {
result = CreateHit(pos, combo, comboOffset); result = CreateHit(pos, combo, comboOffset);
if (split.Length > 5) if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo); readCustomSampleBanks(split[5], bankInfo);
} }
else if (type.HasFlagFast(LegacyHitObjectType.Slider)) else if (type.HasFlag(LegacyHitObjectType.Slider))
{ {
double? length = null; double? length = null;
@ -145,7 +144,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
} }
else if (type.HasFlagFast(LegacyHitObjectType.Spinner)) else if (type.HasFlag(LegacyHitObjectType.Spinner))
{ {
double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime);
@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (split.Length > 6) if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo); readCustomSampleBanks(split[6], bankInfo);
} }
else if (type.HasFlagFast(LegacyHitObjectType.Hold)) else if (type.HasFlag(LegacyHitObjectType.Hold))
{ {
// Note: Hold is generated by BMS converts // Note: Hold is generated by BMS converts
@ -472,7 +471,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank, soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))); type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)));
} }
else else
{ {
@ -480,13 +479,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume)); soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
} }
if (type.HasFlagFast(LegacyHitSoundType.Finish)) if (type.HasFlag(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Whistle)) if (type.HasFlag(LegacyHitSoundType.Whistle))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Clap)) if (type.HasFlag(LegacyHitSoundType.Clap))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
return soundTypes; return soundTypes;

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -12,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -21,6 +20,7 @@ using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
using static osu.Game.Input.Handlers.ReplayInputHandler; using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.UI
public readonly KeyBindingContainer<T> KeyBindingContainer; public readonly KeyBindingContainer<T> KeyBindingContainer;
[Resolved(CanBeNull = true)] [Resolved]
private ScoreProcessor scoreProcessor { get; set; } private ScoreProcessor? scoreProcessor { get; set; }
private ReplayRecorder recorder; private ReplayRecorder? recorder;
public ReplayRecorder Recorder public ReplayRecorder? Recorder
{ {
set set
{ {
@ -103,14 +103,23 @@ namespace osu.Game.Rulesets.UI
#region IHasReplayHandler #region IHasReplayHandler
private ReplayInputHandler replayInputHandler; private ReplayInputHandler? replayInputHandler;
public ReplayInputHandler ReplayInputHandler public ReplayInputHandler? ReplayInputHandler
{ {
get => replayInputHandler; get => replayInputHandler;
set set
{ {
if (replayInputHandler != null) RemoveHandler(replayInputHandler); if (replayInputHandler == value)
return;
if (replayInputHandler != null)
RemoveHandler(replayInputHandler);
// ensures that all replay keys are released, that the last replay state is correctly cleared,
// and that all user-pressed keys are released, so that the replay handler may trigger them itself
// setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/17d65f476d51cc5f2aaea818534f8fbac47e5fe6/osu.Framework/Input/PassThroughInputManager.cs#L179-L182)
new ReplayStateReset().Apply(CurrentState, this);
replayInputHandler = value; replayInputHandler = value;
UseParentInput = replayInputHandler == null; UseParentInput = replayInputHandler == null;
@ -124,8 +133,8 @@ namespace osu.Game.Rulesets.UI
#region Setting application (disables etc.) #region Setting application (disables etc.)
private Bindable<bool> mouseDisabled; private Bindable<bool> mouseDisabled = null!;
private Bindable<bool> tapsDisabled; private Bindable<bool> tapsDisabled = null!;
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
@ -222,14 +231,34 @@ namespace osu.Game.Rulesets.UI
RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings);
} }
} }
private class ReplayStateReset : IInput
{
public void Apply(InputState state, IInputStateChangeHandler handler)
{
if (!(state is RulesetInputManagerInputState<T> inputState))
throw new InvalidOperationException($"{nameof(ReplayState<T>)} should only be applied to a {nameof(RulesetInputManagerInputState<T>)}");
new MouseButtonInput([], state.Mouse.Buttons).Apply(state, handler);
new KeyboardKeyInput([], state.Keyboard.Keys).Apply(state, handler);
new TouchInput(Enum.GetValues<TouchSource>().Select(s => new Touch(s, Vector2.Zero)), false).Apply(state, handler);
new JoystickButtonInput([], state.Joystick.Buttons).Apply(state, handler);
new MidiKeyInput(new MidiState(), state.Midi).Apply(state, handler);
new TabletPenButtonInput([], state.Tablet.PenButtons).Apply(state, handler);
new TabletAuxiliaryButtonInput([], state.Tablet.AuxiliaryButtons).Apply(state, handler);
handler.HandleInputStateChange(new ReplayStateChangeEvent<T>(state, this, inputState.LastReplayState?.PressedActions.ToArray() ?? [], []));
inputState.LastReplayState = null;
}
}
} }
public class RulesetInputManagerInputState<T> : InputState public class RulesetInputManagerInputState<T> : InputState
where T : struct where T : struct
{ {
public ReplayState<T> LastReplayState; public ReplayState<T>? LastReplayState;
public RulesetInputManagerInputState(InputState state = null) public RulesetInputManagerInputState(InputState state)
: base(state) : base(state)
{ {
} }

View File

@ -10,9 +10,11 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -25,14 +27,16 @@ namespace osu.Game.Screens.Edit.Components
public partial class PlaybackControl : BottomBarContainer public partial class PlaybackControl : BottomBarContainer
{ {
private IconButton playButton = null!; private IconButton playButton = null!;
private PlaybackSpeedControl playbackSpeedControl = null!;
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } = null!; private EditorClock editorClock { get; set; } = null!;
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1); private readonly Bindable<EditorScreenMode> currentScreenMode = new Bindable<EditorScreenMode>();
private readonly BindableNumber<double> tempoAdjustment = new BindableDouble(1);
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider, Editor? editor)
{ {
Background.Colour = colourProvider.Background4; Background.Colour = colourProvider.Background4;
@ -47,31 +51,61 @@ namespace osu.Game.Screens.Edit.Components
Icon = FontAwesome.Regular.PlayCircle, Icon = FontAwesome.Regular.PlayCircle,
Action = togglePause, Action = togglePause,
}, },
new OsuSpriteText playbackSpeedControl = new PlaybackSpeedControl
{ {
Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Y,
Text = EditorStrings.PlaybackSpeed, RelativeSizeAxes = Axes.X,
RelativePositionAxes = Axes.Y, Padding = new MarginPadding { Left = 45, },
Y = 0.5f, Anchor = Anchor.CentreRight,
Padding = new MarginPadding { Left = 45 } Origin = Anchor.CentreRight,
}, Direction = FillDirection.Vertical,
new Container Children = new Drawable[]
{ {
Anchor = Anchor.BottomLeft, new OsuSpriteText
Origin = Anchor.BottomLeft, {
RelativeSizeAxes = Axes.Both, Text = EditorStrings.PlaybackSpeed,
Height = 0.5f, },
Padding = new MarginPadding { Left = 45 }, new PlaybackTabControl
Child = new PlaybackTabControl { Current = freqAdjust }, {
Current = tempoAdjustment,
RelativeSizeAxes = Axes.X,
Height = 16,
},
}
} }
}; };
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true); Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true);
if (editor != null)
currentScreenMode.BindTo(editor.Mode);
}
protected override void LoadComplete()
{
base.LoadComplete();
currentScreenMode.BindValueChanged(_ =>
{
if (currentScreenMode.Value == EditorScreenMode.Timing)
{
tempoAdjustment.Value = 1;
tempoAdjustment.Disabled = true;
playbackSpeedControl.FadeTo(0.5f, 400, Easing.OutQuint);
playbackSpeedControl.TooltipText = "Speed adjustment is unavailable in timing mode. Timing at slower speeds is inaccurate due to resampling artifacts.";
}
else
{
tempoAdjustment.Disabled = false;
playbackSpeedControl.FadeTo(1, 400, Easing.OutQuint);
playbackSpeedControl.TooltipText = default;
}
});
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust); Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment);
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
@ -109,6 +143,11 @@ namespace osu.Game.Screens.Edit.Components
playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon; playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon;
} }
private partial class PlaybackSpeedControl : FillFlowContainer, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
private partial class PlaybackTabControl : OsuTabControl<double> private partial class PlaybackTabControl : OsuTabControl<double>
{ {
private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };
@ -174,7 +213,7 @@ namespace osu.Game.Screens.Edit.Components
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
updateState(); updateState();
return true; return false;
} }
protected override void OnHoverLost(HoverLostEvent e) => updateState(); protected override void OnHoverLost(HoverLostEvent e) => updateState();

View File

@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// <summary> /// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used. /// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary> /// </summary>
public readonly Func<Drawable>? CreateIcon; public readonly Func<Drawable?>? CreateIcon;
private readonly Action? action; private readonly Action? action;
public RadioButton(string label, Action? action, Func<Drawable>? createIcon = null) public RadioButton(string label, Action? action, Func<Drawable?>? createIcon = null)
{ {
Label = label; Label = label;
CreateIcon = createIcon; CreateIcon = createIcon;

View File

@ -4,8 +4,12 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
@ -13,7 +17,6 @@ namespace osu.Game.Screens.Edit.Components
{ {
public partial class TimeInfoContainer : BottomBarContainer public partial class TimeInfoContainer : BottomBarContainer
{ {
private OsuSpriteText trackTimer = null!;
private OsuSpriteText bpm = null!; private OsuSpriteText bpm = null!;
[Resolved] [Resolved]
@ -29,14 +32,7 @@ namespace osu.Game.Screens.Edit.Components
Children = new Drawable[] Children = new Drawable[]
{ {
trackTimer = new OsuSpriteText new TimestampControl(),
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Spacing = new Vector2(-2, 0),
Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light),
Y = -10,
},
bpm = new OsuSpriteText bpm = new OsuSpriteText
{ {
Colour = colours.Orange1, Colour = colours.Orange1,
@ -47,19 +43,12 @@ namespace osu.Game.Screens.Edit.Components
}; };
} }
private double? lastTime;
private double? lastBPM; private double? lastBPM;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (lastTime != editorClock.CurrentTime)
{
lastTime = editorClock.CurrentTime;
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
}
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
if (lastBPM != newBPM) if (lastBPM != newBPM)
@ -68,5 +57,109 @@ namespace osu.Game.Screens.Edit.Components
bpm.Text = @$"{newBPM:0} BPM"; bpm.Text = @$"{newBPM:0} BPM";
} }
} }
private partial class TimestampControl : OsuClickableContainer
{
private Container hoverLayer = null!;
private OsuSpriteText trackTimer = null!;
private OsuTextBox inputTextBox = null!;
[Resolved]
private Editor? editor { get; set; }
[Resolved]
private EditorClock editorClock { get; set; } = null!;
public TimestampControl()
: base(HoverSampleSet.Button)
{
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
AddRangeInternal(new Drawable[]
{
hoverLayer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Top = 5,
Horizontal = -2
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 5,
Masking = true,
Children = new Drawable[]
{
new Box { RelativeSizeAxes = Axes.Both, },
}
},
Alpha = 0,
},
trackTimer = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Spacing = new Vector2(-2, 0),
Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light),
},
inputTextBox = new OsuTextBox
{
Width = 150,
Height = 36,
Alpha = 0,
CommitOnFocusLost = true,
},
});
Action = () =>
{
trackTimer.Alpha = 0;
inputTextBox.Alpha = 1;
inputTextBox.Text = editorClock.CurrentTime.ToEditorFormattedString();
Schedule(() =>
{
GetContainingFocusManager()!.ChangeFocus(inputTextBox);
inputTextBox.SelectAll();
});
};
inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue));
inputTextBox.OnCommit += (_, __) =>
{
trackTimer.Alpha = 1;
inputTextBox.Alpha = 0;
};
}
private double? lastTime;
private bool showingHoverLayer;
protected override void Update()
{
base.Update();
if (lastTime != editorClock.CurrentTime)
{
lastTime = editorClock.CurrentTime;
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
}
bool shouldShowHoverLayer = IsHovered && inputTextBox.Alpha == 0;
if (shouldShowHoverLayer != showingHoverLayer)
{
hoverLayer.FadeTo(shouldShowHoverLayer ? 0.2f : 0, 400, Easing.OutQuint);
showingHoverLayer = shouldShowHoverLayer;
}
}
}
} }
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
breaks.BindTo(beatmap.Breaks); breaks.BindTo(beatmap.Breaks);
breaks.BindCollectionChanged((_, _) => breaks.BindCollectionChanged((_, _) =>
{ {
Clear();
foreach (var breakPeriod in beatmap.Breaks) foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod)); Add(new BreakVisualisation(breakPeriod));
}, true); }, true);

View File

@ -0,0 +1,98 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class CircularPositionSnapGrid : PositionSnapGrid
{
/// <summary>
/// The spacing between grid lines of this <see cref="CircularPositionSnapGrid"/>.
/// </summary>
public BindableFloat Spacing { get; } = new BindableFloat(1f)
{
MinValue = 0f,
};
public CircularPositionSnapGrid()
{
Spacing.BindValueChanged(_ => GridCache.Invalidate());
}
protected override void CreateContent()
{
var drawSize = DrawSize;
// Calculate the required number of circles based on the maximum distance from the origin to the edge of the grid.
float dx = Math.Max(StartPosition.Value.X, DrawWidth - StartPosition.Value.X);
float dy = Math.Max(StartPosition.Value.Y, DrawHeight - StartPosition.Value.Y);
float maxDistance = new Vector2(dx, dy).Length;
// We need to add one because the first circle starts at zero radius.
int requiredCircles = (int)(maxDistance / Spacing.Value) + 1;
generateCircles(requiredCircles);
GenerateOutline(drawSize);
}
private void generateCircles(int count)
{
// Make lines the same width independent of display resolution.
float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width;
List<CircularProgress> generatedCircles = new List<CircularProgress>();
for (int i = 0; i < count; i++)
{
// Add a minimum diameter so the center circle is clearly visible.
float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2);
var gridCircle = new CircularProgress
{
Position = StartPosition.Value,
Origin = Anchor.Centre,
Size = new Vector2(diameter),
InnerRadius = lineWidth * 1f / diameter,
Colour = Colour4.White,
Alpha = 0.2f,
Progress = 1,
};
generatedCircles.Add(gridCircle);
}
if (generatedCircles.Count == 0)
return;
generatedCircles.First().Alpha = 0.8f;
AddInternal(new Container
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = generatedCircles,
});
}
public override Vector2 GetSnappedPosition(Vector2 original)
{
Vector2 relativeToStart = original - StartPosition.Value;
if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON)
return StartPosition.Value;
float length = relativeToStart.Length;
float wantedLength = MathF.Round(length / Spacing.Value) * Spacing.Value;
return StartPosition.Value + Vector2.Multiply(relativeToStart, wantedLength / length);
}
}
}

View File

@ -117,7 +117,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
break; break;
} }
AddSampleBank(bankName); SetSampleBank(bankName);
} }
break; break;
@ -177,14 +177,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo); SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
foreach ((string sampleName, var bindable) in SelectionSampleStates) foreach ((string sampleName, var bindable) in SelectionSampleStates)
{ {
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName)); bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Name == sampleName));
} }
foreach ((string bankName, var bindable) in SelectionBankStates) foreach ((string bankName, var bindable) in SelectionBankStates)
{ {
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName)); bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Bank == bankName));
}
IEnumerable<IList<HitSampleInfo>> enumerateAllSamples(HitObject hitObject)
{
yield return hitObject.Samples;
if (hitObject is IHasRepeats withRepeats)
{
foreach (var node in withRepeats.NodeSamples)
yield return node;
}
} }
} }
@ -193,12 +206,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Ternary state changes #region Ternary state changes
/// <summary> /// <summary>
/// Adds a sample bank to all selected <see cref="HitObject"/>s. /// Sets the sample bank for all selected <see cref="HitObject"/>s.
/// </summary> /// </summary>
/// <param name="bankName">The name of the sample bank.</param> /// <param name="bankName">The name of the sample bank.</param>
public void AddSampleBank(string bankName) public void SetSampleBank(string bankName)
{ {
if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName))) bool hasRelevantBank(HitObject hitObject)
{
bool result = hitObject.Samples.All(s => s.Bank == bankName);
if (hitObject is IHasRepeats hasRepeats)
{
foreach (var node in hasRepeats.NodeSamples)
result &= node.All(s => s.Bank == bankName);
}
return result;
}
if (SelectedItems.All(hasRelevantBank))
return; return;
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
@ -207,17 +233,37 @@ namespace osu.Game.Screens.Edit.Compose.Components
return; return;
h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList(); h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList();
if (h is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i)
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.With(newBank: bankName)).ToList();
}
EditorBeatmap.Update(h); EditorBeatmap.Update(h);
}); });
} }
private bool hasRelevantSample(HitObject hitObject, string sampleName)
{
bool result = hitObject.Samples.Any(s => s.Name == sampleName);
if (hitObject is IHasRepeats hasRepeats)
{
foreach (var node in hasRepeats.NodeSamples)
result &= node.Any(s => s.Name == sampleName);
}
return result;
}
/// <summary> /// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s. /// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary> /// </summary>
/// <param name="sampleName">The name of the hit sample.</param> /// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName) public void AddHitSample(string sampleName)
{ {
if (SelectedItems.All(h => h.Samples.Any(s => s.Name == sampleName))) if (SelectedItems.All(h => hasRelevantSample(h, sampleName)))
return; return;
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
@ -228,6 +274,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(h.CreateHitSampleInfo(sampleName)); h.Samples.Add(h.CreateHitSampleInfo(sampleName));
if (h is IHasRepeats hasRepeats)
{
foreach (var node in hasRepeats.NodeSamples)
{
if (node.Any(s => s.Name == sampleName))
continue;
var hitSample = h.CreateHitSampleInfo(sampleName);
string? existingAdditionBank = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL)?.Bank;
if (existingAdditionBank != null)
hitSample = hitSample.With(newBank: existingAdditionBank);
node.Add(hitSample);
}
}
EditorBeatmap.Update(h); EditorBeatmap.Update(h);
}); });
} }
@ -238,12 +301,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param> /// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName) public void RemoveHitSample(string sampleName)
{ {
if (SelectedItems.All(h => h.Samples.All(s => s.Name != sampleName))) if (SelectedItems.All(h => !hasRelevantSample(h, sampleName)))
return; return;
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
h.SamplesBindable.RemoveAll(s => s.Name == sampleName); h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
if (h is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i)
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Where(s => s.Name != sampleName).ToList();
}
EditorBeatmap.Update(h); EditorBeatmap.Update(h);
}); });
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -298,13 +297,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </remarks> /// </remarks>
public void PerformFlipFromScaleHandles(Axes axes) public void PerformFlipFromScaleHandles(Axes axes)
{ {
if (axes.HasFlagFast(Axes.X)) if (axes.HasFlag(Axes.X))
{ {
dragHandles.FlipScaleHandles(Direction.Horizontal); dragHandles.FlipScaleHandles(Direction.Horizontal);
OnFlip?.Invoke(Direction.Horizontal, false); OnFlip?.Invoke(Direction.Horizontal, false);
} }
if (axes.HasFlagFast(Axes.Y)) if (axes.HasFlag(Axes.Y))
{ {
dragHandles.FlipScaleHandles(Direction.Vertical); dragHandles.FlipScaleHandles(Direction.Vertical);
OnFlip?.Invoke(Direction.Vertical, false); OnFlip?.Invoke(Direction.Vertical, false);

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -74,9 +73,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
foreach (var handle in scaleHandles) foreach (var handle in scaleHandles)
{ {
if (direction == Direction.Horizontal && !handle.Anchor.HasFlagFast(Anchor.x1)) if (direction == Direction.Horizontal && !handle.Anchor.HasFlag(Anchor.x1))
handle.Anchor ^= Anchor.x0 | Anchor.x2; handle.Anchor ^= Anchor.x0 | Anchor.x2;
if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1)) if (direction == Direction.Vertical && !handle.Anchor.HasFlag(Anchor.y1))
handle.Anchor ^= Anchor.y0 | Anchor.y2; handle.Anchor ^= Anchor.y0 | Anchor.y2;
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -46,8 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
Icon = FontAwesome.Solid.Redo, Icon = FontAwesome.Solid.Redo,
Scale = new Vector2 Scale = new Vector2
{ {
X = Anchor.HasFlagFast(Anchor.x0) ? 1f : -1f, X = Anchor.HasFlag(Anchor.x0) ? 1f : -1f,
Y = Anchor.HasFlagFast(Anchor.y0) ? 1f : -1f Y = Anchor.HasFlag(Anchor.y0) ? 1f : -1f
} }
}); });
} }

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -128,6 +127,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1); private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlag(Anchor.x1) && !anchor.HasFlag(Anchor.y1);
} }
} }

View File

@ -9,20 +9,31 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public partial class TimelineBreak : CompositeDrawable public partial class TimelineBreak : CompositeDrawable, IHasContextMenu
{ {
public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>(); public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();
public Action<BreakPeriod>? OnDeleted { get; init; }
private Box background = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public TimelineBreak(BreakPeriod b) public TimelineBreak(BreakPeriod b)
{ {
Break.Value = b; Break.Value = b;
@ -42,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5 }, Padding = new MarginPadding { Horizontal = 5 },
Child = new Box Child = background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.Gray5, Colour = colours.Gray5,
@ -77,6 +88,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, true); }, true);
} }
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
background.FadeColour(IsHovered ? colours.Gray6 : colours.Gray5, 400, Easing.OutQuint);
}
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)),
};
private partial class DragHandle : FillFlowContainer private partial class DragHandle : FillFlowContainer
{ {
public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>(); public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();

View File

@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private Timeline timeline { get; set; } = null!; private Timeline timeline { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? editorChangeHandler { get; set; }
/// <summary> /// <summary>
/// The visible time/position range of the timeline. /// The visible time/position range of the timeline.
/// </summary> /// </summary>
@ -71,7 +74,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!shouldBeVisible(breakPeriod)) if (!shouldBeVisible(breakPeriod))
continue; continue;
Add(new TimelineBreak(breakPeriod)); Add(new TimelineBreak(breakPeriod)
{
OnDeleted = b =>
{
editorChangeHandler?.BeginChange();
breaks.Remove(b);
editorChangeHandler?.EndChange();
},
});
} }
} }

View File

@ -0,0 +1,89 @@
// 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 osu.Framework.Bindables;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid
{
/// <summary>
/// The spacing between grid lines of this <see cref="TriangularPositionSnapGrid"/>.
/// </summary>
public BindableFloat Spacing { get; } = new BindableFloat(1f)
{
MinValue = 0f,
};
/// <summary>
/// The rotation in degrees of the grid lines of this <see cref="TriangularPositionSnapGrid"/>.
/// </summary>
public BindableFloat GridLineRotation { get; } = new BindableFloat();
public TriangularPositionSnapGrid()
{
Spacing.BindValueChanged(_ => GridCache.Invalidate());
GridLineRotation.BindValueChanged(_ => GridCache.Invalidate());
}
private static readonly float sqrt3 = float.Sqrt(3);
private static readonly float sqrt3_over2 = sqrt3 / 2;
private static readonly float one_over_sqrt3 = 1 / sqrt3;
protected override void CreateContent()
{
var drawSize = DrawSize;
float stepSpacing = Spacing.Value * sqrt3_over2;
var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 30);
var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 90);
var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 150);
GenerateGridLines(step1, drawSize);
GenerateGridLines(-step1, drawSize);
GenerateGridLines(step2, drawSize);
GenerateGridLines(-step2, drawSize);
GenerateGridLines(step3, drawSize);
GenerateGridLines(-step3, drawSize);
GenerateOutline(drawSize);
}
public override Vector2 GetSnappedPosition(Vector2 original)
{
Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value);
Vector2 hex = pixelToHex(relativeToStart);
return StartPosition.Value + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation.Value);
}
private Vector2 pixelToHex(Vector2 pixel)
{
float x = pixel.X / Spacing.Value;
float y = pixel.Y / Spacing.Value;
// Algorithm from Charles Chambers
// with modifications and comments by Chris Cox 2023
// <https://gitlab.com/chriscox/hex-coordinates>
float t = sqrt3 * y + 1; // scaled y, plus phase
float temp1 = MathF.Floor(t + x); // (y+x) diagonal, this calc needs floor
float temp2 = t - x; // (y-x) diagonal, no floor needed
float temp3 = 2 * x + 1; // scaled horizontal, no floor needed, needs +1 to get correct phase
float qf = (temp1 + temp3) / 3.0f; // pseudo x with fraction
float rf = (temp1 + temp2) / 3.0f; // pseudo y with fraction
float q = MathF.Floor(qf); // pseudo x, quantized and thus requires floor
float r = MathF.Floor(rf); // pseudo y, quantized and thus requires floor
return new Vector2(q, r);
}
private Vector2 hexToPixel(Vector2 hex)
{
// Taken from <https://www.redblobgames.com/grids/hexagons/#hex-to-pixel>
// with modifications for the different definition of size.
return new Vector2(Spacing.Value * (hex.X - hex.Y / 2), Spacing.Value * one_over_sqrt3 * 1.5f * hex.Y);
}
}
}

View File

@ -1284,16 +1284,20 @@ namespace osu.Game.Screens.Edit
return tcs.Task; return tcs.Task;
} }
public void HandleTimestamp(string timestamp) public bool HandleTimestamp(string timestamp, bool notifyOnError = false)
{ {
if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection))
{ {
Schedule(() => notifications?.Post(new SimpleErrorNotification if (notifyOnError)
{ {
Icon = FontAwesome.Solid.ExclamationTriangle, Schedule(() => notifications?.Post(new SimpleErrorNotification
Text = EditorStrings.FailedToParseEditorLink {
})); Icon = FontAwesome.Solid.ExclamationTriangle,
return; Text = EditorStrings.FailedToParseEditorLink
}));
}
return false;
} }
editorBeatmap.SelectedHitObjects.Clear(); editorBeatmap.SelectedHitObjects.Clear();
@ -1306,7 +1310,7 @@ namespace osu.Game.Screens.Edit
if (string.IsNullOrEmpty(selection)) if (string.IsNullOrEmpty(selection))
{ {
clock.SeekSmoothlyTo(position); clock.SeekSmoothlyTo(position);
return; return true;
} }
// Seek to the next closest HitObject instead // Seek to the next closest HitObject instead
@ -1321,6 +1325,7 @@ namespace osu.Game.Screens.Edit
// Delegate handling the selection to the ruleset. // Delegate handling the selection to the ruleset.
currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection); currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection);
return true;
} }
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);

View File

@ -110,6 +110,9 @@ namespace osu.Game.Screens.Edit
foreach (var obj in HitObjects) foreach (var obj in HitObjects)
trackStartTime(obj); trackStartTime(obj);
Breaks = new BindableList<BreakPeriod>(playableBeatmap.Breaks);
Breaks.BindCollectionChanged((_, _) => playableBeatmap.Breaks = Breaks.ToList());
PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime); PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime);
PreviewTime.BindValueChanged(s => PreviewTime.BindValueChanged(s =>
{ {
@ -172,7 +175,13 @@ namespace osu.Game.Screens.Edit
set => PlayableBeatmap.ControlPointInfo = value; set => PlayableBeatmap.ControlPointInfo = value;
} }
public BindableList<BreakPeriod> Breaks => PlayableBeatmap.Breaks; public readonly BindableList<BreakPeriod> Breaks;
List<BreakPeriod> IBeatmap.Breaks
{
get => PlayableBeatmap.Breaks;
set => PlayableBeatmap.Breaks = value;
}
public List<string> UnhandledEventLines => PlayableBeatmap.UnhandledEventLines; public List<string> UnhandledEventLines => PlayableBeatmap.UnhandledEventLines;

View File

@ -12,11 +12,13 @@ namespace osu.Game.Screens.Edit
{ {
public class EditorBeatmapProcessor : IBeatmapProcessor public class EditorBeatmapProcessor : IBeatmapProcessor
{ {
public IBeatmap Beatmap { get; } public EditorBeatmap Beatmap { get; }
IBeatmap IBeatmapProcessor.Beatmap => Beatmap;
private readonly IBeatmapProcessor? rulesetBeatmapProcessor; private readonly IBeatmapProcessor? rulesetBeatmapProcessor;
public EditorBeatmapProcessor(IBeatmap beatmap, Ruleset ruleset) public EditorBeatmapProcessor(EditorBeatmap beatmap, Ruleset ruleset)
{ {
Beatmap = beatmap; Beatmap = beatmap;
rulesetBeatmapProcessor = ruleset.CreateBeatmapProcessor(beatmap); rulesetBeatmapProcessor = ruleset.CreateBeatmapProcessor(beatmap);

View File

@ -121,7 +121,11 @@ namespace osu.Game.Screens.Edit
scheduledDifficultySwitch = Schedule(() => scheduledDifficultySwitch = Schedule(() =>
{ {
Beatmap.Value = nextBeatmap.Invoke(); var workingBeatmap = nextBeatmap.Invoke();
Ruleset.Value = workingBeatmap.BeatmapInfo.Ruleset;
Beatmap.Value = workingBeatmap;
state = editorState; state = editorState;
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap. // This screen is a weird exception to the rule that nothing after song select changes the global beatmap.

View File

@ -4,17 +4,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest namespace osu.Game.Screens.Edit.GameplayTest
{ {
public partial class EditorPlayer : Player public partial class EditorPlayer : Player, IKeyBindingHandler<GlobalAction>
{ {
private readonly Editor editor; private readonly Editor editor;
private readonly EditorState editorState; private readonly EditorState editorState;
@ -133,6 +137,76 @@ namespace osu.Game.Screens.Edit.GameplayTest
protected override bool CheckModsAllowFailure() => false; // never fail. protected override bool CheckModsAllowFailure() => false; // never fail.
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.EditorTestPlayToggleAutoplay:
toggleAutoplay();
return true;
case GlobalAction.EditorTestPlayToggleQuickPause:
toggleQuickPause();
return true;
case GlobalAction.EditorTestPlayQuickExitToInitialTime:
quickExit(false);
return true;
case GlobalAction.EditorTestPlayQuickExitToCurrentTime:
quickExit(true);
return true;
default:
return false;
}
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void toggleAutoplay()
{
if (DrawableRuleset.ReplayScore == null)
{
var autoplay = Ruleset.Value.CreateInstance().GetAutoplayMod();
if (autoplay == null)
return;
var score = autoplay.CreateScoreFromReplayData(GameplayState.Beatmap, [autoplay]);
// remove past frames to prevent replay frame handler from seeking back to start in an attempt to play back the entirety of the replay.
score.Replay.Frames.RemoveAll(f => f.Time <= GameplayClockContainer.CurrentTime);
DrawableRuleset.SetReplayScore(score);
// Without this schedule, the `GlobalCursorDisplay.Update()` machinery will fade the gameplay cursor out, but we still want it to show.
Schedule(() => DrawableRuleset.Cursor?.Show());
}
else
DrawableRuleset.SetReplayScore(null);
}
private void toggleQuickPause()
{
if (GameplayClockContainer.IsPaused.Value)
GameplayClockContainer.Start();
else
GameplayClockContainer.Stop();
}
private void quickExit(bool useCurrentTime)
{
if (useCurrentTime)
editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
this.Exit();
}
public override void OnEntering(ScreenTransitionEvent e) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(e); base.OnEntering(e);

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
@ -295,7 +294,7 @@ namespace osu.Game.Screens.Play
Drawable drawable = (Drawable)element; Drawable drawable = (Drawable)element;
// for now align some top components with the bottom-edge of the lowest top-anchored hud element. // for now align some top components with the bottom-edge of the lowest top-anchored hud element.
if (drawable.Anchor.HasFlagFast(Anchor.y0)) if (drawable.Anchor.HasFlag(Anchor.y0))
{ {
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
if (element is LegacyHealthDisplay) if (element is LegacyHealthDisplay)
@ -305,20 +304,20 @@ namespace osu.Game.Screens.Play
bool isRelativeX = drawable.RelativeSizeAxes == Axes.X; bool isRelativeX = drawable.RelativeSizeAxes == Axes.X;
if (drawable.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX) if (drawable.Anchor.HasFlag(Anchor.TopRight) || isRelativeX)
{ {
if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value) if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
lowestTopScreenSpaceRight = bottom; lowestTopScreenSpaceRight = bottom;
} }
if (drawable.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX) if (drawable.Anchor.HasFlag(Anchor.TopLeft) || isRelativeX)
{ {
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value) if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
lowestTopScreenSpaceLeft = bottom; lowestTopScreenSpaceLeft = bottom;
} }
} }
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element. // and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
else if (drawable.Anchor.HasFlagFast(Anchor.BottomRight) || (drawable.Anchor.HasFlagFast(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) else if (drawable.Anchor.HasFlag(Anchor.BottomRight) || (drawable.Anchor.HasFlag(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X))
{ {
var topLeft = element.ScreenSpaceDrawQuad.TopLeft; var topLeft = element.ScreenSpaceDrawQuad.TopLeft;
if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y) if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y)

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -282,7 +281,7 @@ namespace osu.Game.Screens.Ranking.Statistics
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{ {
if (invalidation.HasFlagFast(Invalidation.DrawSize)) if (invalidation.HasFlag(Invalidation.DrawSize))
{ {
if (lastDrawHeight != null && lastDrawHeight != DrawHeight) if (lastDrawHeight != null && lastDrawHeight != DrawHeight)
Scheduler.AddOnce(updateMetrics, false); Scheduler.AddOnce(updateMetrics, false);

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
@ -828,7 +827,7 @@ namespace osu.Game.Screens.Select
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{ {
// handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed).
if (invalidation.HasFlagFast(Invalidation.DrawSize)) if (invalidation.HasFlag(Invalidation.DrawSize))
itemsCache.Invalidate(); itemsCache.Invalidate();
return base.OnInvalidate(invalidation, source); return base.OnInvalidate(invalidation, source);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -19,9 +18,9 @@ namespace osu.Game.Skinning
// todo: can probably make this better via deserialisation directly using a common interface. // todo: can probably make this better via deserialisation directly using a common interface.
component.Position = drawableInfo.Position; component.Position = drawableInfo.Position;
component.Rotation = drawableInfo.Rotation; component.Rotation = drawableInfo.Rotation;
if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.X) != true)
component.Width = width; component.Width = width;
if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.Y) != true)
component.Height = height; component.Height = height;
component.Scale = drawableInfo.Scale; component.Scale = drawableInfo.Scale;
component.Anchor = drawableInfo.Anchor; component.Anchor = drawableInfo.Anchor;

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -68,10 +67,10 @@ namespace osu.Game.Skinning
Rotation = component.Rotation; Rotation = component.Rotation;
Scale = component.Scale; Scale = component.Scale;
if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.X) != true)
Width = component.Width; Width = component.Width;
if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.Y) != true)
Height = component.Height; Height = component.Height;
Anchor = component.Anchor; Anchor = component.Anchor;

View File

@ -1,7 +1,6 @@
// 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 osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osuTK; using osuTK;
@ -22,18 +21,18 @@ namespace osu.Game.Storyboards
// Either flip horizontally or negative X scale, but not both. // Either flip horizontally or negative X scale, but not both.
if (flipH ^ (vectorScale.X < 0)) if (flipH ^ (vectorScale.X < 0))
{ {
if (origin.HasFlagFast(Anchor.x0)) if (origin.HasFlag(Anchor.x0))
origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2));
else if (origin.HasFlagFast(Anchor.x2)) else if (origin.HasFlag(Anchor.x2))
origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2));
} }
// Either flip vertically or negative Y scale, but not both. // Either flip vertically or negative Y scale, but not both.
if (flipV ^ (vectorScale.Y < 0)) if (flipV ^ (vectorScale.Y < 0))
{ {
if (origin.HasFlagFast(Anchor.y0)) if (origin.HasFlag(Anchor.y0))
origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2));
else if (origin.HasFlagFast(Anchor.y2)) else if (origin.HasFlag(Anchor.y2))
origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2));
} }

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.5.0" /> <PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.627.0" /> <PackageReference Include="ppy.osu.Framework" Version="2024.702.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.622.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2024.622.0" />
<PackageReference Include="Sentry" Version="4.3.0" /> <PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.627.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2024.702.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>