1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 21:02:54 +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.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.
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: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.

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.627.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.702.0" />
</ItemGroup>
<PropertyGroup>
<!-- 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,
// 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.
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
}.Yield();

View File

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

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit
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 &&
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.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
@ -46,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TickDistanceMultiplier = 1;
[JsonIgnore]
private double velocityFactor;
public double Velocity { get; private set; }
[JsonIgnore]
private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * SliderVelocityMultiplier;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
public double TickDistance { get; private set; }
/// <summary>
/// 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);
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
// 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)

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = new ManiaModInvert(),
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))
.All(j => !j.Type.IsHit()));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].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]

View File

@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
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)
=> verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osuTK;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
else
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)
convertType |= PatternType.Mirror;
@ -102,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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
for (int i = RandomStart; i < TotalColumns; i++)
@ -114,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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
&& (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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
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 (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"
int targetColumn = lastColumn + 1;
@ -152,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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"
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);
if (convertType.HasFlagFast(PatternType.Mirror))
if (convertType.HasFlag(PatternType.Mirror))
{
if (ConversionDifficulty > 6.5)
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 (convertType.HasFlagFast(PatternType.LowProbability))
if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.78, 0.42, 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 (convertType.HasFlagFast(PatternType.LowProbability))
if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.35, 0.08, 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 (convertType.HasFlagFast(PatternType.LowProbability))
if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.18, 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)
{
if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1)
if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair;
if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart)
if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair;
}
@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
var pattern = new Pattern();
bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack);
bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack);
if (!allowStacking)
noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects);
@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int getNextColumn(int last)
{
if (convertType.HasFlagFast(PatternType.Gathered))
if (convertType.HasFlag(PatternType.Gathered))
{
last++;
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>
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);
var pattern = new Pattern();

View File

@ -7,7 +7,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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.85, 0.36, 0.03);
@ -147,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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.56, 0.18, 0);
@ -155,13 +154,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
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.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.27, 0, 0);
@ -219,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
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);
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;
bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability);
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
@ -404,7 +403,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int endTime = startTime + SegmentDuration * SpanCount;
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);
for (int i = 0; i < columnRepeat; i++)
@ -433,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
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);
// Create the hold note

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@ -89,79 +88,79 @@ namespace osu.Game.Rulesets.Mania
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlagFast(LegacyMods.Nightcore))
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime))
else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect))
if (mods.HasFlag(LegacyMods.Perfect))
yield return new ManiaModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new ManiaModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema))
if (mods.HasFlag(LegacyMods.Cinema))
yield return new ManiaModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay))
else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new ManiaModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy))
if (mods.HasFlag(LegacyMods.Easy))
yield return new ManiaModEasy();
if (mods.HasFlagFast(LegacyMods.FadeIn))
if (mods.HasFlag(LegacyMods.FadeIn))
yield return new ManiaModFadeIn();
if (mods.HasFlagFast(LegacyMods.Flashlight))
if (mods.HasFlag(LegacyMods.Flashlight))
yield return new ManiaModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime))
if (mods.HasFlag(LegacyMods.HalfTime))
yield return new ManiaModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock))
if (mods.HasFlag(LegacyMods.HardRock))
yield return new ManiaModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden))
if (mods.HasFlag(LegacyMods.Hidden))
yield return new ManiaModHidden();
if (mods.HasFlagFast(LegacyMods.Key1))
if (mods.HasFlag(LegacyMods.Key1))
yield return new ManiaModKey1();
if (mods.HasFlagFast(LegacyMods.Key2))
if (mods.HasFlag(LegacyMods.Key2))
yield return new ManiaModKey2();
if (mods.HasFlagFast(LegacyMods.Key3))
if (mods.HasFlag(LegacyMods.Key3))
yield return new ManiaModKey3();
if (mods.HasFlagFast(LegacyMods.Key4))
if (mods.HasFlag(LegacyMods.Key4))
yield return new ManiaModKey4();
if (mods.HasFlagFast(LegacyMods.Key5))
if (mods.HasFlag(LegacyMods.Key5))
yield return new ManiaModKey5();
if (mods.HasFlagFast(LegacyMods.Key6))
if (mods.HasFlag(LegacyMods.Key6))
yield return new ManiaModKey6();
if (mods.HasFlagFast(LegacyMods.Key7))
if (mods.HasFlag(LegacyMods.Key7))
yield return new ManiaModKey7();
if (mods.HasFlagFast(LegacyMods.Key8))
if (mods.HasFlag(LegacyMods.Key8))
yield return new ManiaModKey8();
if (mods.HasFlagFast(LegacyMods.Key9))
if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9();
if (mods.HasFlagFast(LegacyMods.KeyCoop))
if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages();
if (mods.HasFlagFast(LegacyMods.NoFail))
if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail();
if (mods.HasFlagFast(LegacyMods.Random))
if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom();
if (mods.HasFlagFast(LegacyMods.Mirror))
if (mods.HasFlag(LegacyMods.Mirror))
yield return new ManiaModMirror();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModEasy(),
new ManiaModNoFail(),
new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()),
new ManiaModNoRelease(),
};
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>
/// The head note of the hold.
/// </summary>
public HeadNote Head { get; private set; }
public HeadNote Head { get; protected set; }
/// <summary>
/// The tail note of the hold.
/// </summary>
public TailNote Tail { get; private set; }
public TailNote Tail { get; protected set; }
/// <summary>
/// The body of the hold.
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
/// </summary>
public HoldNoteBody Body { get; private set; }
public HoldNoteBody Body { get; protected set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;

View File

@ -192,12 +192,12 @@ namespace osu.Game.Rulesets.Mania.UI
if (press)
{
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value);
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
}
else
{
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value);
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
@ -160,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return grid switch
{
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
};
}

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
@ -177,6 +178,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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)
{
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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
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.Drawables;
using osu.Game.Tests.Visual;
@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
}
[Test]
@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
}
[Test]
@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(1, PathType.LINEAR);
}
[Test]
@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
assertLength(100);
}
@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(4);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(1, PathType.LINEAR);
}
[Test]
@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(1, PathType.PERFECT_CURVE);
}
[Test]
@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, 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]
@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(808, tolerance: 10);
assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
assertControlPointType(4, null);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(1, null);
assertFinalControlPointType(2, null);
assertFinalControlPointType(3, null);
assertFinalControlPointType(4, null);
}
[Test]
@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, PathType.BSpline(4));
assertControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(1, PathType.BSpline(4));
assertFinalControlPointType(2, PathType.BSpline(4));
assertFinalControlPointType(3, null);
}
[Test]
@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
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)));
@ -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 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) =>
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
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;
@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (allowSelection)
d.RequestSelection = selectionRequested;
d.ControlPoint.Changed += controlPointChanged;
d.DragStarted = DragStarted;
d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded;
@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
foreach (var point in e.OldItems.Cast<PathControlPoint>())
{
point.Changed -= controlPointChanged;
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
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)
{
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)
{
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
@ -254,30 +339,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
/// <summary>
/// Attempts to set the given control point piece to the given path type.
/// If that would fail, try to change the path such that it instead succeeds
/// Attempts to set all selected control point pieces to the given path type.
/// If that fails, try to change the path such that it instead succeeds
/// in a UX-friendly way.
/// </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>
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
private void updatePathTypeOfSelectedPieces(PathType? type)
{
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
changeHandler?.BeginChange();
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,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
if (pointsInSegment.Count > thirdPointIndex + 1)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
if (type?.Type == SplineType.PerfectCurve)
{
// 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;
piece.ControlPoint.Type = type;
EnsureValidPathTypes();
changeHandler?.EndChange();
}
[Resolved(CanBeNull = true)]
@ -290,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints;
private List<MenuItem> curveTypeItems;
public void DragStarted(PathControlPoint controlPoint)
{
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();
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));
curveTypeItems.Add(new OsuMenuItemSpacer());
// special inherit case
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))
{
curveTypeItems.Add(new OsuMenuItemSpacer());
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
}
var menuItems = new List<MenuItem>
{
@ -424,35 +524,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
() => DeleteSelected())
);
updateCurveMenuItems();
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);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
if (curveTypeItems == null)
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))
updatePathType(p, type);
if (countOfState == totalCount)
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();
});
if (countOfState == totalCount)
item.State.Value = TernaryState.True;
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
return item;
public CurveTypeMenuItem(PathType? pathType, Action<TernaryState> action)
: base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action)
{
PathType = pathType;
}
}
}
}

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved(CanBeNull = true)]
[CanBeNull]
@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
placeNewControlPoint();
else
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
beginNewSegment(lastPoint);
break;
}
@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
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)
{
if (e.Button != MouseButton.Left)
@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
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()
{
base.Update();
@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePathType()
{
if (usingCustomSegmentType)
{
controlPointVisualiser.EnsureValidPathTypes();
return;
}
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(4);
@ -316,6 +389,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return lastPiece.IsHovered != true;
}
private void placeNewControlPoint()
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
private void updateSlider()
{
if (state == SliderPlacementState.Drawing)

View File

@ -55,7 +55,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
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 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.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit
{
@ -20,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IExpandingContainer? expandingContainer { get; set; }
/// <summary>
/// X position of the grid's origin.
/// </summary>
@ -55,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f)
{
MinValue = -45f,
MaxValue = 45f,
MinValue = -180f,
MaxValue = 180f,
Precision = 1f
};
@ -72,10 +82,13 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
public Bindable<Vector2> SpacingVector { get; } = new Bindable<Vector2>();
public Bindable<PositionSnapGridType> GridType { get; } = new Bindable<PositionSnapGridType>();
private ExpandableSlider<float> startPositionXSlider = null!;
private ExpandableSlider<float> startPositionYSlider = null!;
private ExpandableSlider<float> spacingSlider = null!;
private ExpandableSlider<float> gridLinesRotationSlider = null!;
private EditorRadioButtonCollection gridTypeButtons = null!;
public OsuGridToolboxGroup()
: base("grid")
@ -109,6 +122,31 @@ namespace osu.Game.Rulesets.Osu.Edit
Current = GridLinesRotation,
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;
@ -118,6 +156,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.LoadComplete();
gridTypeButtons.Items.First().Select();
StartPositionX.BindValueChanged(x =>
{
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
@ -145,6 +185,32 @@ namespace osu.Game.Rulesets.Osu.Edit
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
}, 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()
@ -167,5 +233,42 @@ namespace osu.Game.Rulesets.Osu.Edit
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.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
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
updateDistanceSnapGrid();
updatePositionSnapGrid();
OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true);
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)
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.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
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;
LayerBelowRuleset.Add(positionSnapGrid);
@ -194,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit
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.
// 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.
// 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.
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));
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);
if (snapType.HasFlagFast(SnapType.RelativeGrids))
if (snapType.HasFlag(SnapType.RelativeGrids))
{
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)
{

View File

@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public SliderCompositionTool()
: 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);

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@ -79,43 +78,43 @@ namespace osu.Game.Rulesets.Taiko
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlagFast(LegacyMods.Nightcore))
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new TaikoModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime))
else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new TaikoModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect))
if (mods.HasFlag(LegacyMods.Perfect))
yield return new TaikoModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new TaikoModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema))
if (mods.HasFlag(LegacyMods.Cinema))
yield return new TaikoModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay))
else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new TaikoModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy))
if (mods.HasFlag(LegacyMods.Easy))
yield return new TaikoModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight))
if (mods.HasFlag(LegacyMods.Flashlight))
yield return new TaikoModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime))
if (mods.HasFlag(LegacyMods.HalfTime))
yield return new TaikoModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock))
if (mods.HasFlag(LegacyMods.HardRock))
yield return new TaikoModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden))
if (mods.HasFlag(LegacyMods.Hidden))
yield return new TaikoModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail))
if (mods.HasFlag(LegacyMods.NoFail))
yield return new TaikoModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax))
if (mods.HasFlag(LegacyMods.Relax))
yield return new TaikoModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
if (mods.HasFlag(LegacyMods.ScoreV2))
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-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
// The control point at the end time of the slider should be applied
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
// The fourth object is a slider.
// `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];

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

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
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));
}
[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()
{
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));
}
[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", () =>
{
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 osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
@ -102,5 +104,56 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("change tool to circle", () => InputManager.Key(Key.Number2));
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
{
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));
}
[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)
=> 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 osu.Game.Beatmaps.ControlPoints;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps
@ -62,7 +61,7 @@ namespace osu.Game.Beatmaps
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>();

