1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-16 17:43:12 +08:00

Merge branch 'master' into move-already-placed-objects-when-adjusting-offset-bpm

This commit is contained in:
Dean Herbert 2024-08-19 14:30:09 +09:00 committed by GitHub
commit f176383dd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
640 changed files with 22436 additions and 5630 deletions

View File

@ -21,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2024.517.0", "version": "2024.802.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -196,6 +196,9 @@ csharp_style_prefer_switch_expression = false:none
csharp_style_namespace_declarations = block_scoped:warning csharp_style_namespace_declarations = block_scoped:warning
#Style - C# 12 features
csharp_style_prefer_primary_constructors = false
[*.{yaml,yml}] [*.{yaml,yml}]
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space

View File

@ -64,10 +64,11 @@ jobs:
matrix: matrix:
os: os:
- { prettyname: Windows, fullname: windows-latest } - { prettyname: Windows, fullname: windows-latest }
- { prettyname: macOS, fullname: macos-latest } # macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
# - { 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
@ -120,9 +121,7 @@ jobs:
build-only-ios: build-only-ios:
name: Build only (iOS) name: Build only (iOS)
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3. runs-on: macos-latest
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
runs-on: macos-13
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
@ -136,8 +135,5 @@ jobs:
- name: Install .NET Workloads - name: Install .NET Workloads
run: dotnet workload install maui-ios run: dotnet workload install maui-ios
- name: Select Xcode 15.2
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
- name: Build - name: Build
run: dotnet build -c Debug osu.iOS run: dotnet build -c Debug osu.iOS

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

@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.

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.809.2" />
</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

@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(-100, 100); AddMouseMoveStep(-100, 100);
addVertexCheckStep(3, 1, times[0], positions[0]); addVertexCheckStep(3, 1, times[0], positions[0]);
addDragEndStep();
} }
[Test] [Test]
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(times[2] - 50, positions[2] - 50); AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50); addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50); addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
addDragEndStep();
} }
[Test] [Test]
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400); AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault); AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
addDragEndStep();
} }
[Test] [Test]
@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("scroll playfield", () => manualClock.CurrentTime += 200); AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100); AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100); addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
addDragEndStep();
} }
[Test] [Test]
@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150); addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150); addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 200); addAddVertexSteps(160, 200);
addVertexCheckStep(4, 1, times[0], positions[0]); addVertexCheckStep(4, 1, 160, 200);
addAddVertexSteps(750, 180); addAddVertexSteps(750, 180);
addVertexCheckStep(5, 4, 750, 180); addVertexCheckStep(5, 4, 800, 160);
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3)); AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
} }
[Test] [Test]
public void TestDeleteVertex() public void TestDeleteVertex()
{ {
double[] times = { 100, 300, 500 }; double[] times = { 100, 300, 400 };
float[] positions = { 100, 200, 150 }; float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions); addBlueprintStep(times, positions);
@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("delete vertex", () => AddStep("delete vertex", () =>
{ {
InputManager.PressKey(Key.ShiftLeft); InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft); InputManager.ReleaseKey(Key.ShiftLeft);
}); });
} }

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

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test] [Test]
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin) public void TestLegacyHUDComboCounterNotExistent([Values] bool withModifiedSkin)
{ {
if (withModifiedSkin) if (withModifiedSkin)
{ {
@ -29,10 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
CreateTest(); CreateTest();
} }
AddAssert("legacy HUD combo counter hidden", () => AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyDefaultComboCounter>().Any());
{
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
});
} }
} }
} }

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

@ -248,7 +248,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true)); AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit())); AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value); AddAssert("correct hit lighting colour",
() => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
} }
[Test] [Test]
@ -259,6 +260,16 @@ namespace osu.Game.Rulesets.Catch.Tests
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any()); AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
} }
[Test]
public void TestAllExplodedObjectsAtUniquePositions()
{
AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
AddStep("catch normal fruit", () => attemptCatch(new Fruit { IndexInBeatmap = 2, LastInCombo = true }));
AddAssert("two fruit at distinct x coordinates",
() => this.ChildrenOfType<CaughtFruit>().Select(f => f.DrawPosition.X).Distinct(),
() => Has.Exactly(2).Items);
}
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count); private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state); private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);

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

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[Resolved] [Resolved]
private IBeatSnapProvider? beatSnapProvider { get; set; } private IBeatSnapProvider? beatSnapProvider { get; set; }
[Resolved]
protected EditorBeatmap? EditorBeatmap { get; private set; }
protected EditablePath(Func<float, double> positionToTime) protected EditablePath(Func<float, double> positionToTime)
{ {
PositionToTime = positionToTime; PositionToTime = positionToTime;
@ -103,15 +107,23 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
// //
// The value is clamped here by the bindable min and max values. // The value is clamped here by the bindable min and max values.
// In case the required velocity is too large, the path is not preserved. // In case the required velocity is too large, the path is not preserved.
double previousVelocity = svBindable.Value;
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); // adjust velocity locally, so that once the SV change is applied by applying defaults
// (triggered by `EditorBeatmap.Update()` call at end of method),
// it results in the outcome desired by the user.
double relativeChange = svBindable.Value / previousVelocity;
double localVelocity = hitObject.Velocity * relativeChange;
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity);
if (beatSnapProvider == null) return; if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Duration; double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity;
EditorBeatmap?.Update(hitObject);
} }
public Vector2 ToRelativePosition(Vector2 screenSpacePosition) public Vector2 ToRelativePosition(Vector2 screenSpacePosition)

View File

