1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:33:22 +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: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest } - { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded'] threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60 timeout-minutes: 120
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

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

2
.gitignore vendored
View File

@ -265,6 +265,8 @@ __pycache__/
.idea/**/usage.statistics.xml .idea/**/usage.statistics.xml
.idea/**/dictionaries .idea/**/dictionaries
.idea/**/shelf .idea/**/shelf
.idea/*/.idea/projectSettingsUpdater.xml
.idea/*/.idea/encodings.xml
# Generated files # Generated files
.idea/**/contentModel.xml .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.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.

View File

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

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Security.Principal;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -21,48 +20,14 @@ namespace osu.Desktop.Security
[Resolved] [Resolved]
private INotificationOverlay notifications { get; set; } = null!; private INotificationOverlay notifications { get; set; } = null!;
private bool elevated;
[BackgroundDependencyLoader]
private void load()
{
elevated = checkElevated();
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (elevated) if (Environment.IsPrivilegedProcess)
notifications.Post(new ElevatedPrivilegesNotification()); 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 private partial class ElevatedPrivilegesNotification : SimpleNotification
{ {
public override bool IsImportant => true; public override bool IsImportant => true;

View File

@ -24,7 +24,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" /> <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="System.IO.Packaging" Version="8.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" /> <PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
</ItemGroup> </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, StartTime = 5000,
} }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(2000, 4000), 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, LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration. // this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1 : ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity, TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1 SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield(); }.Yield();

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -29,6 +28,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -62,43 +62,43 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new CatchModNightcore(); yield return new CatchModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime(); yield return new CatchModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new CatchModPerfect(); yield return new CatchModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new CatchModSuddenDeath(); yield return new CatchModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new CatchModCinema(); yield return new CatchModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new CatchModAutoplay(); yield return new CatchModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new CatchModEasy(); yield return new CatchModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new CatchModFlashlight(); yield return new CatchModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new CatchModHalfTime(); yield return new CatchModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new CatchModHardRock(); yield return new CatchModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new CatchModHidden(); yield return new CatchModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new CatchModNoFail(); yield return new CatchModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new CatchModRelax(); yield return new CatchModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }
@ -223,6 +223,12 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new DifficultySection(),
new ColoursSection(),
];
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) 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); value *= Math.Pow(accuracy(), 5.5);
if (score.Mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
value *= 0.90; value *= Math.Max(0.90, 1.0 - 0.02 * numMiss);
return new CatchPerformanceAttributes return new CatchPerformanceAttributes
{ {

View File

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

View File

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

View File

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

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
base.Update(); 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( SetCatcherPosition(
replayState?.CatcherX ?? replayState?.CatcherX ??

View File

@ -1,13 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; 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.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor 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(); var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction); 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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public partial class TestHitObjectComposer : HitObjectComposer public partial class TestHitObjectComposer : HitObjectComposer
{ {
public override Playfield Playfield { get; } public override Playfield Playfield { get; }
public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException();
public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>(); public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>();
public override bool CursorInPlacementArea => false; 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) 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("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("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("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.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) private void setScrollStep(ScrollingDirection direction)

View File

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

View File

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

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("Hold key", () => AddStep("Hold key", () =>
{ {
clock.CurrentTime = 0; 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); AddStep("progress time", () => clock.CurrentTime = 500);
AddAssert("head is visible", () => note.Head.Alpha == 1); 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)) AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => !j.Type.IsHit())); .All(j => !j.Type.IsHit()));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit())); .All(j => j.Type.IsHit()));
} }
[Test] [Test]

View File

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

View File

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

View File

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

View File

