1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 19:22:54 +08:00

Merge branch 'master' into add-missing-author-links

This commit is contained in:
Dean Herbert 2021-05-07 14:32:44 +09:00
commit 165f443ab5
236 changed files with 5653 additions and 1647 deletions

View File

@ -25,6 +25,6 @@ Please check:
*please attach logs here, which are located at:*
- `%AppData%/osu/logs` *(on Windows),*
- `~/.local/share/osu/logs` *(on Linux & macOS).*
- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
- `Android/data/sh.ppy.osulazer/files/logs` *(on Android)*,
- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
-->

46
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,46 @@
version: 2
updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: monthly
time: "17:00"
open-pull-requests-limit: 99
ignore:
- dependency-name: Microsoft.EntityFrameworkCore.Design
versions:
- "> 2.2.6"
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
versions:
- "> 2.2.6"
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite.Core
versions:
- "> 2.2.6"
- dependency-name: Microsoft.Extensions.DependencyInjection
versions:
- ">= 5.a, < 6"
- dependency-name: NUnit3TestAdapter
versions:
- ">= 3.16.a, < 3.17"
- dependency-name: Microsoft.NET.Test.Sdk
versions:
- 16.9.1
- dependency-name: Microsoft.Extensions.DependencyInjection
versions:
- 3.1.11
- 3.1.12
- dependency-name: Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson
versions:
- 3.1.11
- dependency-name: Microsoft.NETCore.Targets
versions:
- 5.0.0
- dependency-name: Microsoft.AspNetCore.SignalR.Protocols.MessagePack
versions:
- 5.0.2
- dependency-name: NUnit
versions:
- 3.13.1
- dependency-name: Microsoft.AspNetCore.SignalR.Client
versions:
- 3.1.11

View File

@ -24,7 +24,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* the in-game logs, which are located at:
* `%AppData%/osu/logs` (on Windows),
* `~/.local/share/osu/logs` (on Linux and macOS),
* `Android/Data/sh.ppy.osulazer/logs` (on Android),
* `Android/data/sh.ppy.osulazer/files/logs` (on Android),
* on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
* your system specifications (including the operating system and platform you are playing on),
* a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug),

View File

@ -17,7 +17,7 @@ The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commo
This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.416.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.506.0" />
</ItemGroup>
</Project>

View File

@ -9,6 +9,7 @@ using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Security;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
@ -113,6 +114,8 @@ namespace osu.Desktop
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)

View File

@ -0,0 +1,83 @@
// 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.Security.Principal;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
namespace osu.Desktop.Security
{
/// <summary>
/// Checks if the game is running with elevated privileges (as admin in Windows, root in Unix) and displays a warning notification if so.
/// </summary>
public class ElevatedPrivilegesChecker : Component
{
[Resolved]
private NotificationOverlay notifications { get; set; }
private bool elevated;
[BackgroundDependencyLoader]
private void load()
{
elevated = checkElevated();
}
protected override void LoadComplete()
{
base.LoadComplete();
if (elevated)
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 class ElevatedPrivilegesNotification : SimpleNotification
{
public override bool IsImportant => true;
public ElevatedPrivilegesNotification()
{
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, NotificationOverlay notificationOverlay)
{
Icon = FontAwesome.Solid.ShieldAlt;
IconBackgound.Colour = colours.YellowDark;
}
}
}
}

View File

@ -25,6 +25,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="5.0.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />

View File

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="nunit" Version="3.13.1" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup>

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
@ -20,6 +21,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
@ -170,16 +172,25 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
public void TestCatcherStacking()
public void TestCatcherRandomStacking()
{
AddStep("catch more fruits", () => attemptCatch(() => new Fruit
{
X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One)
}, 50));
}
[Test]
public void TestCatcherStackingSameCaughtPosition()
{
AddStep("catch fruit", () => attemptCatch(new Fruit()));
checkPlate(1);
AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
catcher.CaughtObjects.Any(obj => obj.Y < -20));
catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y < -25));
}
[Test]
@ -189,11 +200,11 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1);
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
AddStep("explode", () => catcher.Explode());
AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
AddStep("catch fruits", () => attemptCatch(new Fruit(), 10));
AddStep("catch fruits", () => attemptCatch(() => new Fruit(), 10));
AddStep("drop", () => catcher.Drop());
AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
}
@ -222,10 +233,15 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
private void attemptCatch(CatchHitObject hitObject, int count = 1)
private void attemptCatch(CatchHitObject hitObject)
{
attemptCatch(() => hitObject, 1);
}
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
{
for (var i = 0; i < count; i++)
attemptCatch(hitObject, out _, out _);
attemptCatch(hitObject(), out _, out _);
}
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)

View File

@ -8,6 +8,8 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
@ -31,12 +33,32 @@ namespace osu.Game.Rulesets.Catch.Tests
private float circleSize;
private ScheduledDelegate addManyFruit;
private BeatmapDifficulty beatmapDifficulty;
public TestSceneCatcherArea()
{
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
AddStep("catch many random fruit", () =>
{
int count = 50;
addManyFruit?.Cancel();
addManyFruit = Scheduler.AddDelayed(() =>
{
attemptCatch(new Fruit
{
X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty) * 0.6f,
});
if (count-- == 0)
addManyFruit?.Cancel();
}, 50, true);
});
AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true }));
AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit()));
AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true }));
@ -45,10 +67,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(Fruit fruit)
{
fruit.X = fruit.OriginalX + catcher.X;
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
{
CircleSize = circleSize
});
fruit.ApplyDefaults(new ControlPointInfo(), beatmapDifficulty);
foreach (var area in this.ChildrenOfType<CatcherArea>())
{
@ -71,6 +90,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
circleSize = size;
beatmapDifficulty = new BeatmapDifficulty
{
CircleSize = circleSize
};
SetContents(() =>
{
var droppedObjectContainer = new Container<CaughtObject>
@ -84,7 +108,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Children = new Drawable[]
{
droppedObjectContainer,
new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size })
new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.Catch
return new Mod[]
{
new CatchModDifficultyAdjust(),
new CatchModClassic(),
};
case ModType.Automation:
@ -126,7 +127,8 @@ namespace osu.Game.Rulesets.Catch
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp(), new ModWindDown())
new MultiMod(new ModWindUp(), new ModWindDown()),
new CatchModFloatingFruits()
};
default:

View File

@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModClassic : ModClassic
{
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModFloatingFruits : Mod, IApplicableToDrawableRuleset<CatchHitObject>
{
public override string Name => "Floating Fruits";
public override string Acronym => "FF";
public override string Description => "The fruits are... floating?";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{
drawableRuleset.Anchor = Anchor.Centre;
drawableRuleset.Origin = Anchor.Centre;
drawableRuleset.Scale = new Vector2(1, -1);
}
}
}

View File

