1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 02:53:21 +08:00

Merge branch 'master' into beatmap-info-purge

This commit is contained in:
Dean Herbert 2024-07-23 12:02:20 +09:00
commit d707e29ff7
No known key found for this signature in database
423 changed files with 13063 additions and 3519 deletions

View File

@ -67,7 +67,7 @@ jobs:
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@ -111,7 +111,7 @@ jobs:
steps:
- name: Check permissions
run: |
ALLOWED_USERS=(smoogipoo peppy bdach)
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte)
for i in "${ALLOWED_USERS[@]}"; do
if [[ "${{ github.actor }}" == "$i" ]]; then
exit 0

2
.gitignore vendored
View File

@ -265,6 +265,8 @@ __pycache__/
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/*/.idea/projectSettingsUpdater.xml
.idea/*/.idea/encodings.xml
# Generated files
.idea/**/contentModel.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

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.528.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.720.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Security.Principal;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -21,48 +20,14 @@ namespace osu.Desktop.Security
[Resolved]
private INotificationOverlay notifications { get; set; } = null!;
private bool elevated;
[BackgroundDependencyLoader]
private void load()
{
elevated = checkElevated();
}
protected override void LoadComplete()
{
base.LoadComplete();
if (elevated)
if (Environment.IsPrivilegedProcess)
notifications.Post(new ElevatedPrivilegesNotification());
}
private bool checkElevated()
{
try
{
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
if (!OperatingSystem.IsWindows()) return false;
var windowsIdentity = WindowsIdentity.GetCurrent();
var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
case RuntimeInfo.Platform.macOS:
case RuntimeInfo.Platform.Linux:
return Mono.Unix.Native.Syscall.geteuid() == 0;
}
}
catch
{
}
return false;
}
private partial class ElevatedPrivilegesNotification : SimpleNotification
{
public override bool IsImportant => true;

View File

@ -24,7 +24,6 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
</ItemGroup>

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

@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
StartTime = 5000,
}
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(2000, 4000),
}

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;
@ -29,6 +28,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
@ -62,43 +62,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();
}
@ -223,6 +223,12 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new DifficultySection(),
new ColoursSection(),
];
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= Math.Pow(accuracy(), 5.5);
if (score.Mods.Any(m => m is ModNoFail))
value *= 0.90;
value *= Math.Max(0.90, 1.0 - 0.02 * numMiss);
return new CatchPerformanceAttributes
{

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
base.LoadComplete();
inputManager = GetContainingInputManager();
inputManager = GetContainingInputManager()!;
BeginPlacement();
}

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,14 +63,21 @@ 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)
{
base.CreateNestedHitObjects(cancellationToken);
this.PopulateNodeSamples();
var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
int nodeIndex = 0;

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
var replayState = (GetContainingInputManager()!.CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
SetCatcherPosition(
replayState?.CatcherX ??

View File

@ -1,13 +1,18 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
@ -30,5 +35,43 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
}
[Test]
public void TestReloadOnBPMChange()
{
HitObjectComposer oldComposer = null!;
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
AddStep("switch to timing tab", () => InputManager.Key(Key.F3));
AddUntilStep("wait for loaded", () => this.ChildrenOfType<TimingAdjustButton>().ElementAtOrDefault(1), () => Is.Not.Null);
AddStep("change timing point BPM", () =>
{
var bpmControl = this.ChildrenOfType<TimingAdjustButton>().ElementAt(1);
InputManager.MoveMouseTo(bpmControl);
InputManager.Click(MouseButton.Left);
});
AddStep("switch back to composer", () => InputManager.Key(Key.F1));
AddUntilStep("composer reloaded", () =>
{
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
return composer != null && composer != oldComposer;
});
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
AddStep("undo", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Z);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("composer reloaded", () =>
{
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
return composer != null && composer != oldComposer;
});
}
}
}

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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osuTK;
@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public partial class TestHitObjectComposer : HitObjectComposer
{
public override Playfield Playfield { get; }
public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException();
public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>();
public override bool CursorInPlacementArea => false;
@ -100,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new System.NotImplementedException();
throw new NotImplementedException();
}
}
}