@ -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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Screens.Edit;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@ -18,10 +18,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote> public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
{ {
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; } = null!;
private EditNotePiece head; [Resolved]
private EditNotePiece tail; 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) public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold) : base(hold)
@ -33,8 +42,43 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
head = new EditNotePiece { RelativeSizeAxes = Axes.X }, head = new EditHoldNoteEndPiece
tail = new EditNotePiece { RelativeSizeAxes = Axes.X }, {
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 new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
public BindableBool ShowSpeedChanges { get; } = new BindableBool(); public BindableBool ShowSpeedChanges { get; } = new BindableBool();
public double? TimelineTimeRange { get; set; }
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods) public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
@ -38,5 +41,11 @@ namespace osu.Game.Rulesets.Mania.Edit
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = Vector2.One 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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; 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.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -21,7 +21,10 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject> public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
{ {
private DrawableManiaEditorRuleset drawableRuleset; private DrawableManiaEditorRuleset drawableRuleset = null!;
[Resolved]
private EditorScreenWithTimeline? screenWithTimeline { get; set; }
public ManiaHitObjectComposer(Ruleset ruleset) public ManiaHitObjectComposer(Ruleset ruleset)
: base(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)) if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue; 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) if (current == null)
continue; continue;
@ -83,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); 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; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!; private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
private LabelledSwitchButton specialStyle { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!; private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!; private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!; private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
@ -49,6 +50,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
Precision = 1, 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> healthDrainSlider = new LabelledSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsDrain, 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. // 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. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
Beatmap.SpecialStyle = specialStyle.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -89,79 +88,79 @@ namespace osu.Game.Rulesets.Mania
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore(); yield return new ManiaModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime(); yield return new ManiaModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new ManiaModPerfect(); yield return new ManiaModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new ManiaModSuddenDeath(); yield return new ManiaModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new ManiaModCinema(); yield return new ManiaModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new ManiaModAutoplay(); yield return new ManiaModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new ManiaModEasy(); yield return new ManiaModEasy();
if (mods.HasFlagFast(LegacyMods.FadeIn)) if (mods.HasFlag(LegacyMods.FadeIn))
yield return new ManiaModFadeIn(); yield return new ManiaModFadeIn();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new ManiaModFlashlight(); yield return new ManiaModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new ManiaModHalfTime(); yield return new ManiaModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new ManiaModHardRock(); yield return new ManiaModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new ManiaModHidden(); yield return new ManiaModHidden();
if (mods.HasFlagFast(LegacyMods.Key1)) if (mods.HasFlag(LegacyMods.Key1))
yield return new ManiaModKey1(); yield return new ManiaModKey1();
if (mods.HasFlagFast(LegacyMods.Key2)) if (mods.HasFlag(LegacyMods.Key2))
yield return new ManiaModKey2(); yield return new ManiaModKey2();
if (mods.HasFlagFast(LegacyMods.Key3)) if (mods.HasFlag(LegacyMods.Key3))
yield return new ManiaModKey3(); yield return new ManiaModKey3();
if (mods.HasFlagFast(LegacyMods.Key4)) if (mods.HasFlag(LegacyMods.Key4))
yield return new ManiaModKey4(); yield return new ManiaModKey4();
if (mods.HasFlagFast(LegacyMods.Key5)) if (mods.HasFlag(LegacyMods.Key5))
yield return new ManiaModKey5(); yield return new ManiaModKey5();
if (mods.HasFlagFast(LegacyMods.Key6)) if (mods.HasFlag(LegacyMods.Key6))
yield return new ManiaModKey6(); yield return new ManiaModKey6();
if (mods.HasFlagFast(LegacyMods.Key7)) if (mods.HasFlag(LegacyMods.Key7))
yield return new ManiaModKey7(); yield return new ManiaModKey7();
if (mods.HasFlagFast(LegacyMods.Key8)) if (mods.HasFlag(LegacyMods.Key8))
yield return new ManiaModKey8(); yield return new ManiaModKey8();
if (mods.HasFlagFast(LegacyMods.Key9)) if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9(); yield return new ManiaModKey9();
if (mods.HasFlagFast(LegacyMods.KeyCoop)) if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages(); yield return new ManiaModDualStages();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail(); yield return new ManiaModNoFail();
if (mods.HasFlagFast(LegacyMods.Random)) if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom(); yield return new ManiaModRandom();
if (mods.HasFlagFast(LegacyMods.Mirror)) if (mods.HasFlag(LegacyMods.Mirror))
yield return new ManiaModMirror(); yield return new ManiaModMirror();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }
@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModEasy(), new ManiaModEasy(),
new ManiaModNoFail(), new ManiaModNoFail(),
new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()), new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()),
new ManiaModNoRelease(),
}; };
case ModType.DifficultyIncrease: case ModType.DifficultyIncrease:
@ -419,9 +419,10 @@ namespace osu.Game.Rulesets.Mania
return new ManiaFilterCriteria(); return new ManiaFilterCriteria();
} }
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection(); public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection(); new ManiaDifficultySection(),
];
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null) public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); => 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 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) 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(); ApplyMaxResult();
else else
MissForcefully(); MissForcefully();
}
// Make sure that the hold note is fully judged by giving the body a judgement. // Make sure that the hold note is fully judged by giving the body a judgement.
if (Tail.AllJudged && !Body.AllJudged) if (!Body.AllJudged)
Body.TriggerResult(Tail.IsHit); Body.TriggerResult(Tail.IsHit);
// Important that this is always called when a result is applied.
endHold();
}
} }
public override void MissForcefully() public override void MissForcefully()