View File

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

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
@ -33,7 +32,7 @@ namespace osu.Game.Beatmaps.Formats
/// <remarks>
/// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319
/// </remarks>
private const double control_point_leniency = 5;
public const double CONTROL_POINT_LENIENCY = 5;
internal static RulesetStore? RulesetStore;
@ -160,20 +159,24 @@ namespace osu.Game.Beatmaps.Formats
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)
{
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++)
{
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;
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>
@ -528,8 +531,8 @@ namespace osu.Game.Beatmaps.Formats
if (split.Length >= 8)
{
LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]);
kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine);
kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine);
}
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();

View File

@ -282,19 +282,39 @@ namespace osu.Game.Beatmaps.Formats
{
foreach (var hitObject in hitObjects)
{
if (hitObject.Samples.Count > 0)
if (hitObject is IHasRepeats hasNodeSamples)
{
int volume = hitObject.Samples.Max(o => o.Volume);
int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
? hitObject.Samples.OfType<ConvertHitObjectParser.LegacyHitSampleInfo>().Max(o => o.CustomSampleBank)
: -1;
double spanDuration = hasNodeSamples.Duration / hasNodeSamples.SpanCount();
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))
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)

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
@ -41,11 +40,11 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The breaks in this beatmap.
/// </summary>
BindableList<BreakPeriod> Breaks { get; }
List<BreakPeriod> Breaks { get; set; }
/// <summary>
/// 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>
List<string> UnhandledEventLines { get; }

