1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 04:19:53 +08:00

Compare commits

...

602 Commits

349 changed files with 10933 additions and 3496 deletions
+3 -2
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
+1 -1
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
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
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="3" />
</component>
</project>
-6
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>
-6
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>
+1 -1
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.
-1
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.
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.625.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.720.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -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;
-1
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>
@@ -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));
}
}
}
@@ -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);
});
}
@@ -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));
}
}
}
@@ -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();
+21 -15
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)
@@ -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)
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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)
};
}
@@ -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)
@@ -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;
+10 -10
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)
@@ -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;
});
}
}
}
@@ -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)));
}
}
}
@@ -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,
})
{
}
}
}
}
@@ -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]
@@ -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));
@@ -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();
@@ -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
@@ -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();
}
}
}
@@ -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;
}
}
}
@@ -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;
@@ -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();
}
}
}
+31 -30
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);
@@ -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)
{
@@ -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
});
}
}
}
}
@@ -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()
+3 -3
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;
@@ -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>
@@ -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);
}
}
@@ -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
};
}
@@ -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",
@@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
@@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
}
[Test]
@@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
}
[Test]
@@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(1, PathType.LINEAR);
}
[Test]
@@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
assertLength(100);
}
@@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(4);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
assertFinalControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(1, PathType.LINEAR);
}
[Test]
@@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.LINEAR);
assertFinalControlPointType(1, PathType.PERFECT_CURVE);
}
[Test]
@@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestManualPathTypeControlViaKeyboard()
{
addMovementStep(new Vector2(200));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(300, 200));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(300));
assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE);
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
assertControlPointTypeDuringPlacement(0, PathType.LINEAR);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.ShiftLeft);
});
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
AddStep("start new segment via S", () => InputManager.Key(Key.S));
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
addMovementStep(new Vector2(400, 300));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(400));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
@@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(808, tolerance: 10);
assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
assertControlPointType(4, null);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(1, null);
assertFinalControlPointType(2, null);
assertFinalControlPointType(3, null);
assertFinalControlPointType(4, null);
}
[Test]
@@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
assertControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, PathType.BSpline(4));
assertControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(1, PathType.BSpline(4));
assertFinalControlPointType(2, PathType.BSpline(4));
assertFinalControlPointType(3, null);
}
[Test]
@@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(0, PathType.BEZIER);
}
[Test]
@@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
}
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
@@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}",
() => this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type));
private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
internal readonly Container<PathControlPointPiece<T>> Pieces;
@@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (allowSelection)
d.RequestSelection = selectionRequested;
d.ControlPoint.Changed += controlPointChanged;
d.DragStarted = DragStarted;
d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded;
@@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
foreach (var point in e.OldItems.Cast<PathControlPoint>())
{
point.Changed -= controlPointChanged;
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately();
}
@@ -217,6 +220,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
private void controlPointChanged() => updateCurveMenuItems();
protected override bool OnClick(ClickEvent e)
{
if (Pieces.Any(piece => piece.IsHovered))
@@ -245,6 +250,86 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
}
// ReSharper disable once StaticMemberInGenericType
private static readonly PathType?[] path_types =
[
PathType.LINEAR,
PathType.BEZIER,
PathType.PERFECT_CURVE,
PathType.BSpline(4),
null,
];
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
switch (e.Key)
{
case Key.Tab:
{
var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray();
if (selectedPieces.Length != 1)
return false;
var selectedPiece = selectedPieces.Single();
var selectedPoint = selectedPiece.ControlPoint;
var validTypes = path_types;
if (selectedPoint == controlPoints[0])
validTypes = validTypes.Where(t => t != null).ToArray();
int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type);
if (currentTypeIndex < 0 && e.ShiftPressed)
currentTypeIndex = 0;
changeHandler?.BeginChange();
do
{
currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length;
updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]);
} while (selectedPoint.Type != validTypes[currentTypeIndex]);
changeHandler?.EndChange();
return true;
}
case Key.Number1:
case Key.Number2:
case Key.Number3:
case Key.Number4:
case Key.Number5:
{
if (!e.AltPressed)
return false;
var type = path_types[e.Key - Key.Number1];
if (Pieces[0].IsSelected.Value && type == null)
return false;
updatePathTypeOfSelectedPieces(type);
return true;
}
default:
return false;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var p in Pieces)
p.ControlPoint.Changed -= controlPointChanged;
}
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
{
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
@@ -254,30 +339,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
/// <summary>
/// Attempts to set the given control point piece to the given path type.
/// If that would fail, try to change the path such that it instead succeeds
/// Attempts to set all selected control point pieces to the given path type.
/// If that fails, try to change the path such that it instead succeeds
/// in a UX-friendly way.
/// </summary>
/// <param name="piece">The control point piece that we want to change the path type of.</param>
/// <param name="type">The path type we want to assign to the given control point piece.</param>
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
private void updatePathTypeOfSelectedPieces(PathType? type)
{
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
changeHandler?.BeginChange();
if (type?.Type == SplineType.PerfectCurve)
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
{
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
if (pointsInSegment.Count > thirdPointIndex + 1)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
if (type?.Type == SplineType.PerfectCurve)
{
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (pointsInSegment.Count > thirdPointIndex + 1)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
}
hitObject.Path.ExpectedDistance.Value = null;
p.ControlPoint.Type = type;
}
hitObject.Path.ExpectedDistance.Value = null;
piece.ControlPoint.Type = type;
EnsureValidPathTypes();
changeHandler?.EndChange();
}
[Resolved(CanBeNull = true)]
@@ -290,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints;
private List<MenuItem> curveTypeItems;
public void DragStarted(PathControlPoint controlPoint)
{
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
@@ -386,22 +481,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
int splittableCount = splittablePieces.Count;
List<MenuItem> curveTypeItems = new List<MenuItem>();
curveTypeItems = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0]))
foreach (PathType? type in path_types)
{
curveTypeItems.Add(createMenuItemForPathType(null));
curveTypeItems.Add(new OsuMenuItemSpacer());
// special inherit case
if (type == null)
{
if (selectedPieces.Contains(Pieces[0]))
continue;
curveTypeItems.Add(new OsuMenuItemSpacer());
}
curveTypeItems.Add(createMenuItemForPathType(type));
}
// todo: hide/disable items which aren't valid for selected points
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
{
curveTypeItems.Add(new OsuMenuItemSpacer());
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
}
var menuItems = new List<MenuItem>
{
@@ -424,35 +524,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
() => DeleteSelected())
);
updateCurveMenuItems();
return menuItems.ToArray();
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
}
}
private MenuItem createMenuItemForPathType(PathType? type)
private void updateCurveMenuItems()
{
int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
if (curveTypeItems == null)
return;
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
foreach (var item in curveTypeItems.OfType<CurveTypeMenuItem>())
{
changeHandler?.BeginChange();
int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == item.PathType);
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type);
if (countOfState == totalCount)
item.State.Value = TernaryState.True;
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
}
}
EnsureValidPathTypes();
private class CurveTypeMenuItem : TernaryStateRadioMenuItem
{
public readonly PathType? PathType;
changeHandler?.EndChange();
});
if (countOfState == totalCount)
item.State.Value = TernaryState.True;
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
return item;
public CurveTypeMenuItem(PathType? pathType, Action<TernaryState> action)
: base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action)
{
PathType = pathType;
}
}
}
}
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved(CanBeNull = true)]
[CanBeNull]
@@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
placeNewControlPoint();
else
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
beginNewSegment(lastPoint);
break;
}
@@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
private void beginNewSegment(PathControlPoint lastPoint)
{
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
usingCustomSegmentType = false;
}
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button != MouseButton.Left)
@@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnMouseUp(e);
}
private static readonly PathType[] path_types =
[
PathType.LINEAR,
PathType.BEZIER,
PathType.PERFECT_CURVE,
PathType.BSpline(4),
];
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
if (state != SliderPlacementState.ControlPoints)
return false;
switch (e.Key)
{
case Key.S:
{
if (!canPlaceNewControlPoint(out _))
return false;
placeNewControlPoint();
var last = HitObject.Path.ControlPoints.Last(p => p != cursor);
beginNewSegment(last);
return true;
}
case Key.Number1:
case Key.Number2:
case Key.Number3:
case Key.Number4:
{
if (!e.AltPressed)
return false;
usingCustomSegmentType = true;
segmentStart.Type = path_types[e.Key - Key.Number1];
controlPointVisualiser.EnsureValidPathTypes();
return true;
}
case Key.Tab:
{
usingCustomSegmentType = true;
int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1;
if (currentTypeIndex < 0 && e.ShiftPressed)
currentTypeIndex = 0;
do
{
currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length;
segmentStart.Type = path_types[currentTypeIndex];
controlPointVisualiser.EnsureValidPathTypes();
} while (segmentStart.Type != path_types[currentTypeIndex]);
return true;
}
}
return false;
}
protected override void Update()
{
base.Update();
@@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePathType()
{
if (usingCustomSegmentType)
{
controlPointVisualiser.EnsureValidPathTypes();
return;
}
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(4);
@@ -316,6 +389,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return lastPiece.IsHovered != true;
}
private void placeNewControlPoint()
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
private void updateSlider()
{
if (state == SliderPlacementState.Drawing)
@@ -55,7 +55,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; }
public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad;
public override Quad SelectionQuad
{
get
{
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
if (ControlPointVisualiser != null)
{
foreach (var piece in ControlPointVisualiser.Pieces)
result = RectangleF.Union(result, piece.ScreenSpaceDrawQuad.AABBFloat);
}
return result;
}
}
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
@@ -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,
}
}
@@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
@@ -99,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
updatePositionSnapGrid();
OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true);
RightToolbox.AddRange(new EditorToolboxGroup[]
RightToolbox.AddRange(new Drawable[]
{
OsuGridToolboxGroup,
new TransformToolboxGroup
@@ -114,18 +115,45 @@ namespace osu.Game.Rulesets.Osu.Edit
);
}
private void updatePositionSnapGrid()
private void updatePositionSnapGrid(ValueChangedEvent<PositionSnapGridType> obj)
{
if (positionSnapGrid != null)
LayerBelowRuleset.Remove(positionSnapGrid, true);
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
switch (obj.NewValue)
{
case PositionSnapGridType.Square:
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = rectangularPositionSnapGrid;
positionSnapGrid = rectangularPositionSnapGrid;
break;
case PositionSnapGridType.Triangle:
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = triangularPositionSnapGrid;
break;
case PositionSnapGridType.Circle:
var circularPositionSnapGrid = new CircularPositionSnapGrid();
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
positionSnapGrid = circularPositionSnapGrid;
break;
default:
throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type.");
}
// Bind the start position to the toolbox sliders.
positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
positionSnapGrid.RelativeSizeAxes = Axes.Both;
LayerBelowRuleset.Add(positionSnapGrid);
@@ -192,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
@@ -202,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
@@ -214,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.RelativeGrids))
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
@@ -225,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
if (snapType.HasFlagFast(SnapType.GlobalGrids))
if (snapType.HasFlag(SnapType.GlobalGrids))
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
@@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuHitObjectInspector : HitObjectInspector
{
protected override void AddInspectorValues()
{
base.AddInspectorValues();
if (EditorBeatmap.SelectedHitObjects.Count > 0)
{
var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!;
Debug.Assert(firstInSelection != null && lastInSelection != null);
var precedingObject = (OsuHitObject?)EditorBeatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() < firstInSelection.StartTime);
var nextObject = (OsuHitObject?)EditorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime > lastInSelection.GetEndTime());
if (precedingObject != null && precedingObject is not Spinner)
{
AddHeader("To previous");
AddValue($"{(firstInSelection.StackedPosition - precedingObject.StackedEndPosition).Length:#,0.##}px");
}
if (nextObject != null && nextObject is not Spinner)
{
AddHeader("To next");
AddValue($"{(nextObject.StackedPosition - lastInSelection.StackedEndPosition).Length:#,0.##}px");
}
}
}
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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);
@@ -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)
{
}
@@ -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;
@@ -328,15 +329,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
UpdateState(ArmedState.Idle);
UpdateComboColour();
// This method is called every frame. If we need to, the following can likely be converted
// to code which doesn't use transforms at all.
// This method is called every frame in editor contexts, thus the lack of need for transforms.
// Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
using (BeginAbsoluteSequence(StateUpdateTime - 5))
this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime));
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);
}
using (BeginAbsoluteSequence(HitStateUpdateTime))
this.FadeOut(700).Expire();
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()
@@ -5,19 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableOsuJudgement : DrawableJudgement
{
internal Color4 AccentColour { get; private set; }
internal SkinnableLighting Lighting { get; private set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private bool positionTransferred;
private Vector2 screenSpacePosition;
[BackgroundDependencyLoader]
private void load()
@@ -32,37 +36,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
public override void Apply(JudgementResult result, DrawableHitObject? judgedObject)
{
base.Apply(result, judgedObject);
if (judgedObject is not DrawableOsuHitObject osuObject)
return;
AccentColour = osuObject.AccentColour.Value;
switch (osuObject)
{
case DrawableSlider slider:
screenSpacePosition = slider.TailCircle.ToScreenSpace(slider.TailCircle.OriginPosition);
break;
default:
screenSpacePosition = osuObject.ToScreenSpace(osuObject.OriginPosition);
break;
}
Scale = new Vector2(osuObject.HitObject.Scale);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
positionTransferred = false;
}
protected override void Update()
{
base.Update();
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{
switch (osuObject)
{
case DrawableSlider slider:
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
break;
default:
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
break;
}
positionTransferred = true;
Scale = new Vector2(osuObject.HitObject.Scale);
}
Lighting.SetColourFrom(this, Result);
Position = Parent!.ToLocalSpace(screenSpacePosition);
}
protected override void ApplyHitAnimations()
@@ -382,7 +382,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit, force: true);
UpdateState(ArmedState.Hit);
HeadCircle.RestoreHitAnimations();
TailCircle.RestoreHitAnimations();
}
@@ -3,12 +3,12 @@
#nullable disable
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
@@ -135,11 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
UpdateState(ArmedState.Idle);
UpdateComboColour();
using (BeginAbsoluteSequence(StateUpdateTime - 5))
this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime));
// This method is called every frame in editor contexts, thus the lack of need for transforms.
using (BeginAbsoluteSequence(HitStateUpdateTime))
this.FadeOut(700).Expire();
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()
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
@@ -12,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
internal partial class SkinnableLighting : SkinnableSprite
{
private DrawableHitObject targetObject;
private JudgementResult targetResult;
private DrawableOsuJudgement? targetJudgement;
private JudgementResult? targetResult;
public SkinnableLighting()
: base("lighting")
@@ -29,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <summary>
/// Updates the lighting colour from a given hitobject and result.
/// </summary>
/// <param name="targetObject">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetObject"/> was judged with.</param>
public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult)
/// <param name="targetJudgement">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetJudgement"/> was judged with.</param>
public void SetColourFrom(DrawableOsuJudgement targetJudgement, JudgementResult? targetResult)
{
this.targetObject = targetObject;
this.targetJudgement = targetJudgement;
this.targetResult = targetResult;
updateColour();
@@ -41,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateColour()
{
if (targetObject == null || targetResult == null)
if (targetJudgement == null || targetResult == null)
Colour = Color4.White;
else
Colour = targetResult.IsHit ? targetObject.AccentColour.Value : Color4.Transparent;
Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent;
}
}
}
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects
{
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt
{
/// <summary>
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public const double PREEMPT_MAX = 1800;
public double TimePreempt = 600;
public double TimePreempt { get; set; } = 600;
public double TimeFadeIn = 400;
private HitObjectProperty<Vector2> position;
+23 -20
View File
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@@ -70,55 +69,55 @@ namespace osu.Game.Rulesets.Osu
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlagFast(LegacyMods.Nightcore))
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new OsuModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime))
else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new OsuModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect))
if (mods.HasFlag(LegacyMods.Perfect))
yield return new OsuModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new OsuModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Autopilot))
if (mods.HasFlag(LegacyMods.Autopilot))
yield return new OsuModAutopilot();
if (mods.HasFlagFast(LegacyMods.Cinema))
if (mods.HasFlag(LegacyMods.Cinema))
yield return new OsuModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay))
else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new OsuModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy))
if (mods.HasFlag(LegacyMods.Easy))
yield return new OsuModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight))
if (mods.HasFlag(LegacyMods.Flashlight))
yield return new OsuModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime))
if (mods.HasFlag(LegacyMods.HalfTime))
yield return new OsuModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock))
if (mods.HasFlag(LegacyMods.HardRock))
yield return new OsuModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden))
if (mods.HasFlag(LegacyMods.Hidden))
yield return new OsuModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail))
if (mods.HasFlag(LegacyMods.NoFail))
yield return new OsuModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax))
if (mods.HasFlag(LegacyMods.Relax))
yield return new OsuModRelax();
if (mods.HasFlagFast(LegacyMods.SpunOut))
if (mods.HasFlag(LegacyMods.SpunOut))
yield return new OsuModSpunOut();
if (mods.HasFlagFast(LegacyMods.Target))
if (mods.HasFlag(LegacyMods.Target))
yield return new OsuModTargetPractice();
if (mods.HasFlagFast(LegacyMods.TouchDevice))
if (mods.HasFlag(LegacyMods.TouchDevice))
yield return new OsuModTouchDevice();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
@@ -337,7 +336,11 @@ namespace osu.Game.Rulesets.Osu
};
}
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new OsuDifficultySection(),
new ColoursSection(),
];
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/>
+11 -10
View File
@@ -7,7 +7,6 @@ using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
@@ -194,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private void addPart(Vector2 screenSpacePosition)
{
parts[currentIndex].Position = screenSpacePosition;
parts[currentIndex].Position = ToLocalSpace(screenSpacePosition);
parts[currentIndex].Time = time + 1;
++parts[currentIndex].InvalidationID;
@@ -243,14 +242,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
originPosition = Vector2.Zero;
if (Source.TrailOrigin.HasFlagFast(Anchor.x1))
if (Source.TrailOrigin.HasFlag(Anchor.x1))
originPosition.X = 0.5f;
else if (Source.TrailOrigin.HasFlagFast(Anchor.x2))
else if (Source.TrailOrigin.HasFlag(Anchor.x2))
originPosition.X = 1f;
if (Source.TrailOrigin.HasFlagFast(Anchor.y1))
if (Source.TrailOrigin.HasFlag(Anchor.y1))
originPosition.Y = 0.5f;
else if (Source.TrailOrigin.HasFlagFast(Anchor.y2))
else if (Source.TrailOrigin.HasFlag(Anchor.y2))
originPosition.Y = 1f;
Source.parts.CopyTo(parts, 0);
@@ -286,9 +285,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1)
continue;
Vector2 screenSpacePos = Source.ToScreenSpace(part.Position);
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@@ -297,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@@ -306,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@@ -315,7 +316,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
+3
View File
@@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer();
public override Quad SkinnableComponentScreenSpaceDrawQuad => playfieldBorder.ScreenSpaceDrawQuad;
private readonly Container judgementAboveHitObjectLayer;
public OsuPlayfield()
+3 -1
View File
@@ -116,7 +116,9 @@ namespace osu.Game.Rulesets.Osu.UI
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
// When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score.
// To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events.
Schedule(() => ResumeRequested?.Invoke());
return true;
}
@@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,70 @@
// 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.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public partial class TestSceneTaikoEditorTestGameplay : EditorTestScene
{
protected override bool IsolateSavingFromDatabase => false;
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo importedBeatmapSet = null!;
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
base.SetUpSteps();
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
[Test]
public void TestBasicGameplayTest()
{
AddStep("add objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Swell { StartTime = 500, EndTime = 1500 });
EditorBeatmap.Add(new Hit { StartTime = 3000 });
});
AddStep("seek to 250", () => EditorClock.Seek(250));
AddUntilStep("wait for seek", () => EditorClock.CurrentTime, () => Is.EqualTo(250));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Screens.Edit.Editor);
}
}
}
@@ -315,10 +315,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
hitObjectContainer.Add(drawableSwell);
});
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
@@ -352,10 +349,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
hitObjectContainer.Add(drawableSwell);
});
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
@@ -11,5 +11,6 @@
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
</ItemGroup>
</Project>
@@ -1,93 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
public class Peaks : Skill
{
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
private const double colour_skill_multiplier = 0.375 * final_multiplier;
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
private const double final_multiplier = 0.0625;
private readonly Rhythm rhythm;
private readonly Colour colour;
private readonly Stamina stamina;
public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier;
public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier;
public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier;
public Peaks(Mod[] mods)
: base(mods)
{
rhythm = new Rhythm(mods);
colour = new Colour(mods);
stamina = new Stamina(mods);
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
public override void Process(DifficultyHitObject current)
{
rhythm.Process(current);
colour.Process(current);
stamina.Process(current);
}
/// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
public override double DifficultyValue()
{
List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderDescending())
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
}
}
@@ -23,6 +23,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
private const double difficulty_multiplier = 1.35;
private const double final_multiplier = 0.0625;
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
private const double colour_skill_multiplier = 0.375 * final_multiplier;
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
public override int Version => 20221107;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
@@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
return new Skill[]
{
new Peaks(mods)
new Rhythm(mods),
new Colour(mods),
new Stamina(mods)
};
}
@@ -72,13 +79,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
var combined = (Peaks)skills[0];
Colour colour = (Colour)skills.First(x => x is Colour);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
double colourRating = combined.ColourDifficultyValue * difficulty_multiplier;
double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier;
double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier;
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
double starRating = rescale(combinedRating * 1.4);
HitWindows hitWindows = new TaikoHitWindows();
@@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return 10.43 * Math.Log(sr / 8 + 1);
}
/// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
{
List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderDescending())
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
}
}
@@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Taiko.Edit.Setup
{
public partial class TaikoDifficultySection : SetupSection
{
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
healthDrainSlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
overallDifficultySlider = new LabelledSliderBar<float>
{
Label = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
}
},
baseVelocitySlider = new LabelledSliderBar<double>
{
Label = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
}
},
tickRateSlider = new LabelledSliderBar<double>
{
Label = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
}
},
};
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}
+1
View File
@@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick
{
StartTime = StartTime,
Samples = Samples
});
}
+21 -15
View File
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@@ -36,6 +35,8 @@ using osu.Game.Rulesets.Configuration;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.Edit.Setup;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Taiko
{
@@ -79,43 +80,43 @@ namespace osu.Game.Rulesets.Taiko
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlagFast(LegacyMods.Nightcore))
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new TaikoModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime))
else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new TaikoModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect))
if (mods.HasFlag(LegacyMods.Perfect))
yield return new TaikoModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new TaikoModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema))
if (mods.HasFlag(LegacyMods.Cinema))
yield return new TaikoModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay))
else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new TaikoModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy))
if (mods.HasFlag(LegacyMods.Easy))
yield return new TaikoModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight))
if (mods.HasFlag(LegacyMods.Flashlight))
yield return new TaikoModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime))
if (mods.HasFlag(LegacyMods.HalfTime))
yield return new TaikoModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock))
if (mods.HasFlag(LegacyMods.HardRock))
yield return new TaikoModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden))
if (mods.HasFlag(LegacyMods.Hidden))
yield return new TaikoModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail))
if (mods.HasFlag(LegacyMods.NoFail))
yield return new TaikoModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax))
if (mods.HasFlag(LegacyMods.Relax))
yield return new TaikoModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
@@ -189,6 +190,11 @@ namespace osu.Game.Rulesets.Taiko
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new TaikoDifficultySection(),
];
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
@@ -528,8 +528,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
// The control point at the end time of the slider should be applied
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
// The fourth object is a slider.
// `Samples` of a slider are presumed to control the volume of sounds that last the entire duration of the slider
// (such as ticks, slider slide sounds, etc.)
// Thus, the point of query of control points used for `Samples` is just beyond the start time of the slider.
Assert.AreEqual("Gameplay/soft-hitnormal11", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
// That said, the `NodeSamples` of the slider are responsible for the sounds of the slider's head / tail / repeats / large ticks etc.
// Therefore, they should be read at the time instant correspondent to the given node.
// This means that the tail should use bank 8 rather than 11.
Assert.AreEqual("Gameplay/soft-hitnormal11", ((ConvertSlider)hitObjects[4]).NodeSamples[0][0].LookupNames.First());
Assert.AreEqual("Gameplay/soft-hitnormal8", ((ConvertSlider)hitObjects[4]).NodeSamples[1][0].LookupNames.First());
}
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
@@ -157,8 +157,9 @@ namespace osu.Game.Tests.Database
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
[Test]
public void TestScoreUpgradeFailed()
[TestCase(30000002)]
[TestCase(30000013)]
public void TestScoreUpgradeFailed(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All<RulesetInfo>().First(),
})
{
TotalScoreVersion = 30000002,
TotalScoreVersion = scoreVersion,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
}
[Test]
@@ -168,12 +168,12 @@ namespace osu.Game.Tests.Database
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
// should only contain the modified beatmap (others purged).
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
realm.Run(r => r.Refresh());
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
@@ -479,6 +479,7 @@ namespace osu.Game.Tests.Database
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
@@ -496,7 +497,7 @@ namespace osu.Game.Tests.Database
Debug.Assert(importAfterUpdate != null);
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded).Within(TimeSpan.FromSeconds(1)));
});
}
@@ -71,6 +71,35 @@ namespace osu.Game.Tests.Database
}
}
[Test]
public void TestSubscriptionInitialChangeSetNull()
{
ChangeSet? firstChanges = null;
int receivedChangesCount = 0;
RunTestWithRealm((realm, _) =>
{
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
realm.Run(r => r.Refresh());
Assert.That(receivedChangesCount, Is.EqualTo(1));
Assert.That(firstChanges, Is.Null);
registration.Dispose();
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (receivedChangesCount == 0)
firstChanges = changes;
receivedChangesCount++;
}
}
[Test]
public void TestSubscriptionWithAsyncWrite()
{
@@ -0,0 +1,235 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckTitleMarkersTest
{
private CheckTitleMarkers check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckTitleMarkers();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Title = "Egao no Kanata",
TitleUnicode = "エガオノカナタ"
}
}
};
}
[Test]
public void TestNoTitleMarkers()
{
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestTvSizeMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (TV Size)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedTvSizeMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (tv size)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestGameVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Game Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Game Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedGameVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (game ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (game ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestShortVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Short Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Short Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedShortVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (short ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (short ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestSpedUpVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Sped Up Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedSpedUpVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (sped up ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestNightcoreMixMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Nightcore Mix)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore Mix)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedNightcoreMixMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (nightcore mix)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore mix)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestSpedUpCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Sped Up & Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up & Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedSpedUpCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (sped up & cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up & cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestNightcoreCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Nightcore & Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore & Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedNightcoreCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (nightcore & cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore & cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
private BeatmapVerifierContext getContext(IBeatmap beatmap)
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}
@@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class EditorTimestampParserTest
{
private static readonly object?[][] test_cases =
{
new object?[] { ":", false, null, null },
new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null },
new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null },
new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null },
new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null },
new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null },
new object?[] { "1:92", false, null, null },
new object?[] { "1:002", false, null, null },
new object?[] { "1:02:3", true, new TimeSpan(0, 0, 1, 2, 3), null },
new object?[] { "1:02:300", true, new TimeSpan(0, 0, 1, 2, 300), null },
new object?[] { "1:02:3000", false, null, null },
new object?[] { "1:02:300 ()", false, null, null },
new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
};
[TestCaseSource(nameof(test_cases))]
public void TestTryParse(string timestamp, bool expectedSuccess, TimeSpan? expectedParsedTime, string? expectedSelection)
{
bool actualSuccess = EditorTimestampParser.TryParse(timestamp, out var actualParsedTime, out string? actualSelection);
Assert.Multiple(() =>
{
Assert.That(actualSuccess, Is.EqualTo(expectedSuccess));
Assert.That(actualParsedTime, Is.EqualTo(expectedParsedTime));
Assert.That(actualSelection, Is.EqualTo(expectedSelection));
});
}
}
}
@@ -5,6 +5,8 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
@@ -19,10 +21,11 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
};
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -36,14 +39,15 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new Note { StartTime = 1000 },
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -57,15 +61,16 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -74,20 +79,67 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestHoldNote()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
HitObjects =
{
new HoldNote { StartTime = 1000, Duration = 10000 },
}
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Has.Count.EqualTo(0));
}
[Test]
public void TestHoldNoteWithOverlappingNote()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
HitObjects =
{
new HoldNote { StartTime = 1000, Duration = 10000 },
new Note { StartTime = 2000 },
new Note { StartTime = 12000 },
}
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Has.Count.EqualTo(0));
}
[Test]
public void TestTwoObjectsFarApart()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -106,20 +158,21 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
new BreakPeriod(1200, 4000),
new BreakPeriod(5200, 8000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -138,20 +191,21 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
new Note { StartTime = 9000 },
},
Breaks =
{
new BreakPeriod(1200, 8000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -172,19 +226,20 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1100 },
new Note { StartTime = 9000 },
},
Breaks =
{
new BreakPeriod(1200, 8000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -203,20 +258,21 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
new ManualBreakPeriod(1200, 4000),
new ManualBreakPeriod(5200, 8000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -237,20 +293,21 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
new Note { StartTime = 9000 },
},
Breaks =
{
new ManualBreakPeriod(1200, 8000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -271,19 +328,20 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
new ManualBreakPeriod(1200, 8800),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -302,19 +360,20 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
},
Breaks =
{
new BreakPeriod(10000, 15000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -328,19 +387,47 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
},
Breaks =
{
new ManualBreakPeriod(10000, 15000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestManualBreaksAtEndOfBeatmapAreRemovedCorrectlyEvenWithConcurrentObjects()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HoldNote { StartTime = 1000, EndTime = 20000 },
new HoldNote { StartTime = 2000, EndTime = 3000 },
},
Breaks =
{
new ManualBreakPeriod(10000, 15000),
}
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -354,19 +441,20 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
new Note { StartTime = 10000 },
new Note { StartTime = 11000 },
},
Breaks =
{
new BreakPeriod(0, 9000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -380,19 +468,20 @@ namespace osu.Game.Tests.Editing
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
new Note { StartTime = 10000 },
new Note { StartTime = 11000 },
},
Breaks =
{
new ManualBreakPeriod(0, 9000),
}
};
});
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
@@ -400,5 +489,55 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestTimePreemptIsRespected()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
Difficulty =
{
ApproachRate = 10,
},
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
}
});
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MIN));
});
beatmap.Difficulty.ApproachRate = 0;
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
});
}
}
}
@@ -22,7 +22,7 @@ namespace osu.Game.Tests.NonVisual
public class TestSceneTimedDifficultyCalculation
{
[Test]
public void TestAttributesGeneratedForAllNonSkippedObjects()
public void TestAttributesGeneratedForEachObjectOnce()
{
var beatmap = new Beatmap<TestHitObject>
{
@@ -40,15 +40,14 @@ namespace osu.Game.Tests.NonVisual
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(4));
Assert.That(attribs.Count, Is.EqualTo(3));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object.
assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestAttributesNotGeneratedForSkippedObjects()
public void TestAttributesGeneratedForSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
@@ -72,35 +71,14 @@ namespace osu.Game.Tests.NonVisual
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestNestedObjectOnlyAddsParentOnce()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject
{
StartTime = 1,
Skip = true,
Nested = 2
},
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(2));
Assert.That(attribs.Count, Is.EqualTo(3));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestSkippedLastObjectAddedInLastIteration()
public void TestAttributesGeneratedOnceForSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
@@ -110,6 +88,7 @@ namespace osu.Game.Tests.NonVisual
new TestHitObject
{
StartTime = 2,
Nested = 5,
Skip = true
},
new TestHitObject
@@ -122,8 +101,10 @@ namespace osu.Game.Tests.NonVisual
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
Assert.That(attribs.Count, Is.EqualTo(3));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected)
@@ -0,0 +1,22 @@
osu file format v128
[General]
SampleSet: Normal
[TimingPoints]
15,1000,4,1,0,100,1,0
2271,-100,4,1,0,5,0,0
6021,-100,4,1,0,100,0,0
8515,-100,4,1,0,5,0,0
12765,-100,4,1,0,100,0,0
14764,-100,4,1,0,5,0,0
14770,-100,4,1,0,50,0,0
17264,-100,4,1,0,5,0,0
17270,-100,4,1,0,50,0,0
22264,-100,4,1,0,100,0,0
[HitObjects]
113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0:
82,206,6015,2,0,L|457:204,1,350,0|0,2:0|2:0,2:0:0:0:
75,310,10265,2,0,L|435:312,1,350,0|0,3:0|3:0,3:0:0:0:
75,310,14764,2,0,L|435:312,3,350,0|0|0|0,3:0|3:0|3:0|3:0,3:0:0:0:
@@ -0,0 +1,185 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeCarousel : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
private readonly Bindable<Room> room = new Bindable<Room>(new Room());
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent))
{
Model = { BindTarget = room }
};
[Test]
public void TestBasicAppearance()
{
DailyChallengeCarousel carousel = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
carousel = new DailyChallengeCarousel
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (carousel.IsNotNull())
carousel.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (carousel.IsNotNull())
carousel.Height = height;
});
AddRepeatStep("add content", () => carousel.Add(new FakeContent()), 3);
}
[Test]
public void TestIntegration()
{
GridContainer grid = null!;
DailyChallengeEventFeed feed = null!;
DailyChallengeScoreBreakdown breakdown = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
grid = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RowDimensions =
[
new Dimension(),
new Dimension()
],
Content = new[]
{
new Drawable[]
{
new DailyChallengeCarousel
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new DailyChallengeTimeRemainingRing(),
breakdown = new DailyChallengeScoreBreakdown(),
}
}
},
[
feed = new DailyChallengeEventFeed
{
RelativeSizeAxes = Axes.Both,
}
],
}
},
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (grid.IsNotNull())
grid.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (grid.IsNotNull())
grid.Height = height;
});
AddSliderStep("update time remaining", 0f, 1f, 0f, progress =>
{
var startedTimeAgo = TimeSpan.FromHours(24) * progress;
room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo;
room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
});
AddStep("add normal score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
feed.AddNewScore(ev);
breakdown.AddNewScore(ev);
});
AddStep("add new user best", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), RNG.Next(1, 1000));
feed.AddNewScore(ev);
breakdown.AddNewScore(ev);
});
}
private partial class FakeContent : CompositeDrawable
{
private OsuSpriteText text = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1),
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Fake Content " + (char)('A' + RNG.Next(26)),
},
};
text.FadeOut(500, Easing.OutQuint)
.Then().FadeIn(500, Easing.OutQuint)
.Loop();
}
}
}
}
@@ -7,8 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.DailyChallenge
@@ -50,26 +52,41 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("add normal score", () =>
{
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null));
feed.AddNewScore(ev);
});
AddStep("add new user best", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), RNG.Next(11, 1000));
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000)));
feed.AddNewScore(ev);
});
AddStep("add top 10 score", () =>
{
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), RNG.Next(1, 10));
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 10)));
feed.AddNewScore(ev);
});
}
}
@@ -0,0 +1,142 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osuTK;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeLeaderboard : OsuTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicBehaviour()
{
DailyChallengeLeaderboard leaderboard = null!;
AddStep("set up response without user best", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is IndexPlaylistScoresRequest indexRequest)
{
indexRequest.TriggerSuccess(createResponse(50, false));
return true;
}
return false;
};
});
AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo))
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.8f),
});
AddStep("set up response with user best", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is IndexPlaylistScoresRequest indexRequest)
{
indexRequest.TriggerSuccess(createResponse(50, true));
return true;
}
return false;
};
});
AddStep("force refetch", () => leaderboard.RefetchScores());
}
[Test]
public void TestLoadingBehaviour()
{
IndexPlaylistScoresRequest pendingRequest = null!;
DailyChallengeLeaderboard leaderboard = null!;
AddStep("set up requests handler", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is IndexPlaylistScoresRequest indexRequest)
{
pendingRequest = indexRequest;
return true;
}
return false;
};
});
AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo))
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.8f),
});
AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(3, true)));
AddStep("force refetch", () => leaderboard.RefetchScores());
AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(4, true)));
}
private IndexedMultiplayerScores createResponse(int scoreCount, bool returnUserBest)
{
var result = new IndexedMultiplayerScores();
for (int i = 0; i < scoreCount; ++i)
{
result.Scores.Add(new MultiplayerScore
{
ID = i,
Accuracy = 1 - (float)i / (2 * scoreCount),
Position = i + 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH),
MaxCombo = 1000 - i,
TotalScore = (long)(1_000_000 * (1 - (float)i / (2 * scoreCount))),
User = new APIUser { Username = $"user {i}" },
Statistics = new Dictionary<HitResult, int>()
});
}
if (returnUserBest)
{
result.UserScore = new MultiplayerScore
{
ID = 99999,
Accuracy = 0.91,
Position = 4,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.A,
MaxCombo = 100,
TotalScore = 800000,
User = dummyAPI.LocalUser.Value,
Statistics = new Dictionary<HitResult, int>()
};
}
return result;
}
}
}
@@ -7,9 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Resources;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
namespace osu.Game.Tests.Visual.DailyChallenge
{
@@ -51,10 +52,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
AddStep("add new score", () =>
{
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
breakdown.AddNewScore(testScore);
breakdown.AddNewScore(ev);
});
}
}
@@ -12,7 +12,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Storyboards;
@@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestSwitchToDifficultyOfAnotherRuleset()
{
BeatmapInfo targetDifficulty = null;
AddAssert("ruleset is catch", () => Ruleset.Value.CreateInstance() is CatchRuleset);
AddStep("set taiko difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("ruleset switched to taiko", () => Ruleset.Value.CreateInstance() is TaikoRuleset);
AddStep("exit editor forcefully", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
private void switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)
@@ -1,10 +1,13 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
@@ -19,9 +22,10 @@ namespace osu.Game.Tests.Visual.Editing
[Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
public TestSceneEditorClock()
[SetUpSteps]
public void SetUpSteps()
{
Add(new FillFlowContainer
AddStep("create content", () => Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@@ -39,19 +43,17 @@ namespace osu.Game.Tests.Visual.Editing
Size = new Vector2(200, 100)
}
}
}));
AddStep("set working beatmap", () =>
{
Beatmap.Disabled = false;
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// ensure that music controller does not change this beatmap due to it
// completing naturally as part of the test.
Beatmap.Disabled = true;
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// ensure that music controller does not change this beatmap due to it
// completing naturally as part of the test.
Beatmap.Disabled = true;
}
[Test]
public void TestStopAtTrackEnd()
{
@@ -102,6 +104,29 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength));
}
[Test]
public void TestCurrentTimeDoubleTransform()
{
AddAssert("seek smoothly twice and current time is accurate", () =>
{
EditorClock.SeekSmoothlyTo(1000);
EditorClock.SeekSmoothlyTo(2000);
return 2000 == EditorClock.CurrentTimeAccurate;
});
}
[Test]
public void TestAdjustmentsRemovedOnDisposal()
{
AddStep("reset clock", () => EditorClock.Seek(0));
AddStep("set 0.25x speed", () => this.ChildrenOfType<OsuTabControl<double>>().First().Current.Value = 0.25);
AddAssert("track has 0.25x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
AddStep("dispose playback control", () => Clear(disposeChildren: true));
AddAssert("track has 1x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1));
}
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
@@ -193,5 +193,20 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save);
}
[Test]
public void TestBeatDivisor()
{
AddStep("Set custom beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().SetArbitraryDivisor(7));
SaveEditor();
AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash));
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7));
}
}
}
@@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
@@ -126,6 +127,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
}
[TestCase(2000)] // chosen to be after last object in the map
[TestCase(22000)] // chosen to be in the middle of the last spinner
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)
{
AddStep($"seek to end minus {offsetFromEnd}ms", () => EditorClock.Seek(importedBeatmapSet.MaxLength - offsetFromEnd));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestCancelGameplayTestWithUnsavedChanges()
{
@@ -206,6 +225,116 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
[Test]
public void TestAutoplayToggle()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Not.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestQuickPause()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.True);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestQuickExitAtInitialPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
AddWaitStep("wait some", 5);
AddStep("exit player", () => InputManager.PressKey(Key.F1));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
[Test]
public void TestQuickExitAtCurrentPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
AddWaitStep("wait some", 5);
AddStep("exit player", () => InputManager.PressKey(Key.F2));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000));
}
public override void TearDownSteps()
{
base.TearDownSteps();
@@ -402,6 +402,171 @@ namespace osu.Game.Tests.Visual.Editing
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
}
[Test]
public void PopoverForMultipleSelectionChangesAllSamples()
{
AddStep("add slider", () =>
{
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 1000,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples = new List<IList<HitSampleInfo>>
{
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
},
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
},
}
});
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
setBankViaPopover(HitSampleInfo.BANK_DRUM);
samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(2, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleNormalBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
setVolumeViaPopover(30);
samplePopoverHasSingleVolume(30);
hitObjectHasSampleVolume(0, 30);
hitObjectHasSampleVolume(1, 30);
hitObjectHasSampleVolume(2, 30);
hitObjectNodeHasSampleVolume(2, 0, 30);
hitObjectNodeHasSampleVolume(2, 1, 30);
toggleAdditionViaPopover(0);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(2, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(2, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_SOFT);
}
[Test]
public void TestHotkeysAffectNodeSamples()
{
AddStep("add slider", () =>
{
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 1000,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples = new List<IList<HitSampleInfo>>
{
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
},
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
},
}
});
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("add clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP);
AddStep("remove clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
AddStep("set drum bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LShift);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
}
[Test]
public void TestSelectingObjectDoesNotMutateSamples()
{
clickSamplePiece(0);
toggleAdditionViaPopover(1);
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
dismissPopover();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
}
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
@@ -5,11 +5,14 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
@@ -102,5 +105,120 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("change tool to circle", () => InputManager.Key(Key.Number2));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
[Test]
public void TestAutomaticBankAssignment()
{
AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
{
StartTime = 0,
Samples =
{
new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM, volume: 70),
}
}));
AddStep("seek to 500", () => EditorClock.Seek(500)); // previous object is the one at time 0
AddStep("enable automatic bank assignment", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single().Bank, () => Is.EqualTo(HitSampleInfo.BANK_SOFT));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
AddStep("seek to 250", () => EditorClock.Seek(250)); // previous object is the one at time 0
AddStep("enable clap addition", () => InputManager.Key(Key.R));
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[1].Samples, () => Has.Count.EqualTo(2));
AddAssert("normal sample has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank,
() => Is.EqualTo(HitSampleInfo.BANK_SOFT));
AddAssert("clap sample has drum bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_CLAP).Bank,
() => Is.EqualTo(HitSampleInfo.BANK_DRUM));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
AddStep("seek to 1000", () => EditorClock.Seek(1000)); // previous object is the one at time 500, which has no additions
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[3].Samples, () => Has.Count.EqualTo(2));
AddAssert("all samples have soft bank", () => EditorBeatmap.HitObjects[3].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[3].Samples.All(s => s.Volume == 70));
}
[Test]
public void TestVolumeIsInheritedFromLastObject()
{
AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
{
StartTime = 0,
Samples =
{
new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
}
}));
AddStep("seek to 500", () => EditorClock.Seek(500));
AddStep("select drum bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
}
[Test]
public void TestNodeSamplesAndSamplesAreSame()
{
Playfield playfield = null!;
AddStep("select drum bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("enable clap addition", () => InputManager.Key(Key.R));
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
AddStep("move mouse to top left of playfield", () =>
{
playfield = this.ChildrenOfType<Playfield>().Single();
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right of playfield", () =>
{
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("confirm via global action", () =>
{
globalActionContainer.TriggerPressed(GlobalAction.Select);
globalActionContainer.TriggerReleased(GlobalAction.Select);
});
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
AddAssert("slider samples have drum bank", () => EditorBeatmap.HitObjects[0].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
AddAssert("slider node samples have drum bank",
() => ((IHasRepeats)EditorBeatmap.HitObjects[0]).NodeSamples.SelectMany(s => s).All(s => s.Bank == HitSampleInfo.BANK_DRUM));
AddAssert("slider samples have clap addition",
() => EditorBeatmap.HitObjects[0].Samples.Select(s => s.Name), () => Does.Contain(HitSampleInfo.HIT_CLAP));
AddAssert("slider node samples have clap addition",
() => ((IHasRepeats)EditorBeatmap.HitObjects[0]).NodeSamples.All(samples => samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)));
}
}
}
@@ -70,6 +70,51 @@ namespace osu.Game.Tests.Visual.Editing
}));
}
[TestCaseSource(nameof(test_cases))]
public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation)
{
TriangularPositionSnapGrid grid = null;
AddStep("create grid", () =>
{
Child = grid = new TriangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing.X;
grid.GridLineRotation.Value = rotation;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
[TestCaseSource(nameof(test_cases))]
public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation)
{
CircularPositionSnapGrid grid = null;
AddStep("create grid", () =>
{
Child = grid = new CircularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing.X;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
private partial class SnappingCursorContainer : CompositeDrawable
{
public Func<Vector2, Vector2> GetSnapPosition;
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1);
AddAssert("preview time line should not show", () => !Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Any());
AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000);
AddAssert("preview time line should show", () => Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Single().Alpha == 1);
AddAssert("preview time line should show", () => Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Single().Alpha, () => Is.GreaterThan(0));
}
}
}
@@ -403,6 +403,28 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2));
}
[Test]
public void TestBreakRemoval()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 5000 },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddAssert("beatmap has one break", () => EditorBeatmap.Breaks, () => Has.Count.EqualTo(1));
AddStep("move mouse to break", () => InputManager.MoveMouseTo(this.ChildrenOfType<TimelineBreak>().Single()));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("move mouse to delete menu item", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuContextMenu>().First().ChildrenOfType<DrawableOsuMenuItem>().First()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("beatmap has no breaks", () => EditorBeatmap.Breaks, () => Is.Empty);
AddAssert("break piece went away", () => this.ChildrenOfType<TimelineBreak>().Count(), () => Is.Zero);
}
private void assertSelectionIs(IEnumerable<HitObject> hitObjects)
=> AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
}
@@ -99,6 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Scheduler.AddDelayed(applyMiss, 500 + 30);
});
AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks);
}
[Test]
@@ -120,6 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
});
AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks);
}
[Test]
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay
return true;
});
AddAssert("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value);
AddUntilStep("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value);
// because we are in frame stable context, it's quite likely that not all samples are "played" at this point.
// the important thing is that at least one started, and that sample has since stopped.
@@ -0,0 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneJudgementContainer : OsuTestScene
{
private JudgementContainer<DrawableOsuJudgement> judgementContainer = null!;
[SetUpSteps]
public void SetUp()
{
AddStep("create judgement container", () => Child = judgementContainer = new JudgementContainer<DrawableOsuJudgement>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestJudgementFromSameHitObjectIsRemoved()
{
DrawableHitCircle drawableHitCircle1 = null!;
DrawableHitCircle drawableHitCircle2 = null!;
AddStep("create hit circles", () =>
{
Add(drawableHitCircle1 = new DrawableHitCircle(createHitCircle()));
Add(drawableHitCircle2 = new DrawableHitCircle(createHitCircle()));
});
int judgementCount = 0;
AddStep("judge the same hitobject twice via different drawables", () =>
{
addDrawableJudgement(drawableHitCircle1);
drawableHitCircle2.Apply(drawableHitCircle1.HitObject);
addDrawableJudgement(drawableHitCircle2);
judgementCount = judgementContainer.Count;
});
AddAssert("one judgement in container", () => judgementCount, () => Is.EqualTo(1));
}
[Test]
public void TestJudgementFromDifferentHitObjectIsNotRemoved()
{
DrawableHitCircle drawableHitCircle = null!;
AddStep("create hit circle", () => Add(drawableHitCircle = new DrawableHitCircle(createHitCircle())));
int judgementCount = 0;
AddStep("judge two hitobjects via the same drawable", () =>
{
addDrawableJudgement(drawableHitCircle);
drawableHitCircle.Apply(createHitCircle());
addDrawableJudgement(drawableHitCircle);
judgementCount = judgementContainer.Count;
});
AddAssert("two judgements in container", () => judgementCount, () => Is.EqualTo(2));
}
private void addDrawableJudgement(DrawableHitObject drawableHitObject)
{
var judgement = new DrawableOsuJudgement();
judgement.Apply(new JudgementResult(drawableHitObject.HitObject, new OsuJudgement())
{
Type = HitResult.Great,
TimeOffset = Time.Current
}, drawableHitObject);
judgementContainer.Add(judgement);
}
private HitCircle createHitCircle()
{
var circle = new HitCircle();
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return circle;
}
}
}

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