1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 04:07:25 +08:00

Merge branch 'master' into polygon-tool

This commit is contained in:
Dean Herbert 2024-08-19 13:00:02 +09:00 committed by GitHub
commit f34132fd6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
514 changed files with 16653 additions and 5030 deletions

View File

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

View File

@ -64,10 +64,11 @@ jobs:
matrix:
os:
- { 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 }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
@ -120,9 +121,7 @@ jobs:
build-only-ios:
name: Build only (iOS)
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
# 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
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
@ -136,8 +135,5 @@ jobs:
- name: Install .NET Workloads
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
run: dotnet build -c Debug osu.iOS

View File

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

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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("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]
@ -259,6 +260,16 @@ namespace osu.Game.Rulesets.Catch.Tests
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 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,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1 : ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity,
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield();

View File

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

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[Resolved]
private IBeatSnapProvider? beatSnapProvider { get; set; }
[Resolved]
protected EditorBeatmap? EditorBeatmap { get; private set; }
protected EditablePath(Func<float, double> 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.
// In case the required velocity is too large, the path is not preserved.
double previousVelocity = svBindable.Value;
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;
double endTime = hitObject.StartTime + path.Duration;
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)

View File

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

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public partial class VertexPiece : Circle
{
private VertexState state = new VertexState();
[Resolved]
private OsuColour osuColour { get; set; } = null!;
@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
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;
}
}

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
scrollingPath = new ScrollingPath(),
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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit
result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlagFast(SnapType.RelativeGrids))
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)

View File

@ -15,7 +15,7 @@ using osuTK;
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;

View File

@ -21,11 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
public Vector2 DisplayPosition => DrawPosition;
public Vector2 DisplaySize => Size * Scale;
public float DisplayRotation => Rotation;
public double DisplayStartTime => HitObject.StartTime;
/// <summary>
@ -44,19 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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()
{
ClearTransforms();
@ -64,5 +49,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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>
protected readonly Container ScalingContainer;
public Vector2 DisplayPosition => DrawPosition;
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
public float DisplayRotation => ScalingContainer.Rotation;
@ -95,5 +97,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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
{
PalpableCatchHitObject HitObject { get; }
double DisplayStartTime { get; }
Bindable<Color4> AccentColour { get; }
Bindable<bool> HyperDash { get; }
Bindable<int> IndexInBeatmap { get; }
double DisplayStartTime { get; }
Vector2 DisplayPosition { get; }
Vector2 DisplaySize { 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.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
@ -46,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TickDistanceMultiplier = 1;
[JsonIgnore]
private double velocityFactor;
public double Velocity { get; private set; }
[JsonIgnore]
private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * SliderVelocityMultiplier;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
public double TickDistance { get; private set; }
/// <summary>
/// The length of one span of this <see cref="JuiceStream"/>.
@ -68,8 +63,13 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
double scoringDistance = Velocity * timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -4,8 +4,8 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
@ -28,76 +28,100 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is SkinComponentsContainerLookup containerLookup)
switch (lookup)
{
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container;
case SkinComponentsContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
if (providesComboCounter && components != null)
{
// catch may provide its own combo counter; hide the default.
// 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 (lookup is CatchSkinComponentLookup catchSkinComponent)
{
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
// 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;
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
// Our own ruleset components default.
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
return null;
if (keyCounter != null)
{
// 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(),
}
};
}
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece();
return null;
return null;
case CatchSkinComponentLookup catchSkinComponent:
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
case CatchSkinComponents.Catcher:
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
return null;
if (version < 2.3m)
{
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
return null;
return null;
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece();
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter();
return null;
return null;
case CatchSkinComponents.Catcher:
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
if (version < 2.3m)
{
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
return null;
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
default:
throw new UnsupportedSkinComponentException(lookup);
}
return null;
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter();
return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
}
return base.GetDrawableComponent(lookup);

View File

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

View File

@ -1,13 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
@ -30,5 +35,43 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
}
[Test]
public void TestReloadOnBPMChange()
{
HitObjectComposer oldComposer = null!;
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
AddStep("switch to timing tab", () => InputManager.Key(Key.F3));
AddUntilStep("wait for loaded", () => this.ChildrenOfType<TimingAdjustButton>().ElementAtOrDefault(1), () => Is.Not.Null);
AddStep("change timing point BPM", () =>
{
var bpmControl = this.ChildrenOfType<TimingAdjustButton>().ElementAt(1);
InputManager.MoveMouseTo(bpmControl);
InputManager.Click(MouseButton.Left);
});
AddStep("switch back to composer", () => InputManager.Key(Key.F1));
AddUntilStep("composer reloaded", () =>
{
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
return composer != null && composer != oldComposer;
});
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
AddStep("undo", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Z);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("composer reloaded", () =>
{
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
return composer != null && composer != oldComposer;
});
}
}
}