View File

@ -186,8 +186,106 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
}
[Test]
public void TestDragHoldNoteHead()
{
setScrollStep(ScrollingDirection.Down);
HoldNote holdNote = null;
AddStep("setup beatmap", () =>
{
composer.EditorBeatmap.Clear();
composer.EditorBeatmap.Add(holdNote = new HoldNote
{
Column = 1,
StartTime = 250,
EndTime = 750,
});
});
DrawableHoldNote drawableHoldNote = null;
EditHoldNoteEndPiece headPiece = null;
AddStep("select blueprint", () =>
{
drawableHoldNote = this.ChildrenOfType<DrawableHoldNote>().Single();
InputManager.MoveMouseTo(drawableHoldNote);
InputManager.Click(MouseButton.Left);
});
AddStep("grab hold note head", () =>
{
headPiece = this.ChildrenOfType<EditHoldNoteEndPiece>().First();
InputManager.MoveMouseTo(headPiece);
InputManager.PressButton(MouseButton.Left);
});
AddStep("drag head downwards", () =>
{
InputManager.MoveMouseTo(headPiece, new Vector2(0, 100));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("start time moved back", () => holdNote!.StartTime, () => Is.LessThan(250));
AddAssert("end time unchanged", () => holdNote.EndTime, () => Is.EqualTo(750));
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
}
[Test]
public void TestDragHoldNoteTail()
{
setScrollStep(ScrollingDirection.Down);
HoldNote holdNote = null;
AddStep("setup beatmap", () =>
{
composer.EditorBeatmap.Clear();
composer.EditorBeatmap.Add(holdNote = new HoldNote
{
Column = 1,
StartTime = 250,
EndTime = 750,
});
});
DrawableHoldNote drawableHoldNote = null;
EditHoldNoteEndPiece tailPiece = null;
AddStep("select blueprint", () =>
{
drawableHoldNote = this.ChildrenOfType<DrawableHoldNote>().Single();
InputManager.MoveMouseTo(drawableHoldNote);
InputManager.Click(MouseButton.Left);
});
AddStep("grab hold note tail", () =>
{
tailPiece = this.ChildrenOfType<EditHoldNoteEndPiece>().Last();
InputManager.MoveMouseTo(tailPiece);
InputManager.PressButton(MouseButton.Left);
});
AddStep("drag tail upwards", () =>
{
InputManager.MoveMouseTo(tailPiece, new Vector2(0, -100));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("start time unchanged", () => holdNote!.StartTime, () => Is.EqualTo(250));
AddAssert("end time moved forward", () => holdNote.EndTime, () => Is.GreaterThan(750));
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)

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

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("Hold key", () =>
{
clock.CurrentTime = 0;
note.OnPressed(new KeyBindingPressEvent<ManiaAction>(GetContainingInputManager().CurrentState, ManiaAction.Key1));
note.OnPressed(new KeyBindingPressEvent<ManiaAction>(GetContainingInputManager()!.CurrentState, ManiaAction.Key1));
});
AddStep("progress time", () => clock.CurrentTime = 500);
AddAssert("head is visible", () => note.Head.Alpha == 1);

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

@ -0,0 +1,81 @@
// 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.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
public partial class EditHoldNoteEndPiece : CompositeDrawable
{
public Action? DragStarted { get; init; }
public Action<Vector2>? Dragging { get; init; }
public Action? DragEnded { get; init; }
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Height = DefaultNotePiece.NOTE_HEIGHT;
CornerRadius = 5;
Masking = true;
InternalChild = new DefaultNotePiece();
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
protected override bool OnDragStart(DragStartEvent e)
{
DragStarted?.Invoke();
return true;
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
Dragging?.Invoke(e.ScreenSpaceMousePosition);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
DragEnded?.Invoke();
}
private void updateState()
{
var colour = colours.Yellow;
if (IsHovered)
colour = colour.Lighten(1);
Colour = colour;
}
}
}

View File

@ -1,16 +1,16 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@ -18,10 +18,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
{
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
private EditNotePiece head;
private EditNotePiece tail;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private EditorBeatmap? editorBeatmap { get; set; }
[Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; }
private EditHoldNoteEndPiece head = null!;
private EditHoldNoteEndPiece tail = null!;
public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
@ -33,8 +42,43 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
InternalChildren = new Drawable[]
{
head = new EditNotePiece { RelativeSizeAxes = Axes.X },
tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
head = new EditHoldNoteEndPiece
{
RelativeSizeAxes = Axes.X,
DragStarted = () => changeHandler?.BeginChange(),
Dragging = pos =>
{
double endTimeBeforeDrag = HitObject.EndTime;
double proposedStartTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos);
double proposedEndTime = endTimeBeforeDrag;
if (proposedStartTime >= proposedEndTime)
return;
HitObject.StartTime = proposedStartTime;
HitObject.EndTime = proposedEndTime;
editorBeatmap?.Update(HitObject);
},
DragEnded = () => changeHandler?.EndChange(),
},
tail = new EditHoldNoteEndPiece
{
RelativeSizeAxes = Axes.X,
DragStarted = () => changeHandler?.BeginChange(),
Dragging = pos =>
{
double proposedStartTime = HitObject.StartTime;
double proposedEndTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos);
if (proposedStartTime >= proposedEndTime)
return;
HitObject.StartTime = proposedStartTime;
HitObject.EndTime = proposedEndTime;
editorBeatmap?.Update(HitObject);
},
DragEnded = () => changeHandler?.EndChange(),
},
new Container
{
RelativeSizeAxes = Axes.Both,

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public BindableBool ShowSpeedChanges { get; } = new BindableBool();
public double? TimelineTimeRange { get; set; }
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
@ -38,5 +41,11 @@ namespace osu.Game.Rulesets.Mania.Edit
Origin = Anchor.Centre,
Size = Vector2.One
};
protected override void Update()
{
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<int>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
base.Update();
}
}
}

View File

@ -1,11 +1,10 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -14,6 +13,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -21,7 +21,10 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
{
private DrawableManiaEditorRuleset drawableRuleset;
private DrawableManiaEditorRuleset drawableRuleset = null!;
[Resolved]
private EditorScreenWithTimeline? screenWithTimeline { get; set; }
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -72,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Edit
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue;
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
if (current == null)
continue;
@ -83,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
protected override void Update()
{
base.Update();
if (screenWithTimeline?.TimelineArea.Timeline != null)
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
}
}
}

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
private LabelledSwitchButton specialStyle { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
@ -49,6 +50,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
Precision = 1,
}
},
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
FixedLabelWidth = LABEL_WIDTH,
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.SpecialStyle }
},
healthDrainSlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsDrain,
@ -145,6 +153,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
Beatmap.SpecialStyle = specialStyle.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;