View File

@ -10,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
@ -164,13 +163,13 @@ namespace osu.Game.Database
var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask;
if (content.HasFlagFast(StableContent.Beatmaps))
if (content.HasFlag(StableContent.Beatmaps))
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));
if (content.HasFlagFast(StableContent.Collections))
if (content.HasFlag(StableContent.Collections))
{
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess)
{
@ -180,7 +179,7 @@ namespace osu.Game.Database
}.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));
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);

View File

@ -15,7 +15,6 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -1035,7 +1034,7 @@ namespace osu.Game.Database
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;
score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray();

View File

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

View File

@ -119,9 +119,14 @@ namespace osu.Game.Graphics.UserInterface
Expanded.BindValueChanged(v =>
{
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;
}, 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.Framework.Audio.Track;
using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
@ -57,15 +56,15 @@ namespace osu.Game.Graphics.UserInterface
set
{
base.Origin = value;
c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight;
c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft;
c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight;
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(c2, false);
c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1;
c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0;
c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1;
c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0;
Add(c1);
Add(c2);
}

View File

@ -34,6 +34,7 @@ namespace osu.Game.Input.Bindings
/// </remarks>
public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings
.Concat(editorKeyBindings)
.Concat(editorTestPlayKeyBindings)
.Concat(inGameKeyBindings)
.Concat(replayKeyBindings)
.Concat(songSelectKeyBindings)
@ -68,6 +69,9 @@ namespace osu.Game.Input.Bindings
case GlobalActionCategory.Overlays:
return overlayKeyBindings;
case GlobalActionCategory.EditorTestPlay:
return editorTestPlayKeyBindings;
default:
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.T }, GlobalAction.ToggleToolbar),
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),
@ -118,6 +121,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
};
private static IEnumerable<KeyBinding> editorKeyBindings => new[]
@ -145,6 +149,14 @@ namespace osu.Game.Input.Bindings
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[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
@ -432,6 +444,18 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.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
@ -442,6 +466,7 @@ namespace osu.Game.Input.Bindings
Replay,
SongSelect,
AudioControl,
Overlays
Overlays,
EditorTestPlay,
}
}