View File

@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests
{
[TestCase(ManiaAction.Key1)]
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
[TestCase(ManiaAction.Special1)]
[TestCase(ManiaAction.Key8)]
[TestCase(ManiaAction.Key5)]
[TestCase(ManiaAction.Key9)]
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
{
var beatmap = new ManiaBeatmap(new StageDefinition(9));
@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key1)]
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
[TestCase(ManiaAction.Special1)]
[TestCase(ManiaAction.Special2)]
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
[TestCase(ManiaAction.Key3)]
[TestCase(ManiaAction.Key8)]
[TestCase(ManiaAction.Key3, ManiaAction.Key8)]
[TestCase(ManiaAction.Key3, ManiaAction.Key6)]
[TestCase(ManiaAction.Key10)]
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
{
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.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
});
[Test]
public void TestBreaksPreservedOnOriginalBeatmap()
{
var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo);
beatmap.Breaks.Clear();
beatmap.Breaks.Add(new BreakPeriod(0, 1000));
var workingBeatmap = new FlatWorkingBeatmap(beatmap);
var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() });
Assert.That(playableWithInvert.Breaks.Count, Is.Zero);
var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo);
Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero);
Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000)));
}
}
}

View File

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

View File

@ -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,14 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
AddStep("Show " + result.GetDescription(), () =>
{
SetContents(_ =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
}, null)
{
var drawableManiaJudgement = new DrawableManiaJudgement
{
Anchor = 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
// (see `LegacyManiaJudgementPiece.load()`).

View File

@ -3,15 +3,22 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public partial class TestScenePlayfield : ManiaSkinnableTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
[Test]
@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
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)]
@ -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()

View File

@ -14,12 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
SetContents(_ =>
{
ManiaAction normalAction = ManiaAction.Key1;
ManiaAction specialAction = ManiaAction.Special1;
ManiaAction action = ManiaAction.Key1;
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(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.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
}
[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(1000, generated.Frames[frame_offset].Time, "Incorrect hit 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.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
}
[Test]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -14,6 +13,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -21,7 +21,10 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
{
private DrawableManiaEditorRuleset drawableRuleset;
private DrawableManiaEditorRuleset drawableRuleset = null!;
[Resolved]
private EditorScreenWithTimeline? screenWithTimeline { get; set; }
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -72,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Edit
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue;
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
if (current == null)
continue;
@ -83,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
protected override void Update()
{
base.Update();
if (screenWithTimeline?.TimelineArea.Timeline != null)
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
}
}
}

View File

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

View File

@ -1,49 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
public partial class ManiaSetupSection : RulesetSetupSection
{
private LabelledSwitchButton specialStyle;
public ManiaSetupSection()
: base(new ManiaRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.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
{
[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")]
Key1 = 10,
Key1,
[Description("Key 2")]
Key2,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,28 +17,9 @@ namespace osu.Game.Rulesets.Mania.Replays
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
private readonly ManiaAction[] columnActions;
public ManiaAutoGenerator(ManiaBeatmap 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()
@ -57,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Replays
switch (point)
{
case HitPoint:
actions.Add(columnActions[point.Column]);
actions.Add(ManiaAction.Key1 + point.Column);
break;
case ReleasePoint:
actions.Remove(columnActions[point.Column]);
actions.Remove(ManiaAction.Key1 + point.Column);
break;
}
}

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Replays;
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)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1;
var action = ManiaAction.Key1;
int activeColumns = (int)(legacyFrame.MouseX ?? 0);
int counter = 0;
while (activeColumns > 0)
{
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction);
Actions.Add(action);
if (isSpecial)
specialAction++;
else
normalAction++;
counter++;
action++;
activeColumns >>= 1;
}
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
int keys = 0;
foreach (var action in Actions)
{
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;
}
}
keys |= 1 << (int)action;
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,
RightKeys = rightKeys,
SpecialKey = InputKey.Space,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1,
}.GenerateKeyBindingsFor(variant, out _);
}.GenerateKeyBindingsFor(variant);
}
}

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.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring;
@ -26,6 +28,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
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:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)

View File

@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
// i dunno this looks about right??
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
if (sprite.DrawHeight > 0)
bodySprite.Scale = new Vector2(1, MathF.Max(1, scaleDirection * 32800 / sprite.DrawHeight));
bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight));
}
break;

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