View File

@ -1,49 +0,0 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
public partial class ManiaSetupSection : RulesetSetupSection
{
private LabelledSwitchButton specialStyle;
public ManiaSetupSection()
: base(new ManiaRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.SpecialStyle }
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
specialStyle.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.SpecialStyle = specialStyle.Current.Value;
Beatmap.SaveState();
}
}
}

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:
@ -419,9 +419,10 @@ namespace osu.Game.Rulesets.Mania
return new ManiaFilterCriteria();
}
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection();
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new ManiaDifficultySection(),
];
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) };
public void ApplyToBeatmap(IBeatmap beatmap)
{

View File

@ -0,0 +1,110 @@
// 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 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 override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
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

@ -268,11 +268,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyMaxResult();
else
MissForcefully();
}
// Make sure that the hold note is fully judged by giving the body a judgement.
if (Tail.AllJudged && !Body.AllJudged)
Body.TriggerResult(Tail.IsHit);
// Make sure that the hold note is fully judged by giving the body a judgement.
if (!Body.AllJudged)
Body.TriggerResult(Tail.IsHit);
// Important that this is always called when a result is applied.
endHold();
}
}
public override void MissForcefully()

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

@ -65,11 +65,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength)?.With(d =>
{
if (d == null)
return;
d.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(lightScale);
@ -91,11 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d =>
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d =>
{
if (d == null)
return;
if (d is TextureAnimation animation)
animation.IsPlaying = false;
@ -245,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
// i dunno this looks about right??
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
if (sprite.DrawHeight > 0)
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight));
}
break;