View File

@ -374,6 +374,26 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Increase mod speed"
/// </summary>

View File

@ -49,6 +49,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Reset all bindings in section"
/// </summary>

View File

@ -271,7 +271,7 @@ namespace osu.Game.Online.Chat
handleAdvanced(advanced_link_regex, result, startIndex);
// 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
handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel);

View File

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

View File

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@ -131,7 +130,7 @@ namespace osu.Game.Overlays.Music
filter.Search.HoldFocus = true;
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);
}

View File

@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame),
new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay),
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.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -161,7 +160,7 @@ namespace osu.Game.Overlays
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
if (invalidation.HasFlagFast(Invalidation.DrawSize))
if (invalidation.HasFlag(Invalidation.DrawSize))
headerTextVisibilityCache.Invalidate();
return base.OnInvalidate(invalidation, source);

View File

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

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
@ -51,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor
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 Vector2? defaultOrigin;

View File

@ -4,7 +4,6 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -150,9 +149,9 @@ namespace osu.Game.Overlays.Toolbar
{
Direction = FillDirection.Vertical,
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,
Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5),
Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),
Alpha = 0,
Children = new Drawable[]
{

View File

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

View File

@ -9,7 +9,6 @@ using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -328,7 +327,12 @@ namespace osu.Game.Rulesets.Difficulty
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 double TotalBreakTime => baseBeatmap.TotalBreakTime;

View File

@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Edit
// Events
new CheckBreaks(),
// Metadata
new CheckTitleMarkers(),
};
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
{
// 00:00:000 (...) - test
// original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
public static readonly Regex TIME_REGEX = new Regex(@"\b(((?<minutes>\d{2,}):(?<seconds>[0-5]\d)[:.](?<milliseconds>\d{3}))(?<selection>\s\([^)]+\))?)", RegexOptions.Compiled);
/// <summary>
/// Used for parsing in contexts where we don't want e.g. normal times of day to be parsed as timestamps (e.g. chat)
/// 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)
{
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)
{
@ -24,16 +52,14 @@ namespace osu.Game.Rulesets.Edit
return false;
}
bool result = true;
int timeMin, timeSec, timeMsec;
result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin);
result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec);
result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec);
int.TryParse(match.Groups[@"minutes"].Value, out timeMin);
int.TryParse(match.Groups[@"seconds"].Value, out timeSec);
int.TryParse(match.Groups[@"milliseconds"].Value, out timeMsec);
// somewhat sane limit for timestamp duration (10 hours).
result &= timeMin < 600;
if (!result)
if (timeMin >= 600)
{
parsedTime = null;
parsedSelection = null;
@ -42,8 +68,7 @@ namespace osu.Game.Rulesets.Edit
parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec);
parsedSelection = match.Groups[@"selection"].Value.Trim();
if (!string.IsNullOrEmpty(parsedSelection))
parsedSelection = parsedSelection[1..^1];
parsedSelection = !string.IsNullOrEmpty(parsedSelection) ? parsedSelection[1..^1] : null;
return true;
}
}