@ -5,9 +5,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -78,6 +80,40 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
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:
return getResult(resultComponent.Component);

View File

@ -5,22 +5,12 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.UI
{
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);
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece

View File

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

View File

@ -66,13 +66,12 @@ namespace osu.Game.Rulesets.Mania.UI
Content = new[] { new Drawable[stageDefinitions.Count] }
});
var normalColumnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1;
var columnAction = ManiaAction.Key1;
int firstColumnIndex = 0;
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;

View File

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

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.UI
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;
Definition = definition;
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
RelativeSizeAxes = Axes.Both,
Width = 1,
Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ }
Action = { Value = columnStartAction++ }
};
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());

View File

@ -26,37 +26,30 @@ namespace osu.Game.Rulesets.Mania
public InputKey SpecialKey;
/// <summary>
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
/// The <see cref="ManiaAction"/> at which the columns should begin.
/// </summary>
public ManiaAction NormalActionStart;
/// <summary>
/// The <see cref="ManiaAction"/> for the special column.
/// </summary>
public ManiaAction SpecialAction;
public ManiaAction ActionStart;
/// <summary>
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
/// </summary>
/// <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>
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>();
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)
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
bindings.Add(new KeyBinding(SpecialKey, currentAction++));
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;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
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();
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
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();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
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();

View File

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

View File

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

View File

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

View File

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
base.PostProcess();
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList();
ApplyStacking(Beatmap);
}
internal static void ApplyStacking(IBeatmap beatmap)
{
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
if (hitObjects.Count > 0)
{
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects)
h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else
applyStackingOld(Beatmap.BeatmapInfo, hitObjects);
applyStackingOld(beatmap.BeatmapInfo, hitObjects);
}
}
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
}
private void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{
for (int i = 0; i < hitObjects.Count; i++)
{

View File

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

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)

View File

@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
}
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
}
}

View File

@ -67,5 +67,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return difficulty * DifficultyMultiplier;
}
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
}
}

View File

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

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -16,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected readonly HitCirclePiece CirclePiece;
private readonly HitCircleOverlapMarker marker;
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
public HitCircleSelectionBlueprint(HitCircle circle)
: base(circle)
@ -27,12 +31,32 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
};
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
}
protected override void LoadComplete()
{
base.LoadComplete();
showHitMarkers.BindValueChanged(_ =>
{
if (!showHitMarkers.Value)
DrawableObject.RestoreHitAnimations();
});
}
protected override void Update()
{
base.Update();
CirclePiece.UpdateFrom(HitObject);
marker.UpdateFrom(HitObject);
if (showHitMarkers.Value)
DrawableObject.SuppressHitAnimations();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);

View File

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