View File

@ -43,11 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d =>
explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength)?.With(d =>
{
if (d == null)
return;
d.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(explosionScale);

View File

@ -28,13 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
string bottomImage = skin.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
?? "mania-stage-bottom";
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
{
if (d == null)
return;
d.Scale = new Vector2(1.6f);
});
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => d.Scale = new Vector2(1.6f));
if (sprite != null)
InternalChild = sprite;

View File

@ -8,9 +8,10 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
@ -56,13 +57,18 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableInt configScrollSpeed = new BindableInt();
private double smoothTimeRange;
private double currentTimeRange;
protected double TargetTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
private ISkinSource currentSkin = null!;
[Resolved]
private GameHost gameHost { get; set; } = null!;
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods)
{
@ -101,9 +107,9 @@ namespace osu.Game.Rulesets.Mania.UI
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea());
}
@ -144,7 +150,9 @@ namespace osu.Game.Rulesets.Mania.UI
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
float scale = lengthToHitPosition / length_to_default_hit_position;
TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
// we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding)
currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime);
TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
}
/// <summary>

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

@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(500, 2000),
},

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
StartTime = 5000,
}
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(2000, 4000),
}

View File

@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(500, 2000),
},

View File

@ -161,9 +161,9 @@ namespace osu.Game.Rulesets.Osu.Tests
pressed = value;
if (value)
OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton));
else
OnReleased(new KeyBindingReleaseEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
OnReleased(new KeyBindingReleaseEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton));
}
}

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void scheduleHit() => AddStep("schedule action", () =>
{
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay);
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton)), delay);
});
}
}

View File

@ -156,6 +156,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
slider = (DrawableSlider)createSlider(repeats: 1);
Add(slider);
slider.HitObject.NodeSamples.Clear();
});
AddStep("change samples", () => slider.HitObject.Samples = new[]

View File

@ -7,7 +7,6 @@ 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.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects.Types;
@ -16,7 +15,6 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Screens.Edit;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
{
@ -48,13 +46,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
},
ring = new RingPiece
{
BorderThickness = 4,

View File

@ -1,8 +1,11 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -16,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected readonly HitCirclePiece CirclePiece;
private readonly HitCircleOverlapMarker marker;
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
public HitCircleSelectionBlueprint(HitCircle circle)
: base(circle)
@ -27,12 +31,32 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
};
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
}
protected override void LoadComplete()
{
base.LoadComplete();
showHitMarkers.BindValueChanged(_ =>
{
if (!showHitMarkers.Value)
DrawableObject.RestoreHitAnimations();
});
}
protected override void Update()
{
base.Update();
CirclePiece.UpdateFrom(HitObject);
marker.UpdateFrom(HitObject);
if (showHitMarkers.Value)
DrawableObject.SuppressHitAnimations();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);

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

