mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 16:52:54 +08:00
Merge branch 'master' into add-missing-author-links
This commit is contained in:
commit
165f443ab5
2
.github/ISSUE_TEMPLATE/01-bug-issues.md
vendored
2
.github/ISSUE_TEMPLATE/01-bug-issues.md
vendored
@ -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
46
.github/dependabot.yml
vendored
Normal 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
|
@ -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),
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
83
osu.Desktop/Security/ElevatedPrivilegesChecker.cs
Normal file
83
osu.Desktop/Security/ElevatedPrivilegesChecker.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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:
|
||||
|
11
osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs
Normal file
11
osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
29
osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
Normal file
29
osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
return;
|
||||
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(true);
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
}
|
||||
|
||||
private double originalStartTime;
|
||||
|
@ -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,
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModDualStages(),
|
||||
new ManiaModMirror(),
|
||||
new ManiaModDifficultyAdjust(),
|
||||
new ManiaModClassic(),
|
||||
new ManiaModInvert(),
|
||||
new ManiaModConstantSpeed()
|
||||
};
|
||||
|
@ -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),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
11
osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs
Normal file
11
osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
Position = new Vector2(128, 128),
|
||||
ComboIndex = 1,
|
||||
}), null));
|
||||
})));
|
||||
}
|
||||
|
||||
private HitCircle prepareObject(HitCircle circle)
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Vector2(300, 0),
|
||||
}),
|
||||
RepeatCount = 1
|
||||
}), null));
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
Normal file
102
osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
17
osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs
Normal file
17
osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
{
|
||||
|
11
osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
Normal file
11
osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
@ -135,6 +135,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
new TaikoModRandom(),
|
||||
new TaikoModDifficultyAdjust(),
|
||||
new TaikoModClassic(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
114
osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs
Normal file
114
osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
130
osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
Normal file
130
osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
194
osu.Game.Tests/Editing/Checks/CheckConcurrentObjectsTest.cs
Normal file
194
osu.Game.Tests/Editing/Checks/CheckConcurrentObjectsTest.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
155
osu.Game.Tests/Editing/Checks/CheckUnsnappedObjectsTest.cs
Normal file
155
osu.Game.Tests/Editing/Checks/CheckUnsnappedObjectsTest.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
94
osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
Normal file
94
osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
});
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
91
osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs
Normal file
91
osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -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()
|
||||
|
@ -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]
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
36
osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
Normal file
36
osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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);
|
||||
|
191
osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
Normal file
191
osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user