@ -53,6 +53,16 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public const double BASE_SPEED = 1.0;
/// <summary>
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
/// </summary>
public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
/// <summary>
/// The amount by which caught fruit should be scaled down to fit on the plate.
/// </summary>
private const float caught_fruit_scale_adjust = 0.5f;
[NotNull]
private readonly Container trailsTarget;
@ -202,13 +212,13 @@ namespace osu.Game.Rulesets.Catch.UI
/// Calculates the width of the area used for attempting catches in gameplay.
/// </summary>
/// <param name="scale">The scale of the catcher.</param>
internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
public static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
/// <summary>
/// Calculates the width of the area used for attempting catches in gameplay.
/// </summary>
/// <param name="difficulty">The beatmap difficulty.</param>
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
@ -240,7 +250,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (result.IsHit)
{
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X);
if (CatchFruitOnPlate)
placeCaughtObject(palpableObject, positionInStack);
@ -384,16 +394,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
updateTrailVisibility();
if (hyperDashing)
{
this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
else
{
this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
@ -479,7 +480,7 @@ namespace osu.Game.Rulesets.Catch.UI
caughtObject.CopyStateFrom(drawableObject);
caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Position = position;
caughtObject.Scale /= 2;
caughtObject.Scale *= caught_fruit_scale_adjust;
caughtObjectContainer.Add(caughtObject);
@ -489,19 +490,21 @@ namespace osu.Game.Rulesets.Catch.UI
private Vector2 computePositionInStack(Vector2 position, float displayRadius)
{
const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
const float allowance = 10;
// this is taken from osu-stable (lenience should be 10 * 10 at standard scale).
const float lenience_adjust = 10 / CatchHitObject.OBJECT_RADIUS;
while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2)))
float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2);
// offset fruit vertically to better place "above" the plate.
position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
float diff = (displayRadius + radius_div_2) / allowance;
position.X += (RNG.NextSingle() - 0.5f) * diff * 2;
position.Y -= RNG.NextSingle() * diff;
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
position.Y -= RNG.NextSingle(0, 5);
}
position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
return position;
}

View File

@ -324,6 +324,33 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Ok);
}
[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 assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);

View File

@ -0,0 +1,80 @@
// 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.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Tests.Visual;
using osu.Framework.Timing;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestSceneTimingBasedNoteColouring : OsuTestScene
{
[Resolved]
private RulesetConfigCache configCache { get; set; }
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>();
protected override void LoadComplete()
{
const double beat_length = 500;
var ruleset = new ManiaRuleset();
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
{
HitObjects =
{
new Note { StartTime = 0 },
new Note { StartTime = beat_length / 16 },
new Note { StartTime = beat_length / 12 },
new Note { StartTime = beat_length / 8 },
new Note { StartTime = beat_length / 6 },
new Note { StartTime = beat_length / 4 },
new Note { StartTime = beat_length / 3 },
new Note { StartTime = beat_length / 2 },
new Note { StartTime = beat_length }
},
ControlPointInfo = new ControlPointInfo(),
BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
};
foreach (var note in beatmap.HitObjects)
{
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}
beatmap.ControlPointInfo.Add(0, new TimingControlPoint
{
BeatLength = beat_length
});
Child = new Container
{
Clock = new FramedClock(new ManualClock()),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
ruleset.CreateDrawableRulesetWith(beatmap)
}
};
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
}
}
}

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@ -34,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
ScrollTime,
ScrollDirection
ScrollDirection,
TimingBasedNoteColouring
}
}

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return;
base.OnMouseUp(e);
EndPlacement(true);
EndPlacement(HitObject.Duration > 0);
}
private double originalStartTime;

View File

@ -12,16 +12,16 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Edit
{
public class DrawableManiaEditRuleset : DrawableManiaRuleset
public class DrawableManiaEditorRuleset : DrawableManiaRuleset
{
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
public DrawableManiaEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{
}
protected override Playfield CreatePlayfield() => new ManiaEditPlayfield(Beatmap.Stages)
protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -4,6 +4,7 @@
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit.Compose.Components;
@ -30,6 +31,6 @@ namespace osu.Game.Rulesets.Mania.Edit
return base.CreateBlueprintFor(hitObject);
}
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
}
}

View File

@ -7,9 +7,9 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaEditPlayfield : ManiaPlayfield
public class ManiaEditorPlayfield : ManiaPlayfield
{
public ManiaEditPlayfield(List<StageDefinition> stages)
public ManiaEditorPlayfield(List<StageDefinition> stages)
: base(stages)
{
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>
{
private DrawableManiaEditRuleset drawableRuleset;
private DrawableManiaEditorRuleset drawableRuleset;
private ManiaBeatSnapGrid beatSnapGrid;
private InputManager inputManager;
@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{
drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
dependencies.CacheAs(drawableRuleset.ScrollingInfo);

View File

@ -7,12 +7,13 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaSelectionHandler : SelectionHandler
public class ManiaSelectionHandler : EditorSelectionHandler
{
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private HitObjectComposer composer { get; set; }
public override bool HandleMovement(MoveSelectionEvent moveEvent)
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
@ -30,11 +31,11 @@ namespace osu.Game.Rulesets.Mania.Edit
return true;
}
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent)
{
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
if (currentColumn == null)
return;

View File

@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
new ManiaModClassic(),
new ManiaModInvert(),
new ManiaModConstantSpeed()
};

View File

@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Mania
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5
},
new SettingsCheckbox
{
LabelText = "Timing-based note colouring",
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
}
};
}

View File

@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModClassic : ModClassic
{
}
}

View File

@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// As the note is being held, adjust the size of the sizing container. This has two effects:
// 1. The contained masking container will mask the body and ticks.
// 2. The head note will move along with the new "head position" in the container.
if (Head.IsHit && releaseTime == null)
if (Head.IsHit && releaseTime == null && DrawHeight > 0)
{
// How far past the hit target this hold note is. Always a positive value.
float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);

View File