@ -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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
@ -14,18 +13,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly Slider slider;
private readonly SliderPosition position;
private readonly HitCircleOverlapMarker marker;
private readonly HitCircleOverlapMarker? marker;
public SliderCircleOverlay(Slider slider, SliderPosition position)
{
this.slider = slider;
this.position = position;
InternalChildren = new Drawable[]
{
marker = new HitCircleOverlapMarker(),
CirclePiece = new HitCirclePiece(),
};
if (position == SliderPosition.Start)
AddInternal(marker = new HitCircleOverlapMarker());
AddInternal(CirclePiece = new HitCirclePiece());
}
protected override void Update()
@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
CirclePiece.UpdateFrom(circle);
marker.UpdateFrom(circle);
marker?.UpdateFrom(circle);
}
public override void Hide()

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

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -54,11 +55,26 @@ 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>();
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
public SliderSelectionBlueprint(Slider slider)
: base(slider)
@ -66,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
InternalChildren = new Drawable[]
{
@ -74,6 +90,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start),
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
};
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
}
protected override void LoadComplete()
@ -90,6 +108,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (editorBeatmap != null)
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true);
showHitMarkers.BindValueChanged(_ =>
{
if (!showHitMarkers.Value)
DrawableObject.RestoreHitAnimations();
});
}
public override bool HandleQuickDeletion()
@ -110,6 +133,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (IsSelected)
BodyPiece.UpdateFrom(HitObject);
if (showHitMarkers.Value)
DrawableObject.SuppressHitAnimations();
}
protected override bool OnHover(HoverEvent e)

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
@ -23,12 +26,32 @@ namespace osu.Game.Rulesets.Osu.Edit
private partial class OsuEditorPlayfield : OsuPlayfield
{
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
protected override GameplayCursorContainer? CreateCursor() => null;
public OsuEditorPlayfield()
{
HitPolicy = new AnyOrderHitPolicy();
}
protected override void LoadComplete()
{
base.LoadComplete();
editorBeatmap.BeatmapReprocessed += onBeatmapReprocessed;
}
private void onBeatmapReprocessed() => ApplyCircleSizeToPlayfieldBorder(editorBeatmap);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorBeatmap.IsNotNull())
editorBeatmap.BeatmapReprocessed -= onBeatmapReprocessed;
}
}
}
}

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.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;
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
@ -99,9 +100,9 @@ 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 EditorToolboxGroup[]
RightToolbox.AddRange(new Drawable[]
{
OsuGridToolboxGroup,
new TransformToolboxGroup
@ -114,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);
@ -192,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
@ -202,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))
@ -214,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)
{
@ -225,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

@ -0,0 +1,42 @@
// 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.Diagnostics;
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuHitObjectInspector : HitObjectInspector
{
protected override void AddInspectorValues()
{
base.AddInspectorValues();
if (EditorBeatmap.SelectedHitObjects.Count > 0)
{
var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!;
Debug.Assert(firstInSelection != null && lastInSelection != null);
var precedingObject = (OsuHitObject?)EditorBeatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() < firstInSelection.StartTime);
var nextObject = (OsuHitObject?)EditorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime > lastInSelection.GetEndTime());
if (precedingObject != null && precedingObject is not Spinner)
{
AddHeader("To previous");
AddValue($"{(firstInSelection.StackedPosition - precedingObject.StackedEndPosition).Length:#,0.##}px");
}
if (nextObject != null && nextObject is not Spinner)
{
AddHeader("To next");
AddValue($"{(nextObject.StackedPosition - lastInSelection.StackedEndPosition).Length:#,0.##}px");
}
}
}
}
}

View File