@ -4,12 +4,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit; using osu.Game.Rulesets.Catch.Objects;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -19,22 +18,27 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray(); public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
private readonly JuiceStream juiceStream;
// To handle when the editor is scrolled while dragging. // To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition; private Vector2 dragStartPosition;
[Resolved] public SelectionEditablePath(JuiceStream juiceStream, Func<float, double> positionToTime)
private IEditorChangeHandler? changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToTime)
: base(positionToTime) : base(positionToTime)
{ {
this.juiceStream = juiceStream;
} }
public void AddVertex(Vector2 relativePosition) public void AddVertex(Vector2 relativePosition)
{ {
EditorBeatmap?.BeginChange();
double time = Math.Max(0, PositionToTime(relativePosition.Y)); double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(time, relativePosition.X); int index = AddVertex(time, relativePosition.X);
UpdateHitObjectFromPath(juiceStream);
selectOnly(index); selectOnly(index);
EditorBeatmap?.EndChange();
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos)); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
@ -45,9 +49,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (index == -1 || VertexStates[index].IsFixed) if (index == -1 || VertexStates[index].IsFixed)
return false; return false;
if (e.Button == MouseButton.Left && e.ShiftPressed) if (e.Button == MouseButton.Right && e.ShiftPressed)
{ {
EditorBeatmap?.BeginChange();
RemoveVertex(index); RemoveVertex(index);
UpdateHitObjectFromPath(juiceStream);
EditorBeatmap?.EndChange();
return true; return true;
} }
@ -74,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 0; i < VertexCount; i++) for (int i = 0; i < VertexCount; i++)
VertexStates[i].VertexBeforeChange = Vertices[i]; VertexStates[i].VertexBeforeChange = Vertices[i];
changeHandler?.BeginChange(); EditorBeatmap?.BeginChange();
return true; return true;
} }
@ -88,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
{ {
changeHandler?.EndChange(); EditorBeatmap?.EndChange();
} }
private int getMouseTargetVertex(Vector2 screenSpacePosition) private int getMouseTargetVertex(Vector2 screenSpacePosition)
@ -118,11 +126,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
private void deleteSelectedVertices() private void deleteSelectedVertices()
{ {
EditorBeatmap?.BeginChange();
for (int i = VertexCount - 1; i >= 0; i--) for (int i = VertexCount - 1; i >= 0; i--)
{ {
if (VertexStates[i].IsSelected) if (VertexStates[i].IsSelected)
RemoveVertex(i); RemoveVertex(i);
} }
UpdateHitObjectFromPath(juiceStream);
EditorBeatmap?.EndChange();
} }
} }
} }

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK; using osuTK;
@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
public partial class VertexPiece : Circle public partial class VertexPiece : Circle
{ {
private VertexState state = new VertexState();
[Resolved] [Resolved]
private OsuColour osuColour { get; set; } = null!; private OsuColour osuColour { get; set; } = null!;
@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateFrom(VertexState state) public void UpdateFrom(VertexState state)
{ {
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow; this.state = state;
updateMarkerDisplay();
}
protected override bool OnHover(HoverEvent e)
{
updateMarkerDisplay();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateMarkerDisplay();
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
var colour = osuColour.Yellow;
if (IsHovered || state.IsSelected)
colour = colour.Lighten(1);
Colour = colour;
Alpha = state.IsFixed ? 0.5f : 1; Alpha = state.IsFixed ? 0.5f : 1;
} }
} }

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

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToTime) editablePath = new SelectionEditablePath(hitObject, positionToTime)
}; };
} }

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

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;

View File

@ -21,11 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>(); public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>(); public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>(); public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
public Vector2 DisplayPosition => DrawPosition;
public Vector2 DisplaySize => Size * Scale; public Vector2 DisplaySize => Size * Scale;
public float DisplayRotation => Rotation; public float DisplayRotation => Rotation;
public double DisplayStartTime => HitObject.StartTime; public double DisplayStartTime => HitObject.StartTime;
/// <summary> /// <summary>
@ -44,19 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
} }
/// <summary>
/// Copies the hit object visual state from another <see cref="IHasCatchObjectState"/> object.
/// </summary>
public virtual void CopyStateFrom(IHasCatchObjectState objectState)
{
HitObject = objectState.HitObject;
Scale = Vector2.Divide(objectState.DisplaySize, Size);
Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value;
IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
}
protected override void FreeAfterUse() protected override void FreeAfterUse()
{ {
ClearTransforms(); ClearTransforms();
@ -64,5 +49,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
base.FreeAfterUse(); base.FreeAfterUse();
} }
public void RestoreState(CatchObjectState state)
{
HitObject = state.HitObject;
AccentColour.Value = state.AccentColour;
HyperDash.Value = state.HyperDash;
IndexInBeatmap.Value = state.IndexInBeatmap;
Position = state.DisplayPosition;
Scale = Vector2.Divide(state.DisplaySize, Size);
Rotation = state.DisplayRotation;
}
} }
} }

View File

@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
/// </summary> /// </summary>
protected readonly Container ScalingContainer; protected readonly Container ScalingContainer;
public Vector2 DisplayPosition => DrawPosition;
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale; public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
public float DisplayRotation => ScalingContainer.Rotation; public float DisplayRotation => ScalingContainer.Rotation;
@ -95,5 +97,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
base.OnFree(); base.OnFree();
} }
public void RestoreState(CatchObjectState state) => throw new NotSupportedException("Cannot restore state into a drawable catch hitobject.");
} }
} }

View File