@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@ -17,6 +23,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction>
{
[Resolved]
private OsuColour colours { get; set; }
[Resolved(canBeNull: true)]
private IBeatmap beatmap { get; set; }
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>();
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
private readonly Drawable headPiece;
@ -34,6 +48,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
[BackgroundDependencyLoader(true)]
private void load(ManiaRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
}
protected override void LoadComplete()
{
HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour());
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true);
}
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{
base.OnDirectionChanged(e);
@ -73,5 +99,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public virtual void OnReleased(ManiaAction action)
{
}
private void updateSnapColour()
{
if (beatmap == null) return;
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
Colour = configTimingBasedNoteColouring.Value ? BindableBeatDivisor.GetColourFor(snapDivisor, colours) : Color4.White;
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
@ -30,25 +31,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
[Test]
public void TestCircleInCenter()
{
var beatmap = new Beatmap<HitObject>
assertOk(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = playfield_centre // Playfield is 640 x 480.
Position = playfield_centre
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
});
}
[Test]
public void TestCircleNearEdge()
{
var beatmap = new Beatmap<HitObject>
assertOk(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -58,15 +57,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(5, 5)
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
});
}
[Test]
public void TestCircleNearEdgeStackedOffscreen()
{
var beatmap = new Beatmap<HitObject>
assertOffscreenCircle(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -77,15 +74,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
StackHeight = 5
}
}
};
assertOffscreenCircle(beatmap);
});
}
[Test]
public void TestCircleOffscreen()
{
var beatmap = new Beatmap<HitObject>
assertOffscreenCircle(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -95,15 +90,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(0, 0)
}
}
};
assertOffscreenCircle(beatmap);
});
}
[Test]
public void TestSliderInCenter()
{
var beatmap = new Beatmap<HitObject>
assertOk(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -118,15 +111,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}),
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
});
}
[Test]
public void TestSliderNearEdge()
{
var beatmap = new Beatmap<HitObject>
assertOk(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -141,15 +132,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}),
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
});
}
[Test]
public void TestSliderNearEdgeStackedOffscreen()
{
var beatmap = new Beatmap<HitObject>
assertOffscreenSlider(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -165,15 +154,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
StackHeight = 5
}
}
};
assertOffscreenSlider(beatmap);
});
}
[Test]
public void TestSliderOffscreenStart()
{
var beatmap = new Beatmap<HitObject>
assertOffscreenSlider(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -188,15 +175,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}),
}
}
};
assertOffscreenSlider(beatmap);
});
}
[Test]
public void TestSliderOffscreenEnd()
{
var beatmap = new Beatmap<HitObject>
assertOffscreenSlider(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -211,15 +196,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}),
}
}
};
assertOffscreenSlider(beatmap);
});
}
[Test]
public void TestSliderOffscreenPath()
{
var beatmap = new Beatmap<HitObject>
assertOffscreenSlider(new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
@ -236,14 +219,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}),
}
}
};
});
}
assertOffscreenSlider(beatmap);
private void assertOk(IBeatmap beatmap)
{
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
}
private void assertOffscreenCircle(IBeatmap beatmap)
{
var issues = check.Run(beatmap).ToList();
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
@ -251,7 +237,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
private void assertOffscreenSlider(IBeatmap beatmap)
{
var issues = check.Run(beatmap).ToList();
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Position = new Vector2(128, 128),
ComboIndex = 1,
}), null));
})));
}
private HitCircle prepareObject(HitCircle circle)

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(300, 0),
}),
RepeatCount = 1
}), null));
})));
}
[Test]

View File

@ -34,6 +34,18 @@ namespace osu.Game.Rulesets.Osu.Tests
private List<JudgementResult> judgementResults;
[Test]
public void TestPressBothKeysSimultaneouslyAndReleaseOne()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
AddAssert("Tracking retained", assertMaxJudge);
}
/// <summary>
/// Scenario:
/// - Press a key before a slider starts

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
ComboIndex = 1,
Duration = 1000,
}), null));
})));
AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
}

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0);
AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public abstract class OsuSelectionBlueprint<T> : OverlaySelectionBlueprint
where T : OsuHitObject
{
protected new T HitObject => (T)DrawableObject.HitObject;
protected T HitObject => (T)DrawableObject.HitObject;
protected override bool AlwaysShowWhenSelected => true;

View File

@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
lastPoint = last;
return lastPiece?.IsHovered != true;
return lastPiece.IsHovered != true;
}
private void updateSlider()

View File

@ -31,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
new IssueTemplateOffscreenSlider(this)
};
public IEnumerable<Issue> Run(IBeatmap beatmap)
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{
foreach (var hitobject in beatmap.HitObjects)
foreach (var hitobject in playableBeatmap.HitObjects)
{
switch (hitobject)
{

View File

@ -1,79 +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.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditRuleset : DrawableOsuRuleset
{
public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{
}
protected override Playfield CreatePlayfield() => new OsuEditPlayfield();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One };
private class OsuEditPlayfield : OsuPlayfield
{
protected override GameplayCursorContainer CreateCursor() => null;
protected override void OnNewDrawableHitObject(DrawableHitObject d)
{
d.ApplyCustomUpdateState += updateState;
}
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
}
}
}

View File

@ -0,0 +1,102 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
{
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{
}
protected override Playfield CreatePlayfield() => new OsuEditorPlayfield();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One };
private class OsuEditorPlayfield : OsuPlayfield
{
private Bindable<bool> hitAnimations;
protected override GameplayCursorContainer CreateCursor() => null;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations);
}
protected override void OnNewDrawableHitObject(DrawableHitObject d)
{
d.ApplyCustomUpdateState += updateState;
}
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle || hitAnimations.Value)
return;
if (hitObject is DrawableHitCircle circle)
{
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
.Expire();
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
}
if (hitObject is IHasMainCirclePiece mainPieceContainer)
{
// clear any explode animation logic.
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
}
if (hitObject is DrawableSliderRepeat repeat)
{
repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
}
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
switch (hitObject)
{
case DrawableSlider _:
case DrawableHitCircle _:
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
break;
}
}
}
}
}

View File

@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit
new CheckOffscreenObjects()
};
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
{
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
=> new DrawableOsuEditRuleset(ruleset, beatmap, mods);
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (b.IsSelected)
continue;
var hitObject = (OsuHitObject)b.HitObject;
var hitObject = (OsuHitObject)b.Item;
Vector2? snap = checkSnap(hitObject.Position);
if (snap == null && hitObject.Position != hitObject.EndPosition)

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@ -15,7 +16,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
public class OsuSelectionHandler : EditorSelectionHandler
{
protected override void OnSelectionChanged()
{
@ -36,13 +37,13 @@ namespace osu.Game.Rulesets.Osu.Edit
referencePathTypes = null;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent)
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var hitObjects = selectedMovableObjects;
// this will potentially move the selection out of bounds...
foreach (var h in hitObjects)
h.Position += moveEvent.InstantDelta;
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// but this will be corrected.
moveSelectionInBounds();
@ -374,8 +375,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
.OfType<OsuHitObject>()
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();

View File

@ -1,47 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using System.Collections.Generic;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObjects
{
[SettingSource("Roll speed", "Rotations per minute")]
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
MinValue = 0.02,
MaxValue = 4,
Precision = 0.01,
};
[SettingSource("Direction", "The direction of rotation")]
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
public override string Name => "Barrel Roll";
public override string Acronym => "BR";
public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public void Update(Playfield playfield)
{
playfield.Rotation = (Direction.Value == RotationDirection.CounterClockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// scale the playfield to allow all hitobjects to stay within the visible region.
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
foreach (var d in drawables)
{
d.OnUpdate += _ =>
{
switch (d)
{
case DrawableHitCircle circle:
circle.CirclePiece.Rotation = -CurrentRotation;
break;
}
};
}
}
}
}

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -16,22 +15,8 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Classic";
public override string Acronym => "CL";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.History;
public override string Description => "Feeling nostalgic?";
public override bool Ranked => false;
public override ModType Type => ModType.Conversion;
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Mods
var osuObject = (OsuHitObject)drawable.HitObject;
Vector2 origin = drawable.Position;
// Wiggle the repeat points with the slider instead of independently.
// Wiggle the repeat points and the tail with the slider instead of independently.
// Also fixes an issue with repeat points being positioned incorrectly.
if (osuObject is SliderRepeat)
if (osuObject is SliderRepeat || osuObject is SliderTailCircle)
return;
Random objRand = new Random((int)osuObject.StartTime);

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Entry = null;
}
private void onEntryInvalidated() => refreshPoints();
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
private void refreshPoints()
{

View File

@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableHitCircle : DrawableOsuHitObject
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece
{
public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
@ -66,7 +66,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()),
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
ApproachCircle = new ApproachCircle
{
Alpha = 0,
@ -178,6 +182,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut();
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
switch (state)
{
case ArmedState.Idle:

View File

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IHasMainCirclePiece
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
@ -26,9 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private double animDuration;
public Drawable CirclePiece { get; private set; }
public SkinnableDrawable CirclePiece { get; private set; }
public ReverseArrowPiece Arrow { get; private set; }
private Drawable scaleContainer;
private ReverseArrowPiece arrow;
public override bool DisplayResult => false;
@ -53,11 +55,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
Children = new Drawable[]
{
// no default for this; only visible in legacy skins.
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
arrow = new ReverseArrowPiece(),
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
Arrow = new ReverseArrowPiece(),
}
};
@ -91,6 +97,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
switch (state)
{
case ArmedState.Idle:
@ -102,8 +110,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
case ArmedState.Hit:
this.FadeOut(animDuration, Easing.Out)
.ScaleTo(Scale * 1.5f, animDuration, Easing.Out);
this.FadeOut(animDuration, Easing.Out);
const float final_scale = 1.5f;
Arrow.ScaleTo(Scale * final_scale, animDuration, Easing.Out);
CirclePiece.ScaleTo(Scale * final_scale, animDuration, Easing.Out);
break;
}
}
@ -139,18 +151,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
while (Math.Abs(aimRotation - arrow.Rotation) > 180)
aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
if (!hasRotation)
{
arrow.Rotation = aimRotation;
Arrow.Rotation = aimRotation;
hasRotation = true;
}
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}