View File

@ -10,7 +10,6 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -216,14 +215,14 @@ namespace osu.Game.Rulesets.Edit
toolboxCollection.Items = CompositionTools
.Prepend(new SelectTool())
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
.Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
.ToList();
foreach (var item in toolboxCollection.Items)
{
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);
double? targetTime = null;
if (snapType.HasFlagFast(SnapType.GlobalGrids))
if (snapType.HasFlag(SnapType.GlobalGrids))
{
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);
}
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
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
if (AutomaticBankAssignment)
{
// Take the hitnormal sample of the last hit object
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Edit.Tools
{
@ -11,14 +10,16 @@ namespace osu.Game.Rulesets.Edit.Tools
{
public readonly string Name;
public LocalisableString TooltipText { get; init; }
protected HitObjectCompositionTool(string 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;
}

View File

@ -12,7 +12,6 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4;
type &= ~LegacyHitObjectType.ComboOffset;
bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo);
bool combo = type.HasFlag(LegacyHitObjectType.NewCombo);
type &= ~LegacyHitObjectType.NewCombo;
var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
@ -66,14 +65,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
HitObject result = null;
if (type.HasFlagFast(LegacyHitObjectType.Circle))
if (type.HasFlag(LegacyHitObjectType.Circle))
{
result = CreateHit(pos, combo, comboOffset);
if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo);
}
else if (type.HasFlagFast(LegacyHitObjectType.Slider))
else if (type.HasFlag(LegacyHitObjectType.Slider))
{
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);
}
else if (type.HasFlagFast(LegacyHitObjectType.Spinner))
else if (type.HasFlag(LegacyHitObjectType.Spinner))
{
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)
readCustomSampleBanks(split[6], bankInfo);
}
else if (type.HasFlagFast(LegacyHitObjectType.Hold))
else if (type.HasFlag(LegacyHitObjectType.Hold))
{
// 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,
// 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
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)));
type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)));
}
else
{
@ -480,13 +479,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
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));
if (type.HasFlagFast(LegacyHitSoundType.Whistle))
if (type.HasFlag(LegacyHitSoundType.Whistle))
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));
return soundTypes;

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using System.Linq;
using osu.Framework.Allocation;
@ -12,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States;
using osu.Game.Configuration;
@ -21,6 +20,7 @@ using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.UI
@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.UI
public readonly KeyBindingContainer<T> KeyBindingContainer;
[Resolved(CanBeNull = true)]
private ScoreProcessor scoreProcessor { get; set; }
[Resolved]
private ScoreProcessor? scoreProcessor { get; set; }
private ReplayRecorder recorder;
private ReplayRecorder? recorder;
public ReplayRecorder Recorder
public ReplayRecorder? Recorder
{
set
{
@ -103,14 +103,23 @@ namespace osu.Game.Rulesets.UI
#region IHasReplayHandler
private ReplayInputHandler replayInputHandler;
private ReplayInputHandler? replayInputHandler;
public ReplayInputHandler ReplayInputHandler
public ReplayInputHandler? ReplayInputHandler
{
get => replayInputHandler;
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;
UseParentInput = replayInputHandler == null;
@ -124,8 +133,8 @@ namespace osu.Game.Rulesets.UI
#region Setting application (disables etc.)
private Bindable<bool> mouseDisabled;
private Bindable<bool> tapsDisabled;
private Bindable<bool> mouseDisabled = null!;
private Bindable<bool> tapsDisabled = null!;
protected override bool Handle(UIEvent e)
{
@ -222,14 +231,34 @@ namespace osu.Game.Rulesets.UI
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
where T : struct
{
public ReplayState<T> LastReplayState;
public ReplayState<T>? LastReplayState;
public RulesetInputManagerInputState(InputState state = null)
public RulesetInputManagerInputState(InputState state)
: base(state)
{
}

View File

@ -10,9 +10,11 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -25,14 +27,16 @@ namespace osu.Game.Screens.Edit.Components
public partial class PlaybackControl : BottomBarContainer
{
private IconButton playButton = null!;
private PlaybackSpeedControl playbackSpeedControl = null!;
[Resolved]
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]
private void load(OverlayColourProvider colourProvider)
private void load(OverlayColourProvider colourProvider, Editor? editor)
{
Background.Colour = colourProvider.Background4;
@ -47,31 +51,61 @@ namespace osu.Game.Screens.Edit.Components
Icon = FontAwesome.Regular.PlayCircle,
Action = togglePause,
},
new OsuSpriteText
playbackSpeedControl = new PlaybackSpeedControl
{
Origin = Anchor.BottomLeft,
Text = EditorStrings.PlaybackSpeed,
RelativePositionAxes = Axes.Y,
Y = 0.5f,
Padding = new MarginPadding { Left = 45 }
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Left = 45 },
Child = new PlaybackTabControl { Current = freqAdjust },
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Left = 45, },
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = EditorStrings.PlaybackSpeed,
},
new PlaybackTabControl
{
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)
{
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust);
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment);
base.Dispose(isDisposing);
}
@ -109,6 +143,11 @@ namespace osu.Game.Screens.Edit.Components
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 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)
{
updateState();
return true;
return false;
}
protected override void OnHoverLost(HoverLostEvent e) => updateState();