View File

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

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved(CanBeNull = true)]
[CanBeNull]
@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
placeNewControlPoint();
else
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
beginNewSegment(lastPoint);
break;
}
@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
private void beginNewSegment(PathControlPoint lastPoint)
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
usingCustomSegmentType = false;
}
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button != MouseButton.Left)
@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnMouseUp(e);
}
private static readonly PathType[] path_types =
[
PathType.LINEAR,
PathType.BEZIER,
PathType.PERFECT_CURVE,
PathType.BSpline(4),
];
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
if (state != SliderPlacementState.ControlPoints)
return false;
switch (e.Key)
{
case Key.S:
{
if (!canPlaceNewControlPoint(out _))
return false;
placeNewControlPoint();
var last = HitObject.Path.ControlPoints.Last(p => p != cursor);
beginNewSegment(last);
return true;
}
case Key.Number1:
case Key.Number2:
case Key.Number3:
case Key.Number4:
{
if (!e.AltPressed)
return false;
usingCustomSegmentType = true;
segmentStart.Type = path_types[e.Key - Key.Number1];
controlPointVisualiser.EnsureValidPathTypes();
return true;
}
case Key.Tab:
{
usingCustomSegmentType = true;
int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1;
if (currentTypeIndex < 0 && e.ShiftPressed)
currentTypeIndex = 0;
do
{
currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length;
segmentStart.Type = path_types[currentTypeIndex];
controlPointVisualiser.EnsureValidPathTypes();
} while (segmentStart.Type != path_types[currentTypeIndex]);
return true;
}
}
return false;
}
protected override void Update()
{
base.Update();
@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePathType()
{
if (usingCustomSegmentType)
{
controlPointVisualiser.EnsureValidPathTypes();
return;
}
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(4);
@ -286,8 +359,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
cursor.Position = getCursorPosition();
}
else if (cursor != null)
{
@ -301,6 +373,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
private Vector2 getCursorPosition()
{
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
/// <summary>
/// Whether a new control point can be placed at the current mouse position.
/// </summary>
@ -313,7 +391,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
lastPoint = last;
return lastPiece.IsHovered != true;
// We may only place a new control point if the cursor is not overlapping with the last control point.
// If snapping is enabled, the cursor may not hover the last piece while still placing the control point at the same position.
return !lastPiece.IsHovered && (last is null || Vector2.DistanceSquared(last.Position, getCursorPosition()) > 1f);
}
private void placeNewControlPoint()
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
private void updateSlider()

View File

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

View File

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

View File

@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
@ -99,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
updatePositionSnapGrid();
OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true);
RightToolbox.AddRange(new EditorToolboxGroup[]
RightToolbox.AddRange(new Drawable[]
{
OsuGridToolboxGroup,
new TransformToolboxGroup
@ -115,18 +116,45 @@ namespace osu.Game.Rulesets.Osu.Edit
);
}
private void updatePositionSnapGrid()
private void updatePositionSnapGrid(ValueChangedEvent<PositionSnapGridType> obj)
{
if (positionSnapGrid != null)
LayerBelowRuleset.Remove(positionSnapGrid, true);
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
switch (obj.NewValue)
{
case PositionSnapGridType.Square:
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = rectangularPositionSnapGrid;
positionSnapGrid = rectangularPositionSnapGrid;
break;
case PositionSnapGridType.Triangle:
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = triangularPositionSnapGrid;
break;
case PositionSnapGridType.Circle:
var circularPositionSnapGrid = new CircularPositionSnapGrid();
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
positionSnapGrid = circularPositionSnapGrid;
break;
default:
throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type.");
}
// Bind the start position to the toolbox sliders.
positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
positionSnapGrid.RelativeSizeAxes = Axes.Both;
LayerBelowRuleset.Add(positionSnapGrid);
@ -193,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
@ -203,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
@ -215,7 +243,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.RelativeGrids))
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
@ -226,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
if (snapType.HasFlagFast(SnapType.GlobalGrids))
if (snapType.HasFlag(SnapType.GlobalGrids))
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
@ -268,6 +296,12 @@ namespace osu.Game.Rulesets.Osu.Edit
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{
// if the snap target is a stacked object, snap to its unstacked position rather than its stacked position.
// this is intended to make working with stacks easier (because thanks to this, you can drag an object to any
// of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times).
if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero)
closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset);
// only return distance portion, since time is not really valid
snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true;

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuHitObjectInspector : HitObjectInspector
{
protected override void AddInspectorValues(HitObject[] objects)
{
base.AddInspectorValues(objects);
if (objects.Length > 0)
{
var firstInSelection = (OsuHitObject)objects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)objects.MaxBy(ho => ho.GetEndTime())!;
Debug.Assert(firstInSelection != null && lastInSelection != null);
var precedingObject = (OsuHitObject?)EditorBeatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() < firstInSelection.StartTime);
var nextObject = (OsuHitObject?)EditorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime > lastInSelection.GetEndTime());
if (precedingObject != null && precedingObject is not Spinner)
{
AddHeader("To previous");
AddValue($"{(firstInSelection.StackedPosition - precedingObject.StackedEndPosition).Length:#,0.##}px");
}
if (nextObject != null && nextObject is not Spinner)
{
AddHeader("To next");
AddValue($"{(nextObject.StackedPosition - lastInSelection.StackedEndPosition).Length:#,0.##}px");
}
}
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// this conditional is a rather ugly special case for stacks.
// as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around
// (they would stack and then unstack every frame).
// the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position
// which is unique to osu! due to stacking being applied as a post-processing step.
// therefore, the following loop would occur:
// - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully,
// the blueprint moves up the stack from its original drag position.
// - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack*
// to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position).
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
return true;
// this will potentially move the selection out of bounds...
foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
h.Position += localDelta;
// but this will be corrected.
moveSelectionInBounds();
// manually update stacking.
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
// as the entire flow is too expensive to run on every movement.
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
return true;
}

View File

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

View File

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

View File

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

View File

@ -1,56 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Osu.Edit.Setup
{
public partial class OsuSetupSection : RulesetSetupSection
{
private LabelledSliderBar<float> stackLeniency;
public OsuSetupSection()
: base(new OsuRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
{
stackLeniency = new LabelledSliderBar<float>
{
Label = "Stack Leniency",
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
{
Default = 0.7f,
MinValue = 0,
MaxValue = 1,
Precision = 0.1f
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
stackLeniency.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}

View File

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

View File

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

View File

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

View File

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

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