View File

@ -7,12 +7,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking, IHasMainCirclePiece
{
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@ -34,7 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
private SkinnableDrawable circlePiece;
public SkinnableDrawable CirclePiece { get; private set; }
private Container scaleContainer;
public DrawableSliderTail()
@ -63,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Children = new Drawable[]
{
// no default for this; only visible in legacy skins.
circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
};
@ -75,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateInitialTransforms();
circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateHitStateTransforms(ArmedState state)
@ -84,6 +86,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Debug.Assert(HitObject.HitWindows != null);
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
switch (state)
{
case ArmedState.Idle:

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public interface IHasMainCirclePiece
{
SkinnableDrawable CirclePiece { get; }
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public interface IMainCirclePiece
{
/// <summary>
/// Begins animating this <see cref="IMainCirclePiece"/>.
/// </summary>
/// <param name="state">The <see cref="ArmedState"/> of the related <see cref="DrawableHitCircle"/>.</param>
void Animate(ArmedState state);
}
}

View File

@ -13,7 +13,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class MainCirclePiece : CompositeDrawable
public class MainCirclePiece : CompositeDrawable, IMainCirclePiece
{
private readonly CirclePiece circle;
private readonly RingPiece ring;
@ -67,12 +67,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
}
private void updateState(DrawableHitObject drawableObject, ArmedState state)
public void Animate(ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
glow.FadeOut(400);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -134,6 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// </summary>
private double? timeToAcceptAnyKeyAfter;
/// <summary>
/// The actions that were pressed in the previous frame.
/// </summary>
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
protected override void Update()
{
base.Update();
@ -152,8 +158,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
// we can return to accepting all keys if the initial head circle key is the *only* key pressed, or all keys have been released.
if (actions?.Contains(otherKey) != true)
// we can start accepting any key once all other keys have been released in the previous frame.
if (!lastPressedActions.Contains(otherKey))
timeToAcceptAnyKeyAfter = Time.Current;
}
@ -164,6 +170,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
// valid action
(actions?.Any(isValidTrackingAction) ?? false);
lastPressedActions.Clear();
if (actions != null)
lastPressedActions.AddRange(actions);
}
/// <summary>

View File

@ -12,6 +12,7 @@ using osu.Game.Graphics.Sprites;
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;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -19,7 +20,7 @@ using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyMainCirclePiece : CompositeDrawable
public class LegacyMainCirclePiece : CompositeDrawable, IMainCirclePiece
{
private readonly string priorityLookup;
private readonly bool hasNumber;
@ -138,12 +139,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
}
private void updateState(DrawableHitObject drawableObject, ArmedState state)
public void Animate(ArmedState state)
{
const double legacy_fade_duration = 240;

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI
base.PopIn();
GameplayCursor.ActiveCursor.Hide();
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
cursorScaleContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre);
clickToResumeCursor.Appear();
if (localCursorContainer == null)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
StartTime = 400,
Major = true
}), null));
})));
AddHitObject(barLine);
RemoveHitObject(barLine);
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
StartTime = 200,
Major = false
}), null));
})));
AddHitObject(barLine);
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Duration = 500,
IsStrong = false,
TickRate = 2
}), null));
})));
AddHitObject(drumRoll);
RemoveHitObject(drumRoll);
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Duration = 400,
IsStrong = true,
TickRate = 16
}), null));
})));
AddHitObject(drumRoll);
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Type = HitType.Rim,
IsStrong = false,
StartTime = 300
}), null));
})));
AddHitObject(hit);
RemoveHitObject(hit);
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Type = HitType.Centre,
IsStrong = true,
StartTime = 500
}), null));
})));
AddHitObject(hit);
}

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Compose.Components;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TaikoSelectionHandler();
public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) =>
new TaikoSelectionBlueprint(hitObject);

View File

@ -8,12 +8,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class TaikoSelectionHandler : SelectionHandler
public class TaikoSelectionHandler : EditorSelectionHandler
{
private readonly Bindable<TernaryState> selectionRimState = new Bindable<TernaryState>();
private readonly Bindable<TernaryState> selectionStrongState = new Bindable<TernaryState>();
@ -72,16 +73,19 @@ namespace osu.Game.Rulesets.Taiko.Edit
});
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{
if (selection.All(s => s.HitObject is Hit))
if (selection.All(s => s.Item is Hit))
yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } };
if (selection.All(s => s.HitObject is TaikoHitObject))
if (selection.All(s => s.Item is TaikoHitObject))
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
protected override void UpdateTernaryStates()
{

View File

@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModClassic : ModClassic
{
}
}

View File

@ -135,6 +135,7 @@ namespace osu.Game.Rulesets.Taiko
{
new TaikoModRandom(),
new TaikoModDifficultyAdjust(),
new TaikoModClassic(),
};
case ModType.Automation:

View File