View File

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

View File

@ -65,11 +65,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); 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.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive; d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(lightScale); d.Scale = new Vector2(lightScale);
@ -91,11 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting); 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) if (d is TextureAnimation animation)
animation.IsPlaying = false; animation.IsPlaying = false;
@ -245,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
// i dunno this looks about right?? // 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. // 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) 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; break;

View File

@ -43,11 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); 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.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive; d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(explosionScale); 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 string bottomImage = skin.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
?? "mania-stage-bottom"; ?? "mania-stage-bottom";
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => d.Scale = new Vector2(1.6f));
{
if (d == null)
return;
d.Scale = new Vector2(1.6f);
});
if (sprite != null) if (sprite != null)
InternalChild = sprite; InternalChild = sprite;

View File

@ -8,9 +8,10 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers; 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 Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableInt configScrollSpeed = new BindableInt(); private readonly BindableInt configScrollSpeed = new BindableInt();
private double smoothTimeRange;
private double currentTimeRange;
protected double TargetTimeRange;
// Stores the current speed adjustment active in gameplay. // Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0); private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
private ISkinSource currentSkin = null!; private ISkinSource currentSkin = null!;
[Resolved]
private GameHost gameHost { get; set; } = null!;
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
@ -101,9 +107,9 @@ namespace osu.Game.Rulesets.Mania.UI
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed); 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()); 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. // 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; 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> /// <summary>

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -160,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return grid switch return grid switch
{ {
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
_ => Vector2.Zero _ => Vector2.Zero
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -161,9 +161,9 @@ namespace osu.Game.Rulesets.Osu.Tests
pressed = value; pressed = value;
if (value) if (value)
OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton)); OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton));
else 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", () => private void scheduleHit() => AddStep("schedule action", () =>
{ {
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current; 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); slider = (DrawableSlider)createSlider(repeats: 1);
Add(slider); Add(slider);
slider.HitObject.NodeSamples.Clear();
}); });
AddStep("change samples", () => slider.HitObject.Samples = new[] AddStep("change samples", () => slider.HitObject.Samples = new[]

View File

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

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -14,18 +13,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly Slider slider; private readonly Slider slider;
private readonly SliderPosition position; private readonly SliderPosition position;
private readonly HitCircleOverlapMarker marker; private readonly HitCircleOverlapMarker? marker;
public SliderCircleOverlay(Slider slider, SliderPosition position) public SliderCircleOverlay(Slider slider, SliderPosition position)
{ {
this.slider = slider; this.slider = slider;
this.position = position; this.position = position;
InternalChildren = new Drawable[] if (position == SliderPosition.Start)
{ AddInternal(marker = new HitCircleOverlapMarker());
marker = new HitCircleOverlapMarker(),
CirclePiece = new HitCirclePiece(), AddInternal(CirclePiece = new HitCirclePiece());
};
} }
protected override void Update() 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; var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
CirclePiece.UpdateFrom(circle); CirclePiece.UpdateFrom(circle);
marker.UpdateFrom(circle); marker?.UpdateFrom(circle);
} }
public override void Hide() public override void Hide()