@ -13,17 +13,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public interface IHasCatchObjectState public interface IHasCatchObjectState
{ {
PalpableCatchHitObject HitObject { get; } PalpableCatchHitObject HitObject { get; }
double DisplayStartTime { get; }
Bindable<Color4> AccentColour { get; } Bindable<Color4> AccentColour { get; }
Bindable<bool> HyperDash { get; } Bindable<bool> HyperDash { get; }
Bindable<int> IndexInBeatmap { get; } Bindable<int> IndexInBeatmap { get; }
double DisplayStartTime { get; }
Vector2 DisplayPosition { get; }
Vector2 DisplaySize { get; } Vector2 DisplaySize { get; }
float DisplayRotation { get; } float DisplayRotation { get; }
void RestoreState(CatchObjectState state);
} }
public static class HasCatchObjectStateExtensions
{
public static CatchObjectState SaveState(this IHasCatchObjectState target) => new CatchObjectState(
target.HitObject,
target.AccentColour.Value,
target.HyperDash.Value,
target.IndexInBeatmap.Value,
target.DisplayPosition,
target.DisplaySize,
target.DisplayRotation);
}
public readonly record struct CatchObjectState(
PalpableCatchHitObject HitObject,
Color4 AccentColour,
bool HyperDash,
int IndexInBeatmap,
Vector2 DisplayPosition,
Vector2 DisplaySize,
float DisplayRotation);
} }

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

@ -4,8 +4,8 @@
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy namespace osu.Game.Rulesets.Catch.Skinning.Legacy
@ -28,27 +28,51 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{ {
if (lookup is SkinComponentsContainerLookup containerLookup) switch (lookup)
{ {
case SkinComponentsContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
// Our own ruleset components default.
switch (containerLookup.Target) switch (containerLookup.Target)
{ {
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container; // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
if (providesComboCounter && components != null)
{ {
// catch may provide its own combo counter; hide the default. var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
// todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>())
legacyComboCounter.HiddenByRulesetImplementation = false;
}
return components; if (keyCounter != null)
}
}
if (lookup is CatchSkinComponentLookup catchSkinComponent)
{ {
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.CentreRight;
keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
}
};
}
return null;
case CatchSkinComponentLookup catchSkinComponent:
switch (catchSkinComponent.Component) switch (catchSkinComponent.Component)
{ {
case CatchSkinComponents.Fruit: case CatchSkinComponents.Fruit:

View File

@ -2,6 +2,7 @@
// 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.Buffers;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -362,7 +363,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (caughtObject == null) return; if (caughtObject == null) return;
caughtObject.CopyStateFrom(drawableObject); caughtObject.RestoreState(drawableObject.SaveState());
caughtObject.Anchor = Anchor.TopCentre; caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Position = position; caughtObject.Position = position;
caughtObject.Scale *= caught_fruit_scale_adjust; caughtObject.Scale *= caught_fruit_scale_adjust;
@ -411,41 +412,50 @@ namespace osu.Game.Rulesets.Catch.UI
} }
} }
private CaughtObject getDroppedObject(CaughtObject caughtObject) private CaughtObject getDroppedObject(CatchObjectState state)
{ {
var droppedObject = getCaughtObject(caughtObject.HitObject); var droppedObject = getCaughtObject(state.HitObject);
Debug.Assert(droppedObject != null); Debug.Assert(droppedObject != null);
droppedObject.CopyStateFrom(caughtObject); droppedObject.RestoreState(state);
droppedObject.Anchor = Anchor.TopLeft; droppedObject.Anchor = Anchor.TopLeft;
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget); droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(state.DisplayPosition, droppedObjectTarget);
return droppedObject; return droppedObject;
} }
private void clearPlate(DroppedObjectAnimation animation) private void clearPlate(DroppedObjectAnimation animation)
{ {
var caughtObjects = caughtObjectContainer.Children.ToArray(); int caughtCount = caughtObjectContainer.Children.Count;
CatchObjectState[] states = ArrayPool<CatchObjectState>.Shared.Rent(caughtCount);
try
{
for (int i = 0; i < caughtCount; i++)
states[i] = caughtObjectContainer.Children[i].SaveState();
caughtObjectContainer.Clear(false); caughtObjectContainer.Clear(false);
// Use the already returned PoolableDrawables for new objects for (int i = 0; i < caughtCount; i++)
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); {
CaughtObject obj = getDroppedObject(states[i]);
droppedObjectTarget.AddRange(droppedObjects); droppedObjectTarget.Add(obj);
applyDropAnimation(obj, animation);
foreach (var droppedObject in droppedObjects) }
applyDropAnimation(droppedObject, animation); }
finally
{
ArrayPool<CatchObjectState>.Shared.Return(states);
}
} }
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{ {
CatchObjectState state = caughtObject.SaveState();
caughtObjectContainer.Remove(caughtObject, false); caughtObjectContainer.Remove(caughtObject, false);
var droppedObject = getDroppedObject(caughtObject); var droppedObject = getDroppedObject(state);
droppedObjectTarget.Add(droppedObject); droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation); applyDropAnimation(droppedObject, animation);
} }

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

@ -0,0 +1,45 @@
// 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.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneManiaEditorSaving : EditorSavingTestScene
{
protected override Ruleset CreateRuleset() => new ManiaRuleset();
[Test]
public void TestKeyCountChange()
{
LabelledSliderBar<float> keyCount = null!;
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null);
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
AddStep("change key count to 8", () =>
{
keyCount.Current.Value = 8;
});
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
AddStep("refuse", () => InputManager.Key(Key.Number2));
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
AddStep("change key count to 8 again", () =>
{
keyCount.Current.Value = 8;
});
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
}
}
}

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