@ -168,6 +168,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,114 @@
// 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 Moq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckAudioQualityTest
{
private CheckAudioQuality check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckAudioQuality();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
}
};
}
[Test]
public void TestMissing()
{
// While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.AudioFile = null;
var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
mock.SetupGet(w => w.Track).Returns((Track)null);
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestAcceptable()
{
var mock = getMockWorkingBeatmap(192);
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestNullBitrate()
{
var mock = getMockWorkingBeatmap(null);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
}
[Test]
public void TestZeroBitrate()
{
var mock = getMockWorkingBeatmap(0);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
}
[Test]
public void TestTooHighBitrate()
{
var mock = getMockWorkingBeatmap(320);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test]
public void TestTooLowBitrate()
{
var mock = getMockWorkingBeatmap(64);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
}
/// <summary>
/// Returns the mock of the working beatmap with the given audio properties.
/// </summary>
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate)
{
var mockTrack = new Mock<Track>();
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
return mockWorkingBeatmap;
}
}
}

View File

@ -0,0 +1,130 @@
// 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.IO;
using System.Linq;
using JetBrains.Annotations;
using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckBackgroundQualityTest
{
private CheckBackgroundQuality check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckBackgroundQuality();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo
{
Hash = "abcdef"
}
}
})
}
}
};
}
[Test]
public void TestMissing()
{
// While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.BackgroundFile = null;
var mock = getMockWorkingBeatmap(null, System.Array.Empty<byte>());
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestAcceptable()
{
var mock = getMockWorkingBeatmap(new Texture(1920, 1080));
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestTooHighResolution()
{
var mock = getMockWorkingBeatmap(new Texture(3840, 2160));
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooHighResolution);
}
[Test]
public void TestLowResolution()
{
var mock = getMockWorkingBeatmap(new Texture(640, 480));
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateLowResolution);
}
[Test]
public void TestTooLowResolution()
{
var mock = getMockWorkingBeatmap(new Texture(100, 100));
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooLowResolution);
}
[Test]
public void TestTooUncompressed()
{
var mock = getMockWorkingBeatmap(new Texture(1920, 1080), new byte[1024 * 1024 * 3]);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed);
}
/// <summary>
/// Returns the mock of the working beatmap with the given background and filesize.
/// </summary>
/// <param name="background">The texture of the background.</param>
/// <param name="fileBytes">The bytes that represent the background file.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null)
{
var stream = new MemoryStream(fileBytes ?? new byte[1024 * 1024]);
var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
mock.SetupGet(w => w.Background).Returns(background);
mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
return mock;
}
}
}

View File

@ -0,0 +1,194 @@
// 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 Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckConcurrentObjectsTest
{
private CheckConcurrentObjects check;
[SetUp]
public void Setup()
{
check = new CheckConcurrentObjects();
}
[Test]
public void TestCirclesSeparate()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 150 }
});
}
[Test]
public void TestCirclesConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 100 }
});
}
[Test]
public void TestCirclesAlmostConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 101 }
});
}
[Test]
public void TestSlidersSeparate()
{
assertOk(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 500, endTime: 900.75d).Object
});
}
[Test]
public void TestSlidersConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 300, endTime: 700.75d).Object
});
}
[Test]
public void TestSlidersAlmostConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 402, endTime: 902.75d).Object
});
}
[Test]
public void TestSliderAndCircleConcurrent()
{
assertConcurrentDifferent(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
new HitCircle { StartTime = 300 }
});
}
[Test]
public void TestManyObjectsConcurrent()
{
var hitobjects = new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 200, endTime: 500.75d).Object,
new HitCircle { StartTime = 300 }
};
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(3));
Assert.That(issues.Where(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent).ToList(), Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
}
[Test]
public void TestHoldNotesSeparateOnSameColumn()
{
assertOk(new List<HitObject>
{
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
getHoldNoteMock(startTime: 500, endTime: 900.75d, column: 1).Object
});
}
[Test]
public void TestHoldNotesConcurrentOnDifferentColumns()
{
assertOk(new List<HitObject>
{
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
getHoldNoteMock(startTime: 300, endTime: 700.75d, column: 2).Object
});
}
[Test]
public void TestHoldNotesConcurrentOnSameColumn()
{
assertConcurrentSame(new List<HitObject>
{
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
getHoldNoteMock(startTime: 300, endTime: 700.75d, column: 1).Object
});
}
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
{
var mock = new Mock<Slider>();
mock.SetupGet(s => s.StartTime).Returns(startTime);
mock.As<IHasRepeats>().Setup(r => r.RepeatCount).Returns(repeats);
mock.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
return mock;
}
private Mock<HoldNote> getHoldNoteMock(double startTime, double endTime, int column)
{
var mock = new Mock<HoldNote>();
mock.SetupGet(s => s.StartTime).Returns(startTime);
mock.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
mock.As<IHasColumn>().Setup(c => c.Column).Returns(column);
return mock;
}
private void assertOk(List<HitObject> hitobjects)
{
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
}
private void assertConcurrentSame(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
}
private void assertConcurrentDifferent(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent));
}
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
{
return new Beatmap<HitObject>
{
HitObjects = hitobjects
};
}
}
}

View File

@ -5,21 +5,23 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.IO;
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 CheckBackgroundTest
public class CheckFilePresenceTest
{
private CheckBackground check;
private CheckBackgroundPresence check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckBackground();
check = new CheckBackgroundPresence();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
@ -29,7 +31,11 @@ namespace osu.Game.Tests.Editing.Checks
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo { Filename = "abc123.jpg" }
new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
@ -39,7 +45,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestBackgroundSetAndInFiles()
{
Assert.That(check.Run(beatmap), Is.Empty);
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
}
[Test]
@ -47,10 +53,10 @@ namespace osu.Game.Tests.Editing.Checks
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(beatmap).ToList();
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateDoesNotExist);
Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateDoesNotExist);
}
[Test]
@ -58,10 +64,10 @@ namespace osu.Game.Tests.Editing.Checks
{
beatmap.Metadata.BackgroundFile = null;
var issues = check.Run(beatmap).ToList();
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateNoneSet);
Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateNoneSet);
}
}
}

View File

@ -0,0 +1,155 @@
// 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 Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckUnsnappedObjectsTest
{
private CheckUnsnappedObjects check;
private ControlPointInfo cpi;
[SetUp]
public void Setup()
{
check = new CheckUnsnappedObjects();
cpi = new ControlPointInfo();
cpi.Add(100, new TimingControlPoint { BeatLength = 100 });
}
[Test]
public void TestCircleSnapped()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 100 }
});
}
[Test]
public void TestCircleUnsnapped1Ms()
{
assert1Ms(new List<HitObject>
{
new HitCircle { StartTime = 101 }
});
assert1Ms(new List<HitObject>
{
new HitCircle { StartTime = 99 }
});
}
[Test]
public void TestCircleUnsnapped2Ms()
{
assert2Ms(new List<HitObject>
{
new HitCircle { StartTime = 102 }
});
assert2Ms(new List<HitObject>
{
new HitCircle { StartTime = 98 }
});
}
[Test]
public void TestSliderSnapped()
{
// Slider ends are naturally < 1 ms unsnapped because of how SV works.
assertOk(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object
});
}
[Test]
public void TestSliderUnsnapped1Ms()
{
assert1Ms(new List<HitObject>
{
getSliderMock(startTime: 101, endTime: 401.75d).Object
}, count: 2);
// End is only off by 0.25 ms, hence count 1.
assert1Ms(new List<HitObject>
{
getSliderMock(startTime: 99, endTime: 399.75d).Object
}, count: 1);
}
[Test]
public void TestSliderUnsnapped2Ms()
{
assert2Ms(new List<HitObject>
{
getSliderMock(startTime: 102, endTime: 402.75d).Object
}, count: 2);
// Start and end are 2 ms and 1.25 ms off respectively, hence two different issues in one object.
var hitobjects = new List<HitObject>
{
getSliderMock(startTime: 98, endTime: 398.75d).Object
};
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
}
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
{
var mockSlider = new Mock<Slider>();
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
mockSlider.As<IHasRepeats>().Setup(r => r.RepeatCount).Returns(repeats);
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
return mockSlider;
}
private void assertOk(List<HitObject> hitobjects)
{
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
}
private void assert1Ms(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
}
private void assert2Ms(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
}
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
{
return new Beatmap<HitObject>
{
ControlPointInfo = cpi,
HitObjects = hitobjects
};
}
}
}