View File

@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public readonly Func<Drawable>? CreateIcon;
public readonly Func<Drawable?>? CreateIcon;
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;
CreateIcon = createIcon;

View File

@ -4,8 +4,12 @@
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
@ -13,7 +17,6 @@ namespace osu.Game.Screens.Edit.Components
{
public partial class TimeInfoContainer : BottomBarContainer
{
private OsuSpriteText trackTimer = null!;
private OsuSpriteText bpm = null!;
[Resolved]
@ -29,14 +32,7 @@ namespace osu.Game.Screens.Edit.Components
Children = new Drawable[]
{
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),
Y = -10,
},
new TimestampControl(),
bpm = new OsuSpriteText
{
Colour = colours.Orange1,
@ -47,19 +43,12 @@ namespace osu.Game.Screens.Edit.Components
};
}
private double? lastTime;
private double? lastBPM;
protected override void Update()
{
base.Update();
if (lastTime != editorClock.CurrentTime)
{
lastTime = editorClock.CurrentTime;
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
}
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
if (lastBPM != newBPM)
@ -68,5 +57,109 @@ namespace osu.Game.Screens.Edit.Components
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.BindCollectionChanged((_, _) =>
{
Clear();
foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod));
}, 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;
}
AddSampleBank(bankName);
SetSampleBank(bankName);
}
break;
@ -177,14 +177,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
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)
{
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
/// <summary>
/// Adds a sample bank to all selected <see cref="HitObject"/>s.
/// Sets the sample bank for all selected <see cref="HitObject"/>s.
/// </summary>
/// <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;
EditorBeatmap.PerformOnSelection(h =>
@ -207,17 +233,37 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
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);
});
}
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>
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
if (SelectedItems.All(h => h.Samples.Any(s => s.Name == sampleName)))
if (SelectedItems.All(h => hasRelevantSample(h, sampleName)))
return;
EditorBeatmap.PerformOnSelection(h =>
@ -228,6 +274,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
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);
});
}
@ -238,12 +301,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
if (SelectedItems.All(h => h.Samples.All(s => s.Name != sampleName)))
if (SelectedItems.All(h => !hasRelevantSample(h, sampleName)))
return;
EditorBeatmap.PerformOnSelection(h =>
{
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);
});
}