@ -0,0 +1,96 @@
// 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;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneManiaSelectionHandler : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestHorizontalFlipOverSelection()
{
ManiaHitObject first = null!, second = null!, third = null!;
AddStep("create objects", () =>
{
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("flip horizontally over selection", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxButton>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("first object stayed in place", () => first.Column, () => Is.EqualTo(2));
AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(3));
AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(1));
}
[Test]
public void TestHorizontalFlipOverPlayfield()
{
ManiaHitObject first = null!, second = null!, third = null!;
AddStep("create objects", () =>
{
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("flip horizontally", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.H);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("first object flipped", () => first.Column, () => Is.EqualTo(1));
AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(2));
AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(0));
}
[Test]
public void TestVerticalFlip()
{
ManiaHitObject first = null!, second = null!, third = null!;
AddStep("create objects", () =>
{
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("flip vertically", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.J);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("first object flipped", () => first.StartTime, () => Is.EqualTo(2250));
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
}
}
}

View File

@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
[TestCase(ManiaAction.Key1)] [TestCase(ManiaAction.Key1)]
[TestCase(ManiaAction.Key1, ManiaAction.Key2)] [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
[TestCase(ManiaAction.Special1)] [TestCase(ManiaAction.Key5)]
[TestCase(ManiaAction.Key8)] [TestCase(ManiaAction.Key9)]
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions) public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
{ {
var beatmap = new ManiaBeatmap(new StageDefinition(9)); var beatmap = new ManiaBeatmap(new StageDefinition(9));
@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key1)] [TestCase(ManiaAction.Key1)]
[TestCase(ManiaAction.Key1, ManiaAction.Key2)] [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
[TestCase(ManiaAction.Special1)] [TestCase(ManiaAction.Key3)]
[TestCase(ManiaAction.Special2)]
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
[TestCase(ManiaAction.Key8)] [TestCase(ManiaAction.Key8)]
[TestCase(ManiaAction.Key3, ManiaAction.Key8)]
[TestCase(ManiaAction.Key3, ManiaAction.Key6)]
[TestCase(ManiaAction.Key10)]
public void TestEncodeDecodeDualStage(params ManiaAction[] actions) public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
{ {
var beatmap = new ManiaBeatmap(new StageDefinition(5)); var beatmap = new ManiaBeatmap(new StageDefinition(5));

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

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Skinning.Argon;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public partial class TestSceneComboCounter : ManiaSkinnableTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup", () => SetContents(s =>
{
if (s is ArgonSkin)
return new ArgonManiaComboCounter();
if (s is LegacySkin)
return new LegacyManiaComboCounter();
return new LegacyManiaComboCounter();
}));
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
}
}
}

View File

@ -28,13 +28,19 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
AddStep("Show " + result.GetDescription(), () => AddStep("Show " + result.GetDescription(), () =>
{ {
SetContents(_ => SetContents(_ =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{ {
Type = result var drawableManiaJudgement = new DrawableManiaJudgement
}, null)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
};
drawableManiaJudgement.Apply(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
}, null);
return drawableManiaJudgement;
}); });
// for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value // for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value

View File

@ -3,15 +3,22 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
public partial class TestScenePlayfield : ManiaSkinnableTestScene public partial class TestScenePlayfield : ManiaSkinnableTestScene
{ {
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
private List<StageDefinition> stageDefinitions = new List<StageDefinition>(); private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
[Test] [Test]
@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Child = new ManiaPlayfield(stageDefinitions) Child = new ManiaPlayfield(stageDefinitions)
}); });
}); });
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
} }
[TestCase(2)] [TestCase(2)]
@ -54,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
} }
}); });
}); });
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
} }
protected override IBeatmap CreateBeatmapForSkinProvider() protected override IBeatmap CreateBeatmapForSkinProvider()

View File

@ -14,12 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
SetContents(_ => SetContents(_ =>
{ {
ManiaAction normalAction = ManiaAction.Key1; ManiaAction action = ManiaAction.Key1;
ManiaAction specialAction = ManiaAction.Special1;
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{ {
Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction) Child = new Stage(0, new StageDefinition(4), ref action)
}; };
}); });
} }

View File