View File

@ -0,0 +1,94 @@
// 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.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneDrawableHitObject : OsuTestScene
{
[Test]
public void TestEntryLifetime()
{
TestDrawableHitObject dho = null;
var initialHitObject = new HitObject
{
StartTime = 1000
};
var entry = new TestLifetimeEntry(new HitObject
{
StartTime = 2000
});
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject(initialHitObject));
AddAssert("Correct initial lifetime", () => dho.LifetimeStart == initialHitObject.StartTime - TestDrawableHitObject.INITIAL_LIFETIME_OFFSET);
AddStep("Apply entry", () => dho.Apply(entry));
AddAssert("Correct initial lifetime", () => dho.LifetimeStart == entry.HitObject.StartTime - TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
AddStep("Set lifetime", () => dho.LifetimeEnd = 3000);
AddAssert("Entry lifetime is updated", () => entry.LifetimeEnd == 3000);
}
[Test]
public void TestKeepAlive()
{
TestDrawableHitObject dho = null;
TestLifetimeEntry entry = null;
AddStep("Create DHO", () =>
{
dho = new TestDrawableHitObject(null);
dho.Apply(entry = new TestLifetimeEntry(new HitObject())
{
LifetimeStart = 0,
LifetimeEnd = 1000,
});
Child = dho;
});
AddStep("KeepAlive = true", () => entry.KeepAlive = true);
AddAssert("Lifetime is overriden", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == double.MaxValue);
AddStep("Set LifetimeStart", () => dho.LifetimeStart = 500);
AddStep("KeepAlive = false", () => entry.KeepAlive = false);
AddAssert("Lifetime is correct", () => entry.LifetimeStart == 500 && entry.LifetimeEnd == 1000);
AddStep("Set LifetimeStart while KeepAlive", () =>
{
entry.KeepAlive = true;
dho.LifetimeStart = double.MinValue;
entry.KeepAlive = false;
});
AddAssert("Lifetime is changed", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == 1000);
}
private class TestDrawableHitObject : DrawableHitObject
{
public const double INITIAL_LIFETIME_OFFSET = 100;
protected override double InitialLifetimeOffset => INITIAL_LIFETIME_OFFSET;
public TestDrawableHitObject(HitObject hitObject)
: base(hitObject)
{
}
}
private class TestLifetimeEntry : HitObjectLifetimeEntry
{
public const double INITIAL_LIFETIME_OFFSET = 200;
protected override double InitialLifetimeOffset => INITIAL_LIFETIME_OFFSET;
public TestLifetimeEntry(HitObject hitObject)
: base(hitObject)
{
}
}
}
}

View File

@ -1,32 +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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneGameplayClockContainer : OsuTestScene
{
[Test]
public void TestStartThenElapsedTime()
{
GameplayClockContainer gcc = null;
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gcc = new GameplayClockContainer(working, 0));
});
AddStep("start track", () => gcc.Start());
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneMasterGameplayClockContainer : OsuTestScene
{
[Test]
public void TestStartThenElapsedTime()
{
GameplayClockContainer gcc = null;
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gcc = new MasterGameplayClockContainer(working, 0));
});
AddStep("start clock", () => gcc.Start());
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
}
[Test]
public void TestElapseThenReset()
{
GameplayClockContainer gcc = null;
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gcc = new MasterGameplayClockContainer(working, 0));
});
AddStep("start clock", () => gcc.Start());
AddUntilStep("current time greater 2000", () => gcc.GameplayClock.CurrentTime > 2000);
double timeAtReset = 0;
AddStep("reset clock", () =>
{
timeAtReset = gcc.GameplayClock.CurrentTime;
gcc.Reset();
});
AddAssert("current time < time at reset", () => gcc.GameplayClock.CurrentTime < timeAtReset);
}
}
}

View File

@ -20,6 +20,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@ -67,17 +68,47 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gameplayContainer = new GameplayClockContainer(working, 0));
gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)
{
Clock = gameplayContainer.GameplayClock
IsPaused = { Value = true },
Child = new FrameStabilityContainer
{
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
}
});
});
AddStep("reset clock", () => gameplayContainer.Start());
AddUntilStep("sample played", () => sample.RequestedPlaying);
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
}
[Test]
public void TestSampleHasLifetimeEndWithInitialClockTime()
{
GameplayClockContainer gameplayContainer = null;
DrawableStoryboardSample sample = null;
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)
{
IsPaused = { Value = true },
Child = new FrameStabilityContainer
{
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
}
});
});
AddStep("start time", () => gameplayContainer.Start());
AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue);
AddUntilStep("sample not played", () => !sample.RequestedPlaying);
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
}
[TestCase(typeof(OsuModDoubleTime), 1.5)]
@ -114,7 +145,7 @@ namespace osu.Game.Tests.Gameplay
var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)
Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
{
Child = beatmapSkinSourceContainer
});

View File

@ -4,7 +4,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
namespace osu.Game.Tests.Mods
{
@ -26,6 +29,16 @@ namespace osu.Game.Tests.Mods
Assert.That(orderedSettings[3].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.UnorderedSetting)));
}
[Test]
public void TestCustomControl()
{
var objectWithCustomSettingControl = new ClassWithCustomSettingControl();
var settings = objectWithCustomSettingControl.CreateSettingsControls().ToArray();
Assert.That(settings, Has.Length.EqualTo(1));
Assert.That(settings[0], Is.TypeOf<CustomSettingsControl>());
}
private class ClassWithSettings
{
[SettingSource("Unordered setting", "Should be last")]
@ -40,5 +53,21 @@ namespace osu.Game.Tests.Mods
[SettingSource("Third setting", "Yet another description", 3)]
public BindableInt ThirdSetting { get; set; } = new BindableInt();
}
private class ClassWithCustomSettingControl
{
[SettingSource("Custom setting", "Should be a custom control", SettingControlType = typeof(CustomSettingsControl))]
public BindableInt UnorderedSetting { get; set; } = new BindableInt();
}
private class CustomSettingsControl : SettingsItem<int>
{
protected override Drawable CreateControl() => new CustomControl();
private class CustomControl : Drawable, IHasCurrentValue<int>
{
public Bindable<int> Current { get; set; } = new Bindable<int>();
}
}
}
}