View File

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

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -54,11 +55,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; } private BindableBeatDivisor beatDivisor { get; set; }
public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad; public override Quad SelectionQuad
{
get
{
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
if (ControlPointVisualiser != null)
{
foreach (var piece in ControlPointVisualiser.Pieces)
result = RectangleF.Union(result, piece.ScreenSpaceDrawQuad.AABBFloat);
}
return result;
}
}
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>(); private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly IBindable<int> pathVersion = new Bindable<int>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>(); private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
public SliderSelectionBlueprint(Slider slider) public SliderSelectionBlueprint(Slider slider)
: base(slider) : base(slider)
@ -66,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -74,6 +90,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start),
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
}; };
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -90,6 +108,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (editorBeatmap != null) if (editorBeatmap != null)
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects); selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true); selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true);
showHitMarkers.BindValueChanged(_ =>
{
if (!showHitMarkers.Value)
DrawableObject.RestoreHitAnimations();
});
} }
public override bool HandleQuickDeletion() public override bool HandleQuickDeletion()
@ -110,6 +133,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (IsSelected) if (IsSelected)
BodyPiece.UpdateFrom(HitObject); BodyPiece.UpdateFrom(HitObject);
if (showHitMarkers.Value)
DrawableObject.SuppressHitAnimations();
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

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

View File