@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames"); Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
} }
[Test] [Test]
@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames"); Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
} }
[Test] [Test]

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,7 +474,7 @@ 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()));
} }

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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaPlayerLegacySkin : LegacySkinPlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
// play with a converted beatmap to allow dual stages mod to work.
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(new RulesetInfo());
protected override bool HasCustomSteps => true;
[Test]
public void TestSingleStage()
{
AddStep("Load single stage", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
[Test]
public void TestDualStage()
{
AddStep("Load dual stage", () => LoadPlayer(new Mod[] { new ManiaModDualStages() }));
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
}
}

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

@ -131,9 +131,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action) private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
{ {
var specialAction = ManiaAction.Special1; var stage = new Stage(0, new StageDefinition(2), ref action);
var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction);
stages.Add(stage); stages.Add(stage);
return new ScrollingTestContainer(direction) return new ScrollingTestContainer(direction)

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

@ -45,18 +45,15 @@ namespace osu.Game.Rulesets.Mania
LeftKeys = stage1LeftKeys, LeftKeys = stage1LeftKeys,
RightKeys = stage1RightKeys, RightKeys = stage1RightKeys,
SpecialKey = InputKey.V, SpecialKey = InputKey.V,
SpecialAction = ManiaAction.Special1, }.GenerateKeyBindingsFor(singleStageVariant);
NormalActionStart = ManiaAction.Key1
}.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
var stage2Bindings = new VariantMappingGenerator var stage2Bindings = new VariantMappingGenerator
{ {
LeftKeys = stage2LeftKeys, LeftKeys = stage2LeftKeys,
RightKeys = stage2RightKeys, RightKeys = stage2RightKeys,
SpecialKey = InputKey.B, SpecialKey = InputKey.B,
SpecialAction = ManiaAction.Special2, ActionStart = (ManiaAction)singleStageVariant,
NormalActionStart = nextNormal }.GenerateKeyBindingsFor(singleStageVariant);
}.GenerateKeyBindingsFor(singleStageVariant, out _);
return stage1Bindings.Concat(stage2Bindings); return stage1Bindings.Concat(stage2Bindings);
} }

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

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -16,6 +17,16 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved] [Resolved]
private HitObjectComposer composer { get; set; } = null!; private HitObjectComposer composer { get; set; } = null!;
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
var selectedObjects = SelectedItems.OfType<ManiaHitObject>().ToArray();
SelectionBox.CanFlipX = canFlipX(selectedObjects);
SelectionBox.CanFlipY = canFlipY(selectedObjects);
}
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{ {
var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint; var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint;
@ -26,6 +37,58 @@ namespace osu.Game.Rulesets.Mania.Edit
return true; return true;
} }
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{
var selectedObjects = SelectedItems.OfType<ManiaHitObject>().ToArray();
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
if (selectedObjects.Length == 0)
return false;
switch (direction)
{
case Direction.Horizontal:
if (!canFlipX(selectedObjects))
return false;
int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
EditorBeatmap.PerformOnSelection(hitObject =>
{
var maniaObject = (ManiaHitObject)hitObject;
maniaPlayfield.Remove(maniaObject);
maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
maniaPlayfield.Add(maniaObject);
});
return true;
case Direction.Vertical:
if (!canFlipY(selectedObjects))
return false;
double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
EditorBeatmap.PerformOnSelection(hitObject =>
{
hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
});
return true;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, "Cannot flip over the supplied direction.");
}
}
private static bool canFlipX(ManiaHitObject[] selectedObjects)
=> selectedObjects.Select(ho => ho.Column).Distinct().Count() > 1;
private static bool canFlipY(ManiaHitObject[] selectedObjects)
=> selectedObjects.Length > 1 && selectedObjects.Min(ho => ho.StartTime) < selectedObjects.Max(ho => ho.GetEndTime());
private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent) private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent)
{ {
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield; var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;

View File

@ -3,20 +3,164 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
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.Resources.Localisation.Web;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup namespace osu.Game.Rulesets.Mania.Edit.Setup
{ {
public partial class ManiaDifficultySection : DifficultySection public partial class ManiaDifficultySection : SetupSection
{ {
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
private LabelledSwitchButton specialStyle { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
[Resolved]
private Editor? editor { get; set; }
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania; Children = new Drawable[]
CircleSizeSlider.Description = "The number of columns in the beatmap"; {
if (CircleSizeSlider.Current is BindableNumber<float> circleSizeFloat) keyCountSlider = new LabelledSliderBar<float>
circleSizeFloat.Precision = 1; {
Label = BeatmapsetsStrings.ShowStatsCsMania,
FixedLabelWidth = LABEL_WIDTH,
Description = "The number of columns in the beatmap",
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
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.BeatmapInfo.SpecialStyle }
},
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,
}
},
};
keyCountSlider.Current.BindValueChanged(updateKeyCount);
healthDrainSlider.Current.BindValueChanged(_ => updateValues());
overallDifficultySlider.Current.BindValueChanged(_ => updateValues());
baseVelocitySlider.Current.BindValueChanged(_ => updateValues());
tickRateSlider.Current.BindValueChanged(_ => updateValues());
}
private bool updatingKeyCount;
private void updateKeyCount(ValueChangedEvent<float> keyCount)
{
if (updatingKeyCount) return;
updateValues();
if (editor == null) return;
updatingKeyCount = true;
editor.Reload().ContinueWith(t =>
{
if (!t.GetResultSafely())
{
Schedule(() =>
{
changeHandler!.RestoreState(-1);
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue;
updatingKeyCount = false;
});
}
else
{
updatingKeyCount = false;
}
});
}
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 = keyCountSlider.Current.Value;
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
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

@ -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.BeatmapInfo.SpecialStyle }
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
specialStyle.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
Beatmap.SaveState();
}
}
}

View File

@ -19,16 +19,8 @@ namespace osu.Game.Rulesets.Mania
public enum ManiaAction public enum ManiaAction
{ {
[Description("Special 1")]
Special1 = 1,
[Description("Special 2")]
Special2,
// This offsets the start value of normal keys in-case we add more special keys
// above at a later time, without breaking replays/configs.
[Description("Key 1")] [Description("Key 1")]
Key1 = 10, Key1,
[Description("Key 2")] [Description("Key 2")]
Key2, Key2,

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 DifficultySection 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

@ -17,28 +17,9 @@ namespace osu.Game.Rulesets.Mania.Replays
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
private readonly ManiaAction[] columnActions;
public ManiaAutoGenerator(ManiaBeatmap beatmap) public ManiaAutoGenerator(ManiaBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
columnActions = new ManiaAction[Beatmap.TotalColumns];
var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1;
int totalCounter = 0;
foreach (var stage in Beatmap.Stages)
{
for (int i = 0; i < stage.Columns; i++)
{
if (stage.IsSpecialColumn(i))
columnActions[totalCounter] = specialAction++;
else
columnActions[totalCounter] = normalAction++;
totalCounter++;
}
}
} }
protected override void GenerateFrames() protected override void GenerateFrames()
@ -57,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Replays
switch (point) switch (point)
{ {
case HitPoint: case HitPoint:
actions.Add(columnActions[point.Column]); actions.Add(ManiaAction.Key1 + point.Column);
break; break;
case ReleasePoint: case ReleasePoint:
actions.Remove(columnActions[point.Column]); actions.Remove(ManiaAction.Key1 + point.Column);
break; break;
} }
} }

View File