View File

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.NonVisual
{
public class ClosestBeatDivisorTest
{
[Test]
public void TestExactDivisors()
{
var cpi = new ControlPointInfo();
cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 });
double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 };
assertClosestDivisors(divisors, divisors, cpi);
}
[Test]
public void TestExactDivisorWithTempoChanges()
{
int offset = 0;
int[] beatLengths = { 1000, 200, 100, 50 };
var cpi = new ControlPointInfo();
foreach (int beatLength in beatLengths)
{
cpi.Add(offset, new TimingControlPoint { BeatLength = beatLength });
offset += beatLength * 2;
}
double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3 };
assertClosestDivisors(divisors, divisors, cpi);
}
[Test]
public void TestExactDivisorsHighBPMStream()
{
var cpi = new ControlPointInfo();
cpi.Add(-50, new TimingControlPoint { BeatLength = 50 }); // 1200 BPM 1/4 (limit testing)
// A 1/4 stream should land on 1/1, 1/2 and 1/4 divisors.
double[] divisors = { 4, 4, 4, 4, 4, 4, 4, 4 };
double[] closestDivisors = { 4, 2, 4, 1, 4, 2, 4, 1 };
assertClosestDivisors(divisors, closestDivisors, cpi, step: 1 / 4d);
}
[Test]
public void TestApproximateDivisors()
{
var cpi = new ControlPointInfo();
cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 });
double[] divisors = { 3.03d, 0.97d, 14, 13, 7.94d, 6.08d, 3.93d, 2.96d, 2.02d, 64 };
double[] closestDivisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 };
assertClosestDivisors(divisors, closestDivisors, cpi);
}
private void assertClosestDivisors(IReadOnlyList<double> divisors, IReadOnlyList<double> closestDivisors, ControlPointInfo cpi, double step = 1)
{
List<HitObject> hitobjects = new List<HitObject>();
double offset = cpi.TimingPoints[0].Time;
for (int i = 0; i < divisors.Count; ++i)
{
double beatLength = cpi.TimingPointAt(offset).BeatLength;
hitobjects.Add(new HitObject { StartTime = offset + beatLength / divisors[i] });
offset += beatLength * step;
}
var beatmap = new Beatmap
{
HitObjects = hitobjects,
ControlPointInfo = cpi
};
for (int i = 0; i < divisors.Count; ++i)
Assert.AreEqual(closestDivisors[i], beatmap.ControlPointInfo.GetClosestBeatDivisor(beatmap.HitObjects[i].StartTime), $"at index {i}");
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
@ -278,6 +279,54 @@ namespace osu.Game.Tests.NonVisual
setTime(-100, -100);
}
[Test]
public void TestReplayFramesSortStability()
{
const double repeating_time = 5000;
// add a collection of frames in shuffled order time-wise; each frame also stores its original index to check stability later.
// data is hand-picked and breaks if the unstable List<T>.Sort() is used.
// in theory this can still return a false-positive with another unstable algorithm if extremely unlucky,
// but there is no conceivable fool-proof way to prevent that anyways.
replay.Frames.AddRange(new[]
{
repeating_time,
0,
3000,
repeating_time,
repeating_time,
6000,
9000,
repeating_time,
repeating_time,
1000,
11000,
21000,
4000,
repeating_time,
repeating_time,
8000,
2000,
7000,
repeating_time,
repeating_time,
10000
}.Select((time, index) => new TestReplayFrame(time, true, index)));
replay.HasReceivedAllFrames = true;
// create a new handler with the replay for the sort to be performed.
handler = new TestInputHandler(replay);
// ensure sort stability by checking that the frames with time == repeating_time are sorted in ascending frame index order themselves.
var repeatingTimeFramesData = replay.Frames
.Cast<TestReplayFrame>()
.Where(f => f.Time == repeating_time)
.Select(f => f.FrameIndex);
Assert.That(repeatingTimeFramesData, Is.Ordered.Ascending);
}
private void setReplayFrames()
{
replay.Frames = new List<ReplayFrame>
@ -324,11 +373,13 @@ namespace osu.Game.Tests.NonVisual
private class TestReplayFrame : ReplayFrame
{
public readonly bool IsImportant;
public readonly int FrameIndex;
public TestReplayFrame(double time, bool isImportant = false)
public TestReplayFrame(double time, bool isImportant = false, int frameIndex = 0)
: base(time)
{
IsImportant = isImportant;
FrameIndex = frameIndex;
}
}

View File

@ -41,6 +41,32 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
checkPlayingUserCount(0);
}
[Test]
public void TestPlayingUsersUpdatedOnJoin()
{
AddStep("leave room", () => Client.LeaveRoom());
AddUntilStep("wait for room part", () => Client.Room == null);
AddStep("create room initially in gameplay", () =>
{
Room.RoomID.Value = null;
Client.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
room.Users.Add(new MultiplayerRoomUser(55)
{
User = new User { Id = 55 },
State = MultiplayerUserState.Playing
});
};
RoomManager.CreateRoom(Room);
});
AddUntilStep("wait for room join", () => Client.Room != null);
checkPlayingUserCount(1);
}
private void checkPlayingUserCount(int expectedCount)
=> AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount);

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
@ -51,7 +50,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
};
scoreProcessor.ApplyResult(judgementResult);
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
}
/// <summary>
@ -84,8 +83,8 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
@ -96,8 +95,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points)
// TODO: The following two cases don't match expectations currently (a single hit is registered in acc portion when it shouldn't be). See https://github.com/ppy/osu/issues/12604.
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 330)] // (1 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 450)] // (1 * 1 * 300) * (1 + 0 / 25) + 3 * 50 (bonus points)
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
scoreProcessor.ApplyResult(judgementResult);
}
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
}
/// <remarks>
@ -158,7 +158,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
};
scoreProcessor.ApplyResult(lastJudgementResult);
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
}
[Test]
@ -169,7 +169,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value));
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
}
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
@ -287,6 +287,23 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
}
[TestCase(HitResult.Perfect, 1_000_000)]
[TestCase(HitResult.SmallTickHit, 1_000_000)]
[TestCase(HitResult.LargeTickHit, 1_000_000)]
[TestCase(HitResult.SmallBonus, 1_000_000 + Judgement.SMALL_BONUS_SCORE)]
[TestCase(HitResult.LargeBonus, 1_000_000 + Judgement.LARGE_BONUS_SCORE)]
public void TestGetScoreWithExternalStatistics(HitResult result, int expectedScore)
{
var statistic = new Dictionary<HitResult, int> { { result, 1 } };
scoreProcessor.ApplyBeatmap(new Beatmap
{
HitObjects = { new TestHitObject(result) }
});
Assert.That(scoreProcessor.GetImmediateScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d));
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }

View File