@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>(); private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons() protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons() => base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.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 // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
updatePositionSnapGrid(); OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true);
RightToolbox.AddRange(new EditorToolboxGroup[] RightToolbox.AddRange(new Drawable[]
{ {
OsuGridToolboxGroup, OsuGridToolboxGroup,
new TransformToolboxGroup new TransformToolboxGroup
@ -114,18 +115,45 @@ namespace osu.Game.Rulesets.Osu.Edit
); );
} }
private void updatePositionSnapGrid() private void updatePositionSnapGrid(ValueChangedEvent<PositionSnapGridType> obj)
{ {
if (positionSnapGrid != null) if (positionSnapGrid != null)
LayerBelowRuleset.Remove(positionSnapGrid, true); LayerBelowRuleset.Remove(positionSnapGrid, true);
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); switch (obj.NewValue)
{
case PositionSnapGridType.Square:
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = rectangularPositionSnapGrid; positionSnapGrid = rectangularPositionSnapGrid;
break;
case PositionSnapGridType.Triangle:
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = triangularPositionSnapGrid;
break;
case PositionSnapGridType.Circle:
var circularPositionSnapGrid = new CircularPositionSnapGrid();
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
positionSnapGrid = circularPositionSnapGrid;
break;
default:
throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type.");
}
// Bind the start position to the toolbox sliders.
positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
positionSnapGrid.RelativeSizeAxes = Axes.Both; positionSnapGrid.RelativeSizeAxes = Axes.Both;
LayerBelowRuleset.Add(positionSnapGrid); LayerBelowRuleset.Add(positionSnapGrid);
@ -192,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{ {
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{ {
// In the case of snapping to nearby objects, a time value is not provided. // In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
@ -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. // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same. // the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
@ -214,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.RelativeGrids)) if (snapType.HasFlag(SnapType.RelativeGrids))
{ {
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
@ -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) 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() public override void Begin()
{ {
if (objectsInRotation != null) if (OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
base.Begin();
changeHandler?.BeginChange(); changeHandler?.BeginChange();
objectsInRotation = selectedMovableObjects.ToArray(); objectsInRotation = selectedMovableObjects.ToArray();
@ -68,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Update(float rotation, Vector2? origin = null) 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!"); 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; Vector2 actualOrigin = origin ?? defaultOrigin.Value;
@ -91,11 +93,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Commit() public override void Commit()
{ {
if (objectsInRotation == null) if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange(); changeHandler?.EndChange();
base.Commit();
objectsInRotation = null; objectsInRotation = null;
originalPositions = null; originalPositions = null;
originalPathControlPointPositions = null; originalPathControlPointPositions = null;

View File

@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Begin() public override void Begin()
{ {
if (objectsInScale != null) if (OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!"); throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
base.Begin();
changeHandler?.BeginChange(); changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); 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) 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!"); 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; Vector2 actualOrigin = origin ?? defaultOrigin.Value;
@ -117,11 +119,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Commit() public override void Commit()
{ {
if (objectsInScale == null) if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange(); changeHandler?.EndChange();
base.Commit();
objectsInScale = null; objectsInScale = null;
OriginalSurroundingQuad = null; OriginalSurroundingQuad = null;
defaultOrigin = 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() public SliderCompositionTool()
: base(nameof(Slider)) : base(nameof(Slider))
{ {
TooltipText = """
Left click for new point.
Left click twice or S key for new segment.
Tab, Shift-Tab, or Alt-1~4 to change current segment type.
Right click to finish.
Click and drag for drawing mode.
""";
} }
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);

View File

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

View File

@ -6,7 +6,9 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default; 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) }; 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) 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.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables 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) if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{ {
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); switch (osuObject)
Scale = new Vector2(osuObject.HitObject.Scale); {
case DrawableSlider slider:
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
break;
default:
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
break;
}
positionTransferred = true; 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 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.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -125,5 +127,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Slider != null) if (Slider != null)
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0); 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() protected void UpdateNestedSamples()
{ {
this.PopulateNodeSamples();
// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) // 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"); HitSampleInfo tickSample = (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault())?.With("slidertick");

View File

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

View File

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

View File

@ -8,10 +8,12 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -27,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
[Cached] [Cached]
public partial class OsuPlayfield : Playfield public partial class OsuPlayfield : Playfield
{ {
private readonly Container borderContainer;
private readonly PlayfieldBorder playfieldBorder; private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles; private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
@ -45,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer(); protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer();
public override Quad SkinnableComponentScreenSpaceDrawQuad => playfieldBorder.ScreenSpaceDrawQuad;
private readonly Container judgementAboveHitObjectLayer; private readonly Container judgementAboveHitObjectLayer;
public OsuPlayfield() public OsuPlayfield()
@ -54,7 +59,11 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[] 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 }, Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { 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<Spinner, DrawableSpinner>(2, 20);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200); RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(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); 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); 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; return true;
} }

View File

@ -85,6 +85,42 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<Swell>(0, HitResult.IgnoreMiss); 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] [Test]
public void TestHitNoneSwell() 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, Autoplay = false,
Beatmap = new Beatmap Beatmap = new Beatmap
{ {
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(100, 1600), 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 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 override int Version => 20221107;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
return new Skill[] 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) if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods }; 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 colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier; double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
double staminaRating = combined.StaminaDifficultyValue * 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); double starRating = rescale(combinedRating * 1.4);
HitWindows hitWindows = new TaikoHitWindows(); HitWindows hitWindows = new TaikoHitWindows();
@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return 10.43 * Math.Log(sr / 8 + 1); 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 System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Mods 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 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> /// <summary>
/// A list of keys which can result in hits for this HitObject. /// A list of keys which can result in hits for this HitObject.
/// </summary> /// </summary>
public TaikoAction[] HitActions { get; private set; } public TaikoAction[] HitActions { get; internal set; }
/// <summary> /// <summary>
/// The action that caused this <see cref="DrawableHit"/> to be hit. /// 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; 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() public DrawableSwell()
: this(null) : this(null)
{ {
@ -292,7 +297,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre; bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
// Ensure alternating centre and rim hits // Ensure alternating centre and rim hits
if (lastWasCentre == isCentre) if (lastWasCentre == isCentre && MustAlternate)
return false; return false;
// If we've already successfully judged a tick this frame, do not judge more. // If we've already successfully judged a tick this frame, do not judge more.

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -36,6 +35,8 @@ using osu.Game.Rulesets.Configuration;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Configuration; using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.Edit.Setup;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Taiko namespace osu.Game.Rulesets.Taiko
{ {
@ -79,43 +80,43 @@ namespace osu.Game.Rulesets.Taiko
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new TaikoModNightcore(); yield return new TaikoModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new TaikoModDoubleTime(); yield return new TaikoModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new TaikoModPerfect(); yield return new TaikoModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new TaikoModSuddenDeath(); yield return new TaikoModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new TaikoModCinema(); yield return new TaikoModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new TaikoModAutoplay(); yield return new TaikoModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new TaikoModEasy(); yield return new TaikoModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new TaikoModFlashlight(); yield return new TaikoModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new TaikoModHalfTime(); yield return new TaikoModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new TaikoModHardRock(); yield return new TaikoModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new TaikoModHidden(); yield return new TaikoModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new TaikoModNoFail(); yield return new TaikoModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new TaikoModRelax(); yield return new TaikoModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }
@ -189,6 +190,11 @@ namespace osu.Game.Rulesets.Taiko
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new TaikoDifficultySection(),
];
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier(); public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); 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-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
// The control point at the end time of the slider should be applied // The fourth object is a slider.
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); // `Samples` of a slider are presumed to control the volume of sounds that last the entire duration of the slider
// (such as ticks, slider slide sounds, etc.)
// Thus, the point of query of control points used for `Samples` is just beyond the start time of the slider.
Assert.AreEqual("Gameplay/soft-hitnormal11", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
// That said, the `NodeSamples` of the slider are responsible for the sounds of the slider's head / tail / repeats / large ticks etc.
// Therefore, they should be read at the time instant correspondent to the given node.
// This means that the tail should use bank 8 rather than 11.
Assert.AreEqual("Gameplay/soft-hitnormal11", ((ConvertSlider)hitObjects[4]).NodeSamples[0][0].LookupNames.First());
Assert.AreEqual("Gameplay/soft-hitnormal8", ((ConvertSlider)hitObjects[4]).NodeSamples[1][0].LookupNames.First());
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];

View File

@ -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); AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
} }
[Test] [TestCase(30000002)]
public void TestScoreUpgradeFailed() [TestCase(30000013)]
public void TestScoreUpgradeFailed(int scoreVersion)
{ {
ScoreInfo scoreInfo = null!; ScoreInfo scoreInfo = null!;
@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All<RulesetInfo>().First(), Ruleset = r.All<RulesetInfo>().First(),
}) })
{ {
TotalScoreVersion = 30000002, TotalScoreVersion = scoreVersion,
IsLegacyScore = true, 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); 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] [Test]

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -29,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
{ {
var beatmap = new Beatmap<HitObject> var beatmap = new Beatmap<HitObject>
{ {
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(0, 649) new BreakPeriod(0, 649)
} }
@ -52,7 +51,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_200 } new HitCircle { StartTime = 1_200 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(100, 751) new BreakPeriod(100, 751)
} }
@ -75,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 } new HitCircle { StartTime = 1_298 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(200, 850) new BreakPeriod(200, 850)
} }
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1200 } new HitCircle { StartTime = 1200 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(1398, 2300) new BreakPeriod(1398, 2300)
} }
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1100 }, new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 1500 } new HitCircle { StartTime = 1500 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(0, 652) new BreakPeriod(0, 652)
} }
@ -145,7 +144,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1_297 }, new HitCircle { StartTime = 1_297 },
new HitCircle { StartTime = 1_298 } new HitCircle { StartTime = 1_298 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(200, 850) new BreakPeriod(200, 850)
} }
@ -168,7 +167,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_300 } new HitCircle { StartTime = 1_300 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(200, 850) new BreakPeriod(200, 850)
} }

View File

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