@ -1,11 +1,9 @@
// 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 osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
@ -27,118 +25,27 @@ namespace osu.Game.Rulesets.Mania.Replays
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null) public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap; var action = ManiaAction.Key1;
var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1;
int activeColumns = (int)(legacyFrame.MouseX ?? 0); int activeColumns = (int)(legacyFrame.MouseX ?? 0);
int counter = 0;
while (activeColumns > 0) while (activeColumns > 0)
{ {
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
if ((activeColumns & 1) > 0) if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction); Actions.Add(action);
if (isSpecial) action++;
specialAction++;
else
normalAction++;
counter++;
activeColumns >>= 1; activeColumns >>= 1;
} }
} }
public LegacyReplayFrame ToLegacy(IBeatmap beatmap) public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap;
int keys = 0; int keys = 0;
foreach (var action in Actions) foreach (var action in Actions)
{ keys |= 1 << (int)action;
switch (action)
{
case ManiaAction.Special1:
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
break;
case ManiaAction.Special2:
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
break;
default:
// the index in lazer, which doesn't include special keys.
int nonSpecialKeyIndex = action - ManiaAction.Key1;
// the index inclusive of special keys.
int overallIndex = 0;
// iterate to find the index including special keys.
for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
{
// skip over special columns.
if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
continue;
// found a non-special column to use.
if (nonSpecialKeyIndex == 0)
break;
// found a non-special column but not ours.
nonSpecialKeyIndex--;
}
keys |= 1 << overallIndex;
break;
}
}
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None); return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
} }
/// <summary>
/// Find the overall index (across all stages) for a specified special key.
/// </summary>
/// <param name="maniaBeatmap">The beatmap.</param>
/// <param name="specialOffset">The special key offset (0 is S1).</param>
/// <returns>The overall index for the special column.</returns>
private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
{
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
{
if (isColumnAtIndexSpecial(maniaBeatmap, i))
{
if (specialOffset == 0)
return i;
specialOffset--;
}
}
throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
}
/// <summary>
/// Check whether the column at an overall index (across all stages) is a special column.
/// </summary>
/// <param name="beatmap">The beatmap.</param>
/// <param name="index">The overall index to check.</param>
private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
{
foreach (var stage in beatmap.Stages)
{
if (index >= stage.Columns)
{
index -= stage.Columns;
continue;
}
return stage.IsSpecialColumn(index);
}
throw new ArgumentException("Column index is too high.", nameof(index));
}
} }
} }

View File

@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mania
LeftKeys = leftKeys, LeftKeys = leftKeys,
RightKeys = rightKeys, RightKeys = rightKeys,
SpecialKey = InputKey.Space, SpecialKey = InputKey.Space,
SpecialAction = ManiaAction.Special1, }.GenerateKeyBindingsFor(variant);
NormalActionStart = ManiaAction.Key1,
}.GenerateKeyBindingsFor(variant, out _);
} }
} }

View File

@ -0,0 +1,52 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonManiaComboCounter : ArgonComboCounter
{
protected override bool DisplayXSymbol => false;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
protected override void LoadComplete()
{
base.LoadComplete();
// the logic of flipping the position of the combo counter w.r.t. the direction does not work with "Closest" anchor,
// because it always forces the anchor to be top or bottom based on scrolling direction.
UsesFixedAnchor = true;
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => updateAnchor());
// two schedules are required so that updateAnchor is executed in the next frame,
// which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer.
Schedule(() => Schedule(updateAnchor));
}
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (!Anchor.HasFlag(Anchor.y1))
{
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
}
// change the sign of the Y coordinate in line with the scrolling direction.
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
}
}
}

View File

@ -2,8 +2,10 @@
// 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 osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -26,6 +28,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{ {
switch (lookup) switch (lookup)
{ {
case SkinComponentsContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.ShowLabel.Value = false;
combo.Anchor = Anchor.TopCentre;
combo.Origin = Anchor.Centre;
combo.Y = 200;
}
})
{
new ArgonManiaComboCounter(),
};
}
return null;
case GameplaySkinComponentLookup<HitResult> resultComponent: case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported. // This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great) if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)

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

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyManiaComboCounter : LegacyComboCounter
{
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
DisplayedCountText.Anchor = Anchor.Centre;
DisplayedCountText.Origin = Anchor.Centre;
PopOutCountText.Anchor = Anchor.Centre;
PopOutCountText.Origin = Anchor.Centre;
PopOutCountText.Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red;
}
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
protected override void LoadComplete()
{
base.LoadComplete();
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => updateAnchor());
// two schedules are required so that updateAnchor is executed in the next frame,
// which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer.
Schedule(() => Schedule(updateAnchor));
}
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (!Anchor.HasFlag(Anchor.y1))
{
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
}
// since we flip the vertical anchor when changing scroll direction,
// we can use the sign of the Y value as an indicator to make the combo counter displayed correctly.
if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up))
Y = -Y;
}
protected override void OnCountIncrement()
{
base.OnCountIncrement();
PopOutCountText.Hide();
DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f))
.ScaleTo(new Vector2(1f), 300, Easing.Out)
.FadeIn(120);
}
protected override void OnCountChange()
{
base.OnCountChange();
PopOutCountText.Hide();
DisplayedCountText.ScaleTo(1f);
}
protected override void OnCountRolling()
{
if (DisplayedCount > 0)
{
PopOutCountText.Text = FormatCount(DisplayedCount);
PopOutCountText.FadeTo(0.8f).FadeOut(200)
.ScaleTo(1f).ScaleTo(4f, 200);
DisplayedCountText.FadeTo(0.5f, 300);
}
base.OnCountRolling();
}
}
}

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

@ -5,9 +5,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -78,6 +80,40 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
switch (lookup) switch (lookup)
{ {
case SkinComponentsContainerLookup containerLookup:
// Modifications for global components.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.Anchor = Anchor.TopCentre;
combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
}
})
{
new LegacyManiaComboCounter(),
};
}
return null;
case GameplaySkinComponentLookup<HitResult> resultComponent: case GameplaySkinComponentLookup<HitResult> resultComponent:
return getResult(resultComponent.Component); return getResult(resultComponent.Component);

View File