@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.Editing
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
private EditorBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
[Test]
public void TestSelectedObjectHasPriorityWhenOverlapping()

View File

@ -132,8 +132,8 @@ namespace osu.Game.Tests.Visual.Editing
{
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionHandler>().First().Alpha == 0);
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionHandler>().First().Alpha == 0);
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionBox>().First().Alpha == 0);
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionBox>().First().Alpha == 0);
}
AddStep("paste hitobject", () => Editor.Paste());
@ -142,8 +142,8 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000);
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionHandler>().First().Alpha > 0);
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionHandler>().First().Alpha > 0);
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
}
[Test]

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -25,15 +26,15 @@ namespace osu.Game.Tests.Visual.Editing
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
private EditorBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
private void moveMouseToObject(Func<HitObject> targetFunc)
{
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.SelectionBlueprints
.First(s => s.HitObject == targetFunc())
.First(s => s.Item == targetFunc())
.ChildrenOfType<HitCirclePiece>()
.First().ScreenSpaceDrawQuad.Centre;
@ -41,6 +42,28 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestNudgeSelection()
{
HitCircle[] addedObjects = null;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
}));
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("nudge forwards", () => InputManager.Key(Key.K));
AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100);
AddStep("nudge backwards", () => InputManager.Key(Key.J));
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
}
[Test]
public void TestBasicSelect()
{
@ -156,9 +179,35 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
public void TestQuickDeleteRemovesObject()
public void TestQuickDeleteRemovesObjectInPlacement()
{
var addedObject = new HitCircle { StartTime = 1000 };
var addedObject = new HitCircle
{
StartTime = 0,
Position = OsuPlayfield.BASE_SIZE * 0.5f
};
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("enter placement mode", () => InputManager.PressKey(Key.Number2));
moveMouseToObject(() => addedObject);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesObjectInSelection()
{
var addedObject = new HitCircle
{
StartTime = 0,
Position = OsuPlayfield.BASE_SIZE * 0.5f
};
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));

View File

@ -130,9 +130,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime;
protected override void UpdateAfterChildren()
protected override void Update()
{
base.UpdateAfterChildren();
base.Update();
if (!FirstFrameClockTime.HasValue)
{

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditor : PlayerTestScene
{
private SkinEditor skinEditor;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add editor overlay", () =>
{
skinEditor?.Expire();
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
}
[Test]
public void TestToggleEditor()
{
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning.Editor;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create editor overlay", () =>
{
SetContents(() =>
{
var ruleset = new OsuRuleset();
var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo);
ScoreProcessor scoreProcessor = new ScoreProcessor();
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap);
var hudOverlay = new HUDOverlay(scoreProcessor, null, drawableRuleset, Array.Empty<Mod>())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
// Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
hudOverlay.ComboCounter.Current.Value = 1;
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
drawableRuleset,
hudOverlay,
new SkinEditor(hudOverlay),
}
};
});
});
}
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
working.LoadTrack();
Child = gameplayClockContainer = new GameplayClockContainer(working, 0)
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre));
AddStep("click", () =>
{
increment = skip_time - gameplayClock.CurrentTime - GameplayClockContainer.MINIMUM_SKIP_TIME / 2;
increment = skip_time - gameplayClock.CurrentTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME / 2;
InputManager.Click(MouseButton.Left);
});
AddStep("click", () => InputManager.Click(MouseButton.Left));

View File

@ -288,7 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override void WatchUser(int userId)
{
if (sentState)
if (!PlayingUsers.Contains(userId) && sentState)
{
// usually the server would do this.
sendState(beatmapId);

View File

@ -0,0 +1,191 @@
// 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.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneStoryboardWithOutro : PlayerTestScene
{
protected override bool HasCustomSteps => true;
protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentStoryboardDuration;
private bool showResults = true;
private event Func<HealthProcessor, JudgementResult, bool> currentFailConditions;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set ShowResults = true", () => showResults = true);
}
[Test]
public void TestStoryboardSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardNoSkipOutro()
{
CreateTest(null);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardExitToSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("score shown", () => Player.IsScoreShown);
}
[TestCase(false)]
[TestCase(true)]
public void TestStoryboardToggle(bool enabledAtBeginning)
{
CreateTest(null);
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestOutroEndsDuringFailAnimation()
{
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
});
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestShowResultsFalse()
{
CreateTest(() =>
{
AddStep("set ShowResults = false", () => showResults = false);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddWaitStep("wait", 10);
AddAssert("no score shown", () => !Player.IsScoreShown);
}
[Test]
public void TestStoryboardEndsBeforeCompletion()
{
CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100));
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardRewind()
{
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First();
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-1000));
AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
}
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OutroPlayer(currentFailConditions, showResults);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle());
return beatmap;
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
return base.CreateWorkingBeatmap(beatmap, createStoryboard(currentStoryboardDuration));
}
private Storyboard createStoryboard(double duration)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}
protected class OutroPlayer : TestPlayer
{
public void ExitViaPause() => PerformExit(true);
public new FailOverlay FailOverlay => base.FailOverlay;
public bool IsScoreShown => !this.IsCurrentScreen() && this.GetChildScreen() is ResultsScreen;
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults)
{
this.failConditions = failConditions;
}
protected override void LoadComplete()
{
base.LoadComplete();
HealthProcessor.FailConditions += failConditions;
}
protected override Task ImportScore(Score score)
{
return Task.CompletedTask;
}
}
}
}

View File

@ -43,7 +43,10 @@ namespace osu.Game.Tests.Visual.Online
createButtonWithBeatmap(createSoleily());
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526));
AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
createButtonWithBeatmap(createSoleily());
AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
ensureSoleilyRemoved();

View File

@ -83,6 +83,28 @@ namespace osu.Game.Tests.Visual.Online
};
});
[Test]
public void TestSystemMessageOrdering()
{
var standardMessage = new Message(messageIdSequence++)
{
Sender = admin,
Content = "I am a wang!"
};
var infoMessage1 = new InfoMessage($"the system is calling {messageIdSequence++}");
var infoMessage2 = new InfoMessage($"the system is calling {messageIdSequence++}");
AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2));
AddAssert("message order is correct", () => testChannel.Messages.Count == 3
&& testChannel.Messages[0] == standardMessage
&& testChannel.Messages[1] == infoMessage1
&& testChannel.Messages[2] == infoMessage2);
}
[Test]
public void TestManyMessages()
{

View File

@ -7,6 +7,7 @@ using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
@ -112,8 +113,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testInfoLabels(int expectedCount)
{
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount);
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
}
[Test]
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
[Test]
@ -135,15 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelect
private void selectBeatmap([CanBeNull] IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
Container containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
infoBefore = infoWedge.Info;
containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
});
AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore);
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
@ -193,7 +194,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapInfoWedge : BeatmapInfoWedge
{
public new BufferedWedgeInfo Info => base.Info;
public new Container DisplayedContent => base.DisplayedContent;
public new WedgeInfoText Info => base.Info;
}
private class TestHitObject : ConvertHitObject, IHasPosition

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