@ -53,9 +53,11 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Begin()
{
if (objectsInRotation != null)
if (OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
base.Begin();
changeHandler?.BeginChange();
objectsInRotation = selectedMovableObjects.ToArray();
@ -68,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Update(float rotation, Vector2? origin = null)
{
if (objectsInRotation == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
@ -91,11 +93,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Commit()
{
if (objectsInRotation == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
base.Commit();
objectsInRotation = null;
originalPositions = null;
originalPathControlPointPositions = null;

View File

@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Begin()
{
if (objectsInScale != null)
if (OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
base.Begin();
changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
@ -86,10 +88,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (objectsInScale == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
@ -117,11 +119,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Commit()
{
if (objectsInScale == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
base.Commit();
objectsInScale = null;
OriginalSurroundingQuad = null;
defaultOrigin = null;

View File

@ -0,0 +1,150 @@
// 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.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Osu.Edit.Setup
{
public partial class OsuDifficultySection : SetupSection
{
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
private LabelledSliderBar<float> stackLeniency { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
circleSizeSlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsCs,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
healthDrainSlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
approachRateSlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsAr,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
overallDifficultySlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
baseVelocitySlider = new LabelledSliderBar<double>
{
Label = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
}
},
tickRateSlider = new LabelledSliderBar<double>
{
Label = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
}
},
stackLeniency = new LabelledSliderBar<float>
{
Label = "Stack Leniency",
FixedLabelWidth = LABEL_WIDTH,
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.StackLeniency)
{
Default = 0.7f,
MinValue = 0,
MaxValue = 1,
Precision = 0.1f
}
},
};
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.StackLeniency = stackLeniency.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}

View File

@ -1,56 +0,0 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Osu.Edit.Setup
{
public partial class OsuSetupSection : RulesetSetupSection
{
private LabelledSliderBar<float> stackLeniency;
public OsuSetupSection()
: base(new OsuRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
{
stackLeniency = new LabelledSliderBar<float>
{
Label = "Stack Leniency",
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.StackLeniency)
{
Default = 0.7f,
MinValue = 0,
MaxValue = 1,
Precision = 0.1f
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
stackLeniency.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.StackLeniency = stackLeniency.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}

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

@ -77,13 +77,15 @@ namespace osu.Game.Rulesets.Osu.Edit
{
case GlobalAction.EditorToggleRotateControl:
{
rotateButton.TriggerClick();
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
rotateButton.TriggerClick();
return true;
}
case GlobalAction.EditorToggleScaleControl:
{
scaleButton.TriggerClick();
if (!ScaleHandler.OperationInProgress.Value || scaleButton.Selected.Value)
scaleButton.TriggerClick();
return true;
}
}

View File

@ -6,7 +6,9 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick);
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -319,5 +321,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
}
}
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
internal void SuppressHitAnimations()
{
UpdateState(ArmedState.Idle);
UpdateComboColour();
// This method is called every frame in editor contexts, thus the lack of need for transforms.
if (Time.Current >= HitStateUpdateTime)
{
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
AccentColour.Value = Color4.White;
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
}
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit, force: true);
UpdateComboColour();
}
#endregion
}
}

View File

@ -48,10 +48,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
Scale = new Vector2(osuObject.HitObject.Scale);
switch (osuObject)
{
case DrawableSlider slider:
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
break;
default:
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
break;
}
positionTransferred = true;
Scale = new Vector2(osuObject.HitObject.Scale);
}
}

View File

@ -370,5 +370,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private partial class DefaultSliderBody : PlaySliderBody
{
}
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
internal void SuppressHitAnimations()
{
UpdateState(ArmedState.Idle);
HeadCircle.SuppressHitAnimations();
TailCircle.SuppressHitAnimations();
}
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit);
HeadCircle.RestoreHitAnimations();
TailCircle.RestoreHitAnimations();
}
#endregion
}
}

View File

@ -8,10 +8,12 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -125,5 +127,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Slider != null)
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
}
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
internal void SuppressHitAnimations()
{
UpdateState(ArmedState.Idle);
UpdateComboColour();
// This method is called every frame in editor contexts, thus the lack of need for transforms.
if (Time.Current >= HitStateUpdateTime)
{
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
AccentColour.Value = Color4.White;
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
}
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit);
UpdateComboColour();
}
#endregion
}
}

View File

@ -252,6 +252,8 @@ namespace osu.Game.Rulesets.Osu.Objects
protected void UpdateNestedSamples()
{
this.PopulateNodeSamples();
// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
HitSampleInfo tickSample = (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault())?.With("slidertick");

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();
}
@ -337,7 +336,11 @@ namespace osu.Game.Rulesets.Osu
};
}
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new OsuDifficultySection(),
new ColoursSection(),
];
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/>

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,10 +8,12 @@ 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;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
@ -27,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
[Cached]
public partial class OsuPlayfield : Playfield
{
private readonly Container borderContainer;
private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
@ -45,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()
@ -54,7 +59,11 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
borderContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
},
Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
@ -151,6 +160,14 @@ namespace osu.Game.Rulesets.Osu.UI
RegisterPool<Spinner, DrawableSpinner>(2, 20);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
if (beatmap != null)
ApplyCircleSizeToPlayfieldBorder(beatmap);
}
protected void ApplyCircleSizeToPlayfieldBorder(IBeatmap beatmap)
{
borderContainer.Padding = new MarginPadding(OsuHitObject.OBJECT_RADIUS * -LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Difficulty.CircleSize, true));
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);