@ -5,22 +5,12 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
public partial class DrawableManiaJudgement : DrawableJudgement public partial class DrawableManiaJudgement : DrawableJudgement
{ {
public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject)
{
}
public DrawableManiaJudgement()
{
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece

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

@ -66,13 +66,12 @@ namespace osu.Game.Rulesets.Mania.UI
Content = new[] { new Drawable[stageDefinitions.Count] } Content = new[] { new Drawable[stageDefinitions.Count] }
}); });
var normalColumnAction = ManiaAction.Key1; var columnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1;
int firstColumnIndex = 0; int firstColumnIndex = 0;
for (int i = 0; i < stageDefinitions.Count; i++) for (int i = 0; i < stageDefinitions.Count; i++)
{ {
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref columnAction);
playfieldGrid.Content[0][i] = newStage; playfieldGrid.Content[0][i] = newStage;

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

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.UI
private ISkinSource currentSkin = null!; private ISkinSource currentSkin = null!;
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction columnStartAction)
{ {
this.firstColumnIndex = firstColumnIndex; this.firstColumnIndex = firstColumnIndex;
Definition = definition; Definition = definition;
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 1, Width = 1,
Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ } Action = { Value = columnStartAction++ }
}; };
topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); topLevelContainer.Add(column.TopLevelContainer.CreateProxy());

View File

@ -26,37 +26,30 @@ namespace osu.Game.Rulesets.Mania
public InputKey SpecialKey; public InputKey SpecialKey;
/// <summary> /// <summary>
/// The <see cref="ManiaAction"/> at which the normal columns should begin. /// The <see cref="ManiaAction"/> at which the columns should begin.
/// </summary> /// </summary>
public ManiaAction NormalActionStart; public ManiaAction ActionStart;
/// <summary>
/// The <see cref="ManiaAction"/> for the special column.
/// </summary>
public ManiaAction SpecialAction;
/// <summary> /// <summary>
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns. /// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
/// </summary> /// </summary>
/// <param name="columns">The number of columns that need to be bound.</param> /// <param name="columns">The number of columns that need to be bound.</param>
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
/// <returns>The keybindings.</returns> /// <returns>The keybindings.</returns>
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction) public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns)
{ {
ManiaAction currentNormalAction = NormalActionStart; ManiaAction currentAction = ActionStart;
var bindings = new List<KeyBinding>(); var bindings = new List<KeyBinding>();
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++)); bindings.Add(new KeyBinding(LeftKeys[i], currentAction++));
if (columns % 2 == 1) if (columns % 2 == 1)
bindings.Add(new KeyBinding(SpecialKey, SpecialAction)); bindings.Add(new KeyBinding(SpecialKey, currentAction++));
for (int i = 0; i < columns / 2; i++) for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++)); bindings.Add(new KeyBinding(RightKeys[i], currentAction++));
nextNormalAction = currentNormalAction;
return bindings; return bindings;
} }
} }

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
moveMouseToHitObject(1); moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection(); mergeSelection();
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
moveMouseToHitObject(1); moveMouseToHitObject(1);
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
mergeSelection(); mergeSelection();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2)); && EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
moveMouseToHitObject(1); moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection(); mergeSelection();

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;
@ -8,7 +9,9 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -25,22 +28,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false); gridActive<RectangularPositionSnapGrid>(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(true); gridActive<RectangularPositionSnapGrid>(true);
AddStep("disable distance snap grid", () => InputManager.Key(Key.T)); AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
rectangularGridActive(true); gridActive<RectangularPositionSnapGrid>(true);
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y)); AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false); gridActive<RectangularPositionSnapGrid>(false);
} }
[Test] [Test]
@ -117,33 +120,58 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test] [Test]
public void TestGridSnapMomentaryToggle() public void TestGridSnapMomentaryToggle()
{ {
rectangularGridActive(false); gridActive<RectangularPositionSnapGrid>(false);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
rectangularGridActive(true); gridActive<RectangularPositionSnapGrid>(true);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
rectangularGridActive(false); gridActive<RectangularPositionSnapGrid>(false);
} }
private void rectangularGridActive(bool active) private void gridActive<T>(bool active) where T : PositionSnapGrid
{ {
AddStep("choose placement tool", () => InputManager.Key(Key.Number2)); AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
AddStep("move cursor to (1, 1)", () => AddStep("move cursor to spacing + (1, 1)", () =>
{ {
var composer = Editor.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single(); var composer = Editor.ChildrenOfType<T>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1))); InputManager.MoveMouseTo(composer.ToScreenSpace(uniqueSnappingPosition(composer) + new Vector2(1, 1)));
}); });
if (active) if (active)
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0))); {
AddAssert("placement blueprint at spacing + (0, 0)", () =>
{
var composer = Editor.ChildrenOfType<T>().Single();
return Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position,
uniqueSnappingPosition(composer));
});
}
else else
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1))); {
AddAssert("placement blueprint at spacing + (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<T>().Single();
return Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position,
uniqueSnappingPosition(composer) + new Vector2(1, 1));
});
}
}
private Vector2 uniqueSnappingPosition(PositionSnapGrid grid)
{
return grid switch
{
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
_ => Vector2.Zero
};
} }
[Test] [Test]
public void TestGridSizeToggling() public void TestGridSizeToggling()
{ {
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Any()); AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
gridSizeIs(4); gridSizeIs(4);
nextGridSizeIs(8); nextGridSizeIs(8);
@ -159,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
} }
private void gridSizeIs(int size) private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single().Spacing == new Vector2(size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size); && EditorBeatmap.BeatmapInfo.GridSize == size);
} }
} }

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,54 @@ 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("press alt-2", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Number2);
InputManager.ReleaseKey(Key.AltLeft);
});
assertControlPointTypeDuringPlacement(0, PathType.BEZIER);
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.BEZIER);
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -293,7 +342,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 +361,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 +386,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 +408,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 +428,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 +449,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 +470,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 +487,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 +503,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