View File

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

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -74,9 +73,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
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;
if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1))
if (direction == Direction.Vertical && !handle.Anchor.HasFlag(Anchor.y1))
handle.Anchor ^= Anchor.y0 | Anchor.y2;
}
}

View File

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

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Objects;
using osuTK;
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 Action<BreakPeriod>? OnDeleted { get; init; }
private Box background = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public TimelineBreak(BreakPeriod b)
{
Break.Value = b;
@ -42,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5 },
Child = new Box
Child = background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray5,
@ -77,6 +88,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, 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
{
public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();

View File

@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private Timeline timeline { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? editorChangeHandler { get; set; }
/// <summary>
/// The visible time/position range of the timeline.
/// </summary>
@ -71,7 +74,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!shouldBeVisible(breakPeriod))
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;
}
public void HandleTimestamp(string timestamp)
public bool HandleTimestamp(string timestamp, bool notifyOnError = false)
{
if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection))
{
Schedule(() => notifications?.Post(new SimpleErrorNotification
if (notifyOnError)
{
Icon = FontAwesome.Solid.ExclamationTriangle,
Text = EditorStrings.FailedToParseEditorLink
}));
return;
Schedule(() => notifications?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationTriangle,
Text = EditorStrings.FailedToParseEditorLink
}));
}
return false;
}
editorBeatmap.SelectedHitObjects.Clear();
@ -1306,7 +1310,7 @@ namespace osu.Game.Screens.Edit
if (string.IsNullOrEmpty(selection))
{
clock.SeekSmoothlyTo(position);
return;
return true;
}
// Seek to the next closest HitObject instead
@ -1321,6 +1325,7 @@ namespace osu.Game.Screens.Edit
// Delegate handling the selection to the ruleset.
currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection);
return true;
}
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)
trackStartTime(obj);
Breaks = new BindableList<BreakPeriod>(playableBeatmap.Breaks);
Breaks.BindCollectionChanged((_, _) => playableBeatmap.Breaks = Breaks.ToList());
PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime);
PreviewTime.BindValueChanged(s =>
{
@ -172,7 +175,13 @@ namespace osu.Game.Screens.Edit
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;

View File

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

View File

@ -121,7 +121,11 @@ namespace osu.Game.Screens.Edit
scheduledDifficultySwitch = Schedule(() =>
{
Beatmap.Value = nextBeatmap.Invoke();
var workingBeatmap = nextBeatmap.Invoke();
Ruleset.Value = workingBeatmap.BeatmapInfo.Ruleset;
Beatmap.Value = workingBeatmap;
state = editorState;
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest
{
public partial class EditorPlayer : Player
public partial class EditorPlayer : Player, IKeyBindingHandler<GlobalAction>
{
private readonly Editor editor;
private readonly EditorState editorState;
@ -133,6 +137,76 @@ namespace osu.Game.Screens.Edit.GameplayTest
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)
{
base.OnEntering(e);

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
@ -295,7 +294,7 @@ namespace osu.Game.Screens.Play
Drawable drawable = (Drawable)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.
if (element is LegacyHealthDisplay)
@ -305,20 +304,20 @@ namespace osu.Game.Screens.Play
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)
lowestTopScreenSpaceRight = bottom;
}
if (drawable.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
if (drawable.Anchor.HasFlag(Anchor.TopLeft) || isRelativeX)
{
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
lowestTopScreenSpaceLeft = bottom;
}
}
// 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;
if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y)

View File

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

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@ -828,7 +827,7 @@ namespace osu.Game.Screens.Select
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).
if (invalidation.HasFlagFast(Invalidation.DrawSize))
if (invalidation.HasFlag(Invalidation.DrawSize))
itemsCache.Invalidate();
return base.OnInvalidate(invalidation, source);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
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.
component.Position = drawableInfo.Position;
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;
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.Scale = drawableInfo.Scale;
component.Anchor = drawableInfo.Anchor;

View File

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

View File

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

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

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