View File

@ -116,7 +116,9 @@ namespace osu.Game.Rulesets.Osu.UI
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
// When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score.
// To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events.
Schedule(() => ResumeRequested?.Invoke());
return true;
}

View File

@ -85,6 +85,42 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<Swell>(0, HitResult.IgnoreMiss);
}
[Test]
public void TestAlternatingIsRequired()
{
const double hit_time = 1000;
Swell swell = new Swell
{
StartTime = hit_time,
Duration = 1000,
RequiredHits = 10
};
List<ReplayFrame> frames = new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2001),
};
for (int i = 0; i < swell.RequiredHits; i++)
{
double frameTime = 1000 + i * 50;
frames.Add(new TaikoReplayFrame(frameTime, TaikoAction.LeftCentre));
frames.Add(new TaikoReplayFrame(frameTime + 10));
}
PerformTest(frames, CreateBeatmap(swell));
AssertJudgementCount(11);
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
for (int i = 1; i < swell.RequiredHits; i++)
AssertResult<SwellTick>(i, HitResult.IgnoreMiss);
AssertResult<Swell>(0, HitResult.IgnoreMiss);
}
[Test]
public void TestHitNoneSwell()
{

View File

@ -0,0 +1,46 @@
// 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.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModRelax : TaikoModTestScene
{
[Test]
public void TestRelax()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit { StartTime = 0, Type = HitType.Centre, },
new Hit { StartTime = 250, Type = HitType.Rim, },
new DrumRoll { StartTime = 500, Duration = 500, },
new Swell { StartTime = 1250, Duration = 500 },
}
};
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var replay = new TaikoAutoGenerator(beatmap).Generate();
foreach (var frame in replay.Frames.OfType<TaikoReplayFrame>().Where(r => r.Actions.Any()))
frame.Actions = [TaikoAction.LeftCentre];
CreateModTest(new ModTestData
{
Mod = new TaikoModRelax(),
Beatmap = beatmap,
ReplayFrames = replay.Frames,
Autoplay = false,
PassCondition = () => Player.ScoreProcessor.HasCompleted.Value && Player.ScoreProcessor.Accuracy.Value == 1,
});
}
}
}

View File

@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(100, 1600),
},

View File

@ -1,93 +0,0 @@
// 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.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
public class Peaks : Skill
{
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
private const double colour_skill_multiplier = 0.375 * final_multiplier;
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
private const double final_multiplier = 0.0625;
private readonly Rhythm rhythm;
private readonly Colour colour;
private readonly Stamina stamina;
public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier;
public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier;
public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier;
public Peaks(Mod[] mods)
: base(mods)
{
rhythm = new Rhythm(mods);
colour = new Colour(mods);
stamina = new Stamina(mods);
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
public override void Process(DifficultyHitObject current)
{
rhythm.Process(current);
colour.Process(current);
stamina.Process(current);
}
/// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
public override double DifficultyValue()
{
List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderDescending())
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
}
}

View File

@ -23,6 +23,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
private const double difficulty_multiplier = 1.35;
private const double final_multiplier = 0.0625;
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
private const double colour_skill_multiplier = 0.375 * final_multiplier;
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
public override int Version => 20221107;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
return new Skill[]
{
new Peaks(mods)
new Rhythm(mods),
new Colour(mods),
new Stamina(mods)
};
}
@ -72,13 +79,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
var combined = (Peaks)skills[0];
Colour colour = (Colour)skills.First(x => x is Colour);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
double colourRating = combined.ColourDifficultyValue * difficulty_multiplier;
double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier;
double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier;
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
double starRating = rescale(combinedRating * 1.4);
HitWindows hitWindows = new TaikoHitWindows();
@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return 10.43 * Math.Log(sr / 8 + 1);
}
/// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
{
List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderDescending())
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
}
}

View File