@ -2,6 +2,7 @@
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -41,22 +42,27 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
base.PostProcess(); base.PostProcess();
var osuBeatmap = (Beatmap<OsuHitObject>)Beatmap; ApplyStacking(Beatmap);
}
if (osuBeatmap.HitObjects.Count > 0) internal static void ApplyStacking(IBeatmap beatmap)
{
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
if (hitObjects.Count > 0)
{ {
// Reset stacking // Reset stacking
foreach (var h in osuBeatmap.HitObjects) foreach (var h in hitObjects)
h.StackHeight = 0; h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(osuBeatmap, 0, osuBeatmap.HitObjects.Count - 1); applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(osuBeatmap); applyStackingOld(beatmap.BeatmapInfo, hitObjects);
} }
} }
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex) private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -64,24 +70,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
int extendedEndIndex = endIndex; int extendedEndIndex = endIndex;
if (endIndex < beatmap.HitObjects.Count - 1) if (endIndex < hitObjects.Count - 1)
{ {
// Extend the end index to include objects they are stacked on // Extend the end index to include objects they are stacked on
for (int i = endIndex; i >= startIndex; i--) for (int i = endIndex; i >= startIndex; i--)
{ {
int stackBaseIndex = i; int stackBaseIndex = i;
for (int n = stackBaseIndex + 1; n < beatmap.HitObjects.Count; n++) for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++)
{ {
OsuHitObject stackBaseObject = beatmap.HitObjects[stackBaseIndex]; OsuHitObject stackBaseObject = hitObjects[stackBaseIndex];
if (stackBaseObject is Spinner) break; if (stackBaseObject is Spinner) break;
OsuHitObject objectN = beatmap.HitObjects[n]; OsuHitObject objectN = hitObjects[n];
if (objectN is Spinner) if (objectN is Spinner)
continue; continue;
double endTime = stackBaseObject.GetEndTime(); double endTime = stackBaseObject.GetEndTime();
double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency; double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency;
if (objectN.StartTime - endTime > stackThreshold) if (objectN.StartTime - endTime > stackThreshold)
// We are no longer within stacking range of the next object. // We are no longer within stacking range of the next object.
@ -100,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (stackBaseIndex > extendedEndIndex) if (stackBaseIndex > extendedEndIndex)
{ {
extendedEndIndex = stackBaseIndex; extendedEndIndex = stackBaseIndex;
if (extendedEndIndex == beatmap.HitObjects.Count - 1) if (extendedEndIndex == hitObjects.Count - 1)
break; break;
} }
} }
@ -123,10 +129,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
* 2 and 1 will be ignored in the i loop because they already have a stack value. * 2 and 1 will be ignored in the i loop because they already have a stack value.
*/ */
OsuHitObject objectI = beatmap.HitObjects[i]; OsuHitObject objectI = hitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue; if (objectI.StackHeight != 0 || objectI is Spinner) continue;
double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency; double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency;
/* If this object is a hitcircle, then we enter this "special" case. /* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
@ -136,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
while (--n >= 0) while (--n >= 0)
{ {
OsuHitObject objectN = beatmap.HitObjects[n]; OsuHitObject objectN = hitObjects[n];
if (objectN is Spinner) continue; if (objectN is Spinner) continue;
double endTime = objectN.GetEndTime(); double endTime = objectN.GetEndTime();
@ -164,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
for (int j = n + 1; j <= i; j++) for (int j = n + 1; j <= i; j++)
{ {
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above). // For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
OsuHitObject objectJ = beatmap.HitObjects[j]; OsuHitObject objectJ = hitObjects[j];
if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance) if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
objectJ.StackHeight -= offset; objectJ.StackHeight -= offset;
} }
@ -191,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
*/ */
while (--n >= startIndex) while (--n >= startIndex)
{ {
OsuHitObject objectN = beatmap.HitObjects[n]; OsuHitObject objectN = hitObjects[n];
if (objectN is Spinner) continue; if (objectN is Spinner) continue;
if (objectI.StartTime - objectN.StartTime > stackThreshold) if (objectI.StartTime - objectN.StartTime > stackThreshold)
@ -208,11 +214,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private void applyStackingOld(Beatmap<OsuHitObject> beatmap) private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < beatmap.HitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {
OsuHitObject currHitObject = beatmap.HitObjects[i]; OsuHitObject currHitObject = hitObjects[i];
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider)) if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
continue; continue;
@ -220,11 +226,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
double startTime = currHitObject.GetEndTime(); double startTime = currHitObject.GetEndTime();
int sliderStack = 0; int sliderStack = 0;
for (int j = i + 1; j < beatmap.HitObjects.Count; j++) for (int j = i + 1; j < hitObjects.Count; j++)
{ {
double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency; double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency;
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime) if (hitObjects[j].StartTime - stackThreshold > startTime)
break; break;
// The start position of the hitobject, or the position at the end of the path if the hitobject is a slider // The start position of the hitobject, or the position at the end of the path if the hitobject is a slider
@ -239,17 +245,17 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where // Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
// if we use `EndTime` here it would result in unexpected stacking. // if we use `EndTime` here it would result in unexpected stacking.
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance) if (Vector2Extensions.Distance(hitObjects[j].Position, currHitObject.Position) < stack_distance)
{ {
currHitObject.StackHeight++; currHitObject.StackHeight++;
startTime = beatmap.HitObjects[j].StartTime; startTime = hitObjects[j].StartTime;
} }
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) else if (Vector2Extensions.Distance(hitObjects[j].Position, position2) < stack_distance)
{ {
// Case for sliders - bump notes down and right, rather than up and left. // Case for sliders - bump notes down and right, rather than up and left.
sliderStack++; sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack; hitObjects[j].StackHeight -= sliderStack;
startTime = beatmap.HitObjects[j].StartTime; startTime = hitObjects[j].StartTime;
} }
} }
} }

View File

@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightRating *= 0.7; flashlightRating *= 0.7;
} }
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
double baseFlashlightPerformance = 0.0; double baseFlashlightPerformance = 0.0;
if (mods.Any(h => h is OsuModFlashlight)) if (mods.Any(h => h is OsuModFlashlight))
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0; baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
double basePerformance = double basePerformance =
Math.Pow( Math.Pow(

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