@ -0,0 +1,105 @@
// 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.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Taiko.Edit.Setup
{
public partial class TaikoDifficultySection : SetupSection
{
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
healthDrainSlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
overallDifficultySlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
baseVelocitySlider = new LabelledSliderBar<double>
{
Label = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
}
},
tickRateSlider = new LabelledSliderBar<double>
{
Label = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
}
},
};
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}

View File

@ -5,13 +5,34 @@ using System;
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModRelax : ModRelax
public class TaikoModRelax : ModRelax, IApplicableToDrawableHitObject
{
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
public override LocalisableString Description => @"No need to remember which key is correct anymore!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
var allActions = Enum.GetValues<TaikoAction>();
drawable.HitObjectApplied += dho =>
{
switch (dho)
{
case DrawableHit hit:
hit.HitActions = allActions;
break;
case DrawableSwell swell:
swell.MustAlternate = false;
break;
}
};
}
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// <summary>
/// A list of keys which can result in hits for this HitObject.
/// </summary>
public TaikoAction[] HitActions { get; private set; }
public TaikoAction[] HitActions { get; internal set; }
/// <summary>
/// The action that caused this <see cref="DrawableHit"/> to be hit.

View File

@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool DisplayResult => false;
/// <summary>
/// Whether the player must alternate centre and rim hits.
/// </summary>
public bool MustAlternate { get; internal set; } = true;
public DrawableSwell()
: this(null)
{
@ -292,7 +297,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
// Ensure alternating centre and rim hits
if (lastWasCentre == isCentre)
if (lastWasCentre == isCentre && MustAlternate)
return false;
// If we've already successfully judged a tick this frame, do not judge more.

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;
@ -36,6 +35,8 @@ using osu.Game.Rulesets.Configuration;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.Edit.Setup;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Taiko
{
@ -79,43 +80,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();
}
@ -189,6 +190,11 @@ namespace osu.Game.Rulesets.Taiko
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new TaikoDifficultySection(),
];
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);

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

@ -157,8 +157,9 @@ namespace osu.Game.Tests.Database
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
[Test]
public void TestScoreUpgradeFailed()
[TestCase(30000002)]
[TestCase(30000013)]
public void TestScoreUpgradeFailed(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All<RulesetInfo>().First(),
})
{
TotalScoreVersion = 30000002,
TotalScoreVersion = scoreVersion,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
}
[Test]

View File

@ -168,12 +168,12 @@ namespace osu.Game.Tests.Database
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
// should only contain the modified beatmap (others purged).
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
realm.Run(r => r.Refresh());
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
@ -479,6 +479,7 @@ namespace osu.Game.Tests.Database
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
@ -496,7 +497,7 @@ namespace osu.Game.Tests.Database
Debug.Assert(importAfterUpdate != null);
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded).Within(TimeSpan.FromSeconds(1)));
});
}

View File

@ -71,6 +71,35 @@ namespace osu.Game.Tests.Database
}
}
[Test]
public void TestSubscriptionInitialChangeSetNull()
{
ChangeSet? firstChanges = null;
int receivedChangesCount = 0;
RunTestWithRealm((realm, _) =>
{
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
realm.Run(r => r.Refresh());
Assert.That(receivedChangesCount, Is.EqualTo(1));
Assert.That(firstChanges, Is.Null);
registration.Dispose();
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (receivedChangesCount == 0)
firstChanges = changes;
receivedChangesCount++;
}
}
[Test]
public void TestSubscriptionWithAsyncWrite()
{

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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
@ -29,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var beatmap = new Beatmap<HitObject>
{
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(0, 649)
}
@ -52,7 +51,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_200 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(100, 751)
}
@ -75,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(200, 850)
}
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1200 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(1398, 2300)
}
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 1500 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(0, 652)
}
@ -145,7 +144,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1_297 },
new HitCircle { StartTime = 1_298 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(200, 850)
}
@ -168,7 +167,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_300 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(200, 850)
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 40_000 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(10_000, 21_000)
}

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

Some files were not shown because too many files have changed in this diff Show More