1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 03:42:57 +08:00

Merge branch 'master' into storyboard-loops-v2

This commit is contained in:
Salman Ahmed 2024-05-01 23:46:38 +03:00
commit 895c09d4d1
282 changed files with 7000 additions and 1795 deletions

View File

@ -110,10 +110,14 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
steps:
- name: Check permissions
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0
with:
require: 'write'
run: |
ALLOWED_USERS=(smoogipoo peppy bdach)
for i in "${ALLOWED_USERS[@]}"; do
if [[ "${{ github.actor }}" == "$i" ]]; then
exit 0
fi
done
exit 1
create-comment:
name: Create PR comment
@ -122,7 +126,7 @@ jobs:
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
steps:
- name: Create comment
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
message: |
@ -249,7 +253,7 @@ jobs:
- name: Restore cache
id: restore-cache
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@ -280,7 +284,7 @@ jobs:
- name: Restore cache
id: restore-cache
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@ -354,7 +358,7 @@ jobs:
steps:
- name: Update comment on success
if: ${{ needs.generator.result == 'success' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert
@ -365,7 +369,7 @@ jobs:
- name: Update comment on failure
if: ${{ needs.generator.result == 'failure' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert
@ -375,7 +379,7 @@ jobs:
- name: Update comment on cancellation
if: ${{ needs.generator.result == 'cancelled' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: delete

3
.gitignore vendored
View File

@ -340,4 +340,5 @@ inspectcode
# Fody (pulled in by Realm) - schema file
FodyWeavers.xsd
.idea/.idea.osu.Desktop/.idea/misc.xml
.idea/.idea.osu.Desktop/.idea/misc.xml
.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml

View File

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

View File

@ -5,14 +5,21 @@ using System;
using System.Text;
using DiscordRPC;
using DiscordRPC.Message;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Users;
using LogLevel = osu.Framework.Logging.LogLevel;
@ -21,39 +28,78 @@ namespace osu.Desktop
{
internal partial class DiscordRichPresence : Component
{
private const string client_id = "367827983903490050";
private const string client_id = "1216669957799018608";
private DiscordRpcClient client = null!;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private IBindable<APIUser> user = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private OsuGame game { get; set; } = null!;
[Resolved]
private LoginOverlay? login { get; set; }
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
private readonly RichPresence presence = new RichPresence
{
Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
Secrets = new Secrets
{
JoinSecret = null,
SpectateSecret = null,
},
};
private IBindable<APIUser>? user;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load()
{
client = new DiscordRpcClient(client_id)
{
SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady.
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
// to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady).
SkipIdenticalPresence = true
};
client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
try
{
client.RegisterUriScheme();
client.Subscribe(EventType.Join);
client.OnJoin += onJoin;
}
catch (Exception ex)
{
// This is known to fail in at least the following sandboxed environments:
// - macOS (when packaged as an app bundle)
// - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170)
// There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now.
Logger.Log($"Failed to register Discord URI scheme: {ex}");
}
client.Initialize();
}
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
@ -67,35 +113,57 @@ namespace osu.Desktop
activity.BindTo(u.NewValue.Activity);
}, true);
ruleset.BindValueChanged(_ => updateStatus());
status.BindValueChanged(_ => updateStatus());
activity.BindValueChanged(_ => updateStatus());
privacyMode.BindValueChanged(_ => updateStatus());
client.Initialize();
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated;
}
private void onReady(object _, ReadyMessage __)
{
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
updateStatus();
// when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence).
if (client.CurrentPresence != null)
client.SetPresence(null);
schedulePresenceUpdate();
}
private void updateStatus()
private void onRoomUpdated() => schedulePresenceUpdate();
private ScheduledDelegate? presenceUpdateDelegate;
private void schedulePresenceUpdate()
{
if (!client.IsInitialized)
return;
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
presenceUpdateDelegate?.Cancel();
presenceUpdateDelegate = Scheduler.AddDelayed(() =>
{
client.ClearPresence();
return;
}
if (!client.IsInitialized)
return;
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
{
client.ClearPresence();
return;
}
if (activity.Value != null)
{
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
updatePresence(hideIdentifiableInformation);
client.SetPresence(presence);
}, 200);
}
private void updatePresence(bool hideIdentifiableInformation)
{
if (user == null)
return;
// user activity
if (activity.Value != null)
{
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
@ -121,7 +189,42 @@ namespace osu.Desktop
presence.Details = string.Empty;
}
// update user information
// user party
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
{
MultiplayerRoom room = multiplayerClient.Room;
presence.Party = new Party
{
Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private,
ID = room.RoomID.ToString(),
// technically lobbies can have infinite users, but Discord needs this to be set to something.
// to make party display sensible, assign a powers of two above participants count (8 at minimum).
Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))),
Size = room.Users.Count,
};
RoomSecret roomSecret = new RoomSecret
{
RoomID = room.RoomID,
Password = room.Settings.Password,
};
if (client.HasRegisteredUriScheme)
presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret);
// discord cannot handle both secrets and buttons at the same time, so we need to choose something.
// the multiplayer room seems more important.
presence.Buttons = null;
}
else
{
presence.Party = null;
presence.Secrets.JoinSecret = null;
}
// game images:
// large image tooltip
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty;
else
@ -132,16 +235,43 @@ namespace osu.Desktop
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
}
// update ruleset
// small image
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name;
client.SetPresence(presence);
}
private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() =>
{
game.Window?.Raise();
if (!api.IsLoggedIn)
{
login?.Show();
return;
}
Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug);
// Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other.
// Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion.
if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password))
{
Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important);
return;
}
var request = new GetRoomRequest(roomId);
request.Success += room => Schedule(() =>
{
game.PresentMultiplayerMatch(room, password);
});
request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important);
api.Queue(request);
});
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
private string truncate(string str)
private static string truncate(string str)
{
if (Encoding.UTF8.GetByteCount(str) <= 128)
return str;
@ -160,7 +290,31 @@ namespace osu.Desktop
});
}
private int? getBeatmapID(UserActivity activity)
private static bool tryParseRoomSecret(string secretJson, out long roomId, out string? password)
{
roomId = 0;
password = null;
RoomSecret? roomSecret;
try
{
roomSecret = JsonConvert.DeserializeObject<RoomSecret>(secretJson);
}
catch
{
return false;
}
if (roomSecret == null) return false;
roomId = roomSecret.RoomID;
password = roomSecret.Password;
return true;
}
private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
@ -176,8 +330,20 @@ namespace osu.Desktop
protected override void Dispose(bool isDisposing)
{
if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated;
client.Dispose();
base.Dispose(isDisposing);
}
private class RoomSecret
{
[JsonProperty(@"roomId", Required = Required.Always)]
public long RoomID { get; set; }
[JsonProperty(@"password", Required = Required.AllowNull)]
public string? Password { get; set; }
}
}
}

View File

@ -8,11 +8,13 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using osu.Framework.Logging;
namespace osu.Desktop
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SupportedOSPlatform("windows")]
internal static class NVAPI
{
private const string osu_filename = "osu!.exe";

View File

@ -22,7 +22,7 @@ using osu.Game.IPC;
using osu.Game.Online.Multiplayer;
using osu.Game.Performance;
using osu.Game.Utils;
using SDL2;
using SDL;
namespace osu.Desktop
{
@ -161,7 +161,7 @@ namespace osu.Desktop
host.Window.Title = Name;
}
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo();
protected override void Dispose(bool isDisposing)
{
@ -170,13 +170,14 @@ namespace osu.Desktop
archiveImportIPCChannel?.Dispose();
}
private class SDL2BatteryInfo : BatteryInfo
private unsafe class SDL3BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
SDL.SDL_GetPowerInfo(out _, out int percentage);
int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1)
return null;
@ -185,7 +186,7 @@ namespace osu.Desktop
}
}
public override bool OnBattery => SDL.SDL_GetPowerInfo(out _, out _) == SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Runtime;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Performance;
@ -11,16 +12,26 @@ namespace osu.Desktop.Performance
{
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
{
public bool IsSessionActive => activeSessions > 0;
private int activeSessions;
private GCLatencyMode originalGCMode;
public IDisposable BeginSession()
{
enableHighPerformanceSession();
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.disableHighPerformanceSession());
enterSession();
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.exitSession());
}
private void enableHighPerformanceSession()
private void enterSession()
{
if (Interlocked.Increment(ref activeSessions) > 1)
{
Logger.Log($"High performance session requested ({activeSessions} running in total)");
return;
}
Logger.Log("Starting high performance session");
originalGCMode = GCSettings.LatencyMode;
@ -30,8 +41,14 @@ namespace osu.Desktop.Performance
GC.Collect(0);
}
private void disableHighPerformanceSession()
private void exitSession()
{
if (Interlocked.Decrement(ref activeSessions) > 0)
{
Logger.Log($"High performance session finished ({activeSessions} others remain)");
return;
}
Logger.Log("Ending high performance session");
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)

View File

@ -13,7 +13,7 @@ using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
using SDL2;
using SDL;
using Squirrel;
namespace osu.Desktop
@ -52,16 +52,19 @@ namespace osu.Desktop
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
// disabling it ourselves.
// We could also better detect compatibility mode if required:
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!",
"This version of osu! requires at least Windows 8.1 to run.\n"
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
return;
unsafe
{
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
// disabling it ourselves.
// We could also better detect compatibility mode if required:
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!"u8,
"This version of osu! requires at least Windows 8.1 to run.\n"u8
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
return;
}
}
setupSquirrel();
@ -70,7 +73,8 @@ namespace osu.Desktop
// NVIDIA profiles are based on the executable name of a process.
// Lazer and stable share the same executable name.
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
if (OperatingSystem.IsWindows())
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[new Droplet(), 0.01, true],
[new TinyDroplet(), 0, false],
[new Banana(), 0, false],
[new BananaShower(), 0, false]
];
[TestCaseSource(nameof(test_cases))]

View File

@ -0,0 +1,158 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks
{
[TestFixture]
public class CheckCatchAbnormalDifficultySettingsTest
{
private CheckCatchAbnormalDifficultySettings check = null!;
private readonly IBeatmap beatmap = new Beatmap<HitObject>();
[SetUp]
public void Setup()
{
check = new CheckCatchAbnormalDifficultySettings();
beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo;
beatmap.Difficulty = new BeatmapDifficulty
{
ApproachRate = 5,
CircleSize = 5,
DrainRate = 5,
};
}
[Test]
public void TestNormalSettings()
{
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestApproachRateTwoDecimals()
{
beatmap.Difficulty.ApproachRate = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestCircleSizeTwoDecimals()
{
beatmap.Difficulty.CircleSize = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestDrainRateTwoDecimals()
{
beatmap.Difficulty.DrainRate = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestApproachRateUnder()
{
beatmap.Difficulty.ApproachRate = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestCircleSizeUnder()
{
beatmap.Difficulty.CircleSize = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateUnder()
{
beatmap.Difficulty.DrainRate = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestApproachRateOver()
{
beatmap.Difficulty.ApproachRate = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestCircleSizeOver()
{
beatmap.Difficulty.CircleSize = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateOver()
{
beatmap.Difficulty.DrainRate = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
private BeatmapVerifierContext getContext()
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,253 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
[TestFixture]
public partial class TestSceneCatchReverseSelection : TestSceneEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestReverseSelectionTwoFruits()
{
CatchHitObject[] objects = null!;
bool[] newCombos = null!;
addObjects([
new Fruit
{
StartTime = 200,
X = 0,
},
new Fruit
{
StartTime = 400,
X = 20,
}
]);
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
selectEverything();
reverseSelection();
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
[Test]
public void TestReverseSelectionThreeFruits()
{
CatchHitObject[] objects = null!;
bool[] newCombos = null!;
addObjects([
new Fruit
{
StartTime = 200,
X = 0,
},
new Fruit
{
StartTime = 400,
X = 20,
},
new Fruit
{
StartTime = 600,
X = 40,
}
]);
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
selectEverything();
reverseSelection();
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
[Test]
public void TestReverseSelectionFruitAndJuiceStream()
{
CatchHitObject[] objects = null!;
bool[] newCombos = null!;
addObjects([
new Fruit
{
StartTime = 200,
X = 0,
},
new JuiceStream
{
StartTime = 400,
X = 20,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50))
}
}
}
]);
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
selectEverything();
reverseSelection();
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
[Test]
public void TestReverseSelectionTwoFruitsAndJuiceStream()
{
CatchHitObject[] objects = null!;
bool[] newCombos = null!;
addObjects([
new Fruit
{
StartTime = 200,
X = 0,
},
new Fruit
{
StartTime = 400,
X = 20,
},
new JuiceStream
{
StartTime = 600,
X = 40,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50))
}
}
}
]);
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
selectEverything();
reverseSelection();
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
[Test]
public void TestReverseSelectionTwoCombos()
{
CatchHitObject[] objects = null!;
bool[] newCombos = null!;
addObjects([
new Fruit
{
StartTime = 200,
X = 0,
},
new Fruit
{
StartTime = 400,
X = 20,
},
new Fruit
{
StartTime = 600,
X = 40,
},
new Fruit
{
StartTime = 800,
NewCombo = true,
X = 60,
},
new Fruit
{
StartTime = 1000,
X = 80,
},
new Fruit
{
StartTime = 1200,
X = 100,
}
]);
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
selectEverything();
reverseSelection();
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
private void addObjects(CatchHitObject[] hitObjects) => AddStep("Add objects", () => EditorBeatmap.AddRange(hitObjects));
private IEnumerable<CatchHitObject> getObjects() => EditorBeatmap.HitObjects.OfType<CatchHitObject>();
private IEnumerable<bool> getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
private void selectEverything()
{
AddStep("Select everything", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
}
private void reverseSelection()
{
AddStep("Reverse selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
}
}
}

View File

@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Catch
: base(component)
{
}
protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
}
}

View File

@ -13,7 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckBananaShowerGap()
new CheckBananaShowerGap(),
new CheckCatchAbnormalDifficultySettings(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -76,21 +76,38 @@ namespace osu.Game.Rulesets.Catch.Edit
public override bool HandleReverse()
{
var hitObjects = EditorBeatmap.SelectedHitObjects
.OfType<CatchHitObject>()
.OrderBy(obj => obj.StartTime)
.ToList();
double selectionStartTime = SelectedItems.Min(h => h.StartTime);
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
EditorBeatmap.PerformOnSelection(hitObject =>
{
hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
// the expectation is that even if the objects themselves are reversed temporally,
// the position of new combos in the selection should remain the same.
// preserve it for later before doing the reversal.
var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
if (hitObject is JuiceStream juiceStream)
foreach (var h in hitObjects)
{
h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime);
if (h is JuiceStream juiceStream)
{
juiceStream.Path.Reverse(out Vector2 positionalOffset);
juiceStream.OriginalX += positionalOffset.X;
juiceStream.LegacyConvertedY += positionalOffset.Y;
EditorBeatmap.Update(juiceStream);
}
});
}
// re-order objects by start time again after reversing, and restore new combo flag positioning
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
for (int i = 0; i < hitObjects.Count; ++i)
hitObjects[i].NewCombo = newComboOrder[i];
return true;
}

View File

@ -0,0 +1,39 @@
// 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 osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Catch.Edit.Checks
{
public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySettings
{
public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks catch relevant settings");
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
yield return issue;
if (OutOfRange("Approach rate", diff.ApproachRate, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue))
yield return issue;
if (OutOfRange("Circle size", diff.CircleSize, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
yield return issue;
if (OutOfRange("Drain rate", diff.DrainRate, out issue))
yield return issue;
}
}
}

View File

@ -32,6 +32,10 @@ namespace osu.Game.Rulesets.Catch.Scoring
if (result.Type == HitResult.SmallTickMiss)
return false;
// on stable, banana showers don't exist as concrete objects themselves, so they can't cause a fail.
if (result.HitObject is BananaShower)
return false;
return base.CheckDefaultFailCondition(result);
}

View File

@ -16,6 +16,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
@ -52,5 +54,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
public override DrawableHitObject<CatchHitObject>? CreateDrawableRepresentation(CatchHitObject h) => null;
protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
}
}

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Rulesets.Mania.Edit.Checks;
using System.Linq;
namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
{
[TestFixture]
public class CheckKeyCountTest
{
private CheckKeyCount check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckKeyCount();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Ruleset = new ManiaRuleset().RulesetInfo
}
};
}
[Test]
public void TestKeycountFour()
{
beatmap.Difficulty.CircleSize = 4;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestKeycountSmallerThanFour()
{
beatmap.Difficulty.CircleSize = 1;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow);
}
private BeatmapVerifierContext getContext()
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,121 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
{
[TestFixture]
public class CheckManiaAbnormalDifficultySettingsTest
{
private CheckManiaAbnormalDifficultySettings check = null!;
private readonly IBeatmap beatmap = new Beatmap<HitObject>();
[SetUp]
public void Setup()
{
check = new CheckManiaAbnormalDifficultySettings();
beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo;
beatmap.Difficulty = new BeatmapDifficulty
{
OverallDifficulty = 5,
DrainRate = 5,
};
}
[Test]
public void TestNormalSettings()
{
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestOverallDifficultyTwoDecimals()
{
beatmap.Difficulty.OverallDifficulty = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestDrainRateTwoDecimals()
{
beatmap.Difficulty.DrainRate = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestOverallDifficultyUnder()
{
beatmap.Difficulty.OverallDifficulty = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateUnder()
{
beatmap.Difficulty.DrainRate = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestOverallDifficultyOver()
{
beatmap.Difficulty.OverallDifficulty = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateOver()
{
beatmap.Difficulty.DrainRate = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
private BeatmapVerifierContext getContext()
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
public int TotalColumns => Stages.Sum(g => g.Columns);
/// <summary>
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
/// </summary>
public readonly int OriginalTotalColumns;
/// <summary>
/// Creates a new <see cref="ManiaBeatmap"/>.
/// </summary>
@ -35,7 +30,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{
Stages.Add(defaultStage);
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
}
public override IEnumerable<BeatmapStatistic> GetStatistics()

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
@ -14,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Utils;
using osuTK;
@ -27,24 +26,42 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
private const int max_notes_for_density = 7;
/// <summary>
/// The total number of columns.
/// </summary>
public int TotalColumns => TargetColumns * (Dual ? 2 : 1);
/// <summary>
/// The number of columns per-stage.
/// </summary>
public int TargetColumns;
/// <summary>
/// Whether to double the number of stages.
/// </summary>
public bool Dual;
/// <summary>
/// Whether the beatmap instantiated with is for the mania ruleset.
/// </summary>
public readonly bool IsForCurrentRuleset;
private readonly int originalTargetColumns;
// Internal for testing purposes
internal LegacyRandom Random { get; private set; }
internal readonly LegacyRandom Random;
private Pattern lastPattern = new Pattern();
private ManiaBeatmap beatmap;
public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
: base(beatmap, ruleset)
: this(beatmap, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap), ruleset)
{
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap));
}
private ManiaBeatmapConverter(IBeatmap? beatmap, LegacyBeatmapConversionDifficultyInfo difficulty, Ruleset ruleset)
: base(beatmap!, ruleset)
{
IsForCurrentRuleset = difficulty.SourceRuleset.Equals(ruleset.RulesetInfo);
Random = new LegacyRandom((int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate));
TargetColumns = getColumnCount(difficulty);
if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
@ -52,52 +69,53 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
Dual = true;
}
originalTargetColumns = TargetColumns;
static int getColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
{
double roundedCircleSize = Math.Round(difficulty.CircleSize);
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
return (int)Math.Max(1, roundedCircleSize);
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
{
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
// optimisations, it actually ends up happening on doubles.
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
if (percentSpecialObjects < 0.2)
return 7;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
}
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
}
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList<Mod>? mods = null)
{
double roundedCircleSize = Math.Round(difficulty.CircleSize);
var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
return (int)Math.Max(1, roundedCircleSize);
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
if (mods != null)
{
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
// optimisations, it actually ends up happening on doubles.
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
if (percentSpecialObjects < 0.2)
return 7;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
foreach (var m in mods.OfType<IApplicableToBeatmapConverter>())
m.ApplyToBeatmapConverter(converter);
}
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
return converter.TotalColumns;
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
IBeatmapDifficultyInfo difficulty = original.Difficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new LegacyRandom(seed);
return base.ConvertBeatmap(original, cancellationToken);
}
protected override Beatmap<ManiaHitObject> CreateBeatmap()
{
beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns);
ManiaBeatmap beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns));
if (Dual)
beatmap.Stages.Add(new StageDefinition(TargetColumns));
@ -115,10 +133,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
if (objects == null)
yield break;
foreach (ManiaHitObject obj in objects)
yield return obj;
}
@ -152,7 +166,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
{
var generator = new SpecificBeatmapPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
foreach (var newPattern in generator.Generate())
{
@ -171,13 +185,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
{
Patterns.PatternGenerator conversion = null;
Patterns.PatternGenerator? conversion = null;
switch (original)
{
case IHasPath:
{
var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
conversion = generator;
var positionData = original as IHasPosition;
@ -195,7 +209,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
case IHasDuration endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
@ -206,7 +220,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
computeDensity(original.StartTime);
conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(original.StartTime, positionData.Position);
break;
@ -231,8 +245,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
}

View File

@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly int endTime;
private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);

View File

@ -23,9 +23,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly PatternType convertType;
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density,
PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
double density, PatternType lastStair)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
StairType = lastStair;

View File

@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
convertType = PatternType.None;
if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)

View File

@ -27,20 +27,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
protected readonly LegacyRandom Random;
/// <summary>
/// The beatmap which <see cref="HitObject"/> is being converted from.
/// </summary>
protected readonly IBeatmap OriginalBeatmap;
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
: base(hitObject, beatmap, totalColumns, previousPattern)
{
ArgumentNullException.ThrowIfNull(random);
ArgumentNullException.ThrowIfNull(originalBeatmap);
Random = random;
OriginalBeatmap = originalBeatmap;
RandomStart = TotalColumns == 8 ? 1 : 0;
}
@ -104,17 +96,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (conversionDifficulty != null)
return conversionDifficulty.Value;
HitObject lastObject = OriginalBeatmap.HitObjects.LastOrDefault();
HitObject firstObject = OriginalBeatmap.HitObjects.FirstOrDefault();
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
// Drain time in seconds
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - OriginalBeatmap.TotalBreakTime) / 1000);
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
if (drainTime == 0)
drainTime = 10000;
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
IBeatmapDifficultyInfo difficulty = Beatmap.Difficulty;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
@ -25,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
/// <summary>
/// The beatmap which <see cref="HitObject"/> is a part of.
/// </summary>
protected readonly ManiaBeatmap Beatmap;
protected readonly IBeatmap Beatmap;
protected readonly int TotalColumns;
protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
protected PatternGenerator(HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
{
ArgumentNullException.ThrowIfNull(hitObject);
ArgumentNullException.ThrowIfNull(beatmap);
@ -38,8 +39,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
HitObject = hitObject;
Beatmap = beatmap;
PreviousPattern = previousPattern;
TotalColumns = Beatmap.TotalColumns;
TotalColumns = totalColumns;
}
/// <summary>

View File

@ -51,13 +51,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return multiplier;
// Apply key mod multipliers.
int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty);
int actualColumns = originalColumns;
actualColumns = mods.OfType<ManiaKeyMod>().SingleOrDefault()?.KeyCount ?? actualColumns;
if (mods.Any(m => m is ManiaModDualStages))
actualColumns *= 2;
int actualColumns = ManiaBeatmapConverter.GetColumnCount(difficulty, mods);
if (actualColumns > originalColumns)
multiplier *= 0.9;

View File

@ -0,0 +1,39 @@
// 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 osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Mania.Edit.Checks
{
public class CheckKeyCount : ICheck
{
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Check mania keycount.");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateKeycountTooLow(this),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
if (diff.CircleSize < 4)
{
yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize);
}
}
public class IssueTemplateKeycountTooLow : IssueTemplate
{
public IssueTemplateKeycountTooLow(ICheck check)
: base(check, IssueType.Problem, "Key count is {0} and must be 4 or higher.")
{
}
public Issue Create(float current) => new Issue(this, current);
}
}
}

View File

@ -0,0 +1,33 @@
// 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 osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Mania.Edit.Checks
{
public class CheckManiaAbnormalDifficultySettings : CheckAbnormalDifficultySettings
{
public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks mania relevant settings");
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue))
yield return issue;
if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
yield return issue;
if (OutOfRange("Drain rate", diff.DrainRate, out issue))
yield return issue;
}
}
}

View File

@ -0,0 +1,26 @@
// 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.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Mania.Edit.Checks;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaBeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
// Settings
new CheckKeyCount(),
new CheckManiaAbnormalDifficultySettings(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
return checks.SelectMany(check => check.Run(context));
}
}
}

View File

@ -1,9 +1,14 @@
// 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.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
@ -14,9 +19,9 @@ namespace osu.Game.Rulesets.Mania
{
private FilterCriteria.OptionalRange<float> keys;
public bool Matches(BeatmapInfo beatmapInfo)
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)));
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
@ -30,5 +35,20 @@ namespace osu.Game.Rulesets.Mania
return false;
}
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
if (keys.HasFilter)
{
// Interpreting as the Mod type is required for equality comparison.
HashSet<Mod> oldSet = mods.OldValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
HashSet<Mod> newSet = mods.NewValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
if (!oldSet.SetEquals(newSet))
return true;
}
return false;
}
}
}

View File

@ -65,6 +65,8 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new ManiaBeatmapVerifier();
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
{
switch (skin)
@ -421,8 +423,8 @@ namespace osu.Game.Rulesets.Mania
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
public int GetKeyCount(IBeatmapInfo beatmapInfo)
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo));
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
}
public enum PlayfieldType

View File

@ -15,10 +15,6 @@ namespace osu.Game.Rulesets.Mania
: base(component)
{
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
}
public enum ManiaSkinComponents

View File

@ -26,6 +26,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI
@ -164,6 +165,8 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -0,0 +1,194 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Osu.Edit.Checks;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Tests.Beatmaps;
using System.Linq;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckOsuAbnormalDifficultySettingsTest
{
private CheckOsuAbnormalDifficultySettings check = null!;
private readonly IBeatmap beatmap = new Beatmap<HitObject>();
[SetUp]
public void Setup()
{
check = new CheckOsuAbnormalDifficultySettings();
beatmap.Difficulty = new BeatmapDifficulty
{
ApproachRate = 5,
CircleSize = 5,
DrainRate = 5,
OverallDifficulty = 5,
};
}
[Test]
public void TestNormalSettings()
{
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestApproachRateTwoDecimals()
{
beatmap.Difficulty.ApproachRate = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestCircleSizeTwoDecimals()
{
beatmap.Difficulty.CircleSize = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestDrainRateTwoDecimals()
{
beatmap.Difficulty.DrainRate = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestOverallDifficultyTwoDecimals()
{
beatmap.Difficulty.OverallDifficulty = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestApproachRateUnder()
{
beatmap.Difficulty.ApproachRate = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestCircleSizeUnder()
{
beatmap.Difficulty.CircleSize = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateUnder()
{
beatmap.Difficulty.DrainRate = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestOverallDifficultyUnder()
{
beatmap.Difficulty.OverallDifficulty = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestApproachRateOver()
{
beatmap.Difficulty.ApproachRate = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestCircleSizeOver()
{
beatmap.Difficulty.CircleSize = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateOver()
{
beatmap.Difficulty.DrainRate = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestOverallDifficultyOver()
{
beatmap.Difficulty.OverallDifficulty = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
private BeatmapVerifierContext getContext()
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider created", () =>
{
if (circle1 is null || circle2 is null || slider is null)
if (circle1 == null || circle2 == null || slider == null)
return false;
var controlPoints = slider.Path.ControlPoints;
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider created", () =>
{
if (slider1 is null || slider2 is null || slider1Path is null)
if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path is null || slider2 is null)
if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
@ -231,6 +231,137 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
(pos: circle2.Position, pathType: null)));
}
[Test]
public void TestMergeSliderSliderSameStartTime()
{
Slider? slider1 = null;
SliderPath? slider1Path = null;
Slider? slider2 = null;
AddStep("select two sliders", () =>
{
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
EditorClock.Seek(slider1.StartTime);
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
});
AddStep("move sliders to the same start time", () =>
{
slider2!.StartTime = slider1!.StartTime;
});
mergeSelection();
AddAssert("slider created", () =>
{
if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
var controlPoints2 = slider2.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
for (int i = 0; i < controlPoints1.Count - 1; i++)
{
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
}
for (int i = 0; i < controlPoints2.Count; i++)
{
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
}
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
});
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
});
}
[Test]
public void TestMergeSliderSliderSameStartAndEndTime()
{
Slider? slider1 = null;
SliderPath? slider1Path = null;
Slider? slider2 = null;
AddStep("select two sliders", () =>
{
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
EditorClock.Seek(slider1.StartTime);
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
});
AddStep("move sliders to the same start & end time", () =>
{
slider2!.StartTime = slider1!.StartTime;
slider2.Path = slider1.Path;
});
mergeSelection();
AddAssert("slider created", () =>
{
if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
var controlPoints2 = slider2.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
for (int i = 0; i < controlPoints1.Count - 1; i++)
{
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
}
for (int i = 0; i < controlPoints2.Count; i++)
{
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
}
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
});
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
});
}
private void mergeSelection()
{
AddStep("merge selection", () =>

View File

@ -0,0 +1,300 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public partial class TestSceneOsuReverseSelection : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestReverseSelectionTwoCircles()
{
OsuHitObject[] objects = null!;
bool[] newCombos = null!;
AddStep("Add circles", () =>
{
var circle1 = new HitCircle
{
StartTime = 0,
Position = new Vector2(208, 240)
};
var circle2 = new HitCircle
{
StartTime = 200,
Position = new Vector2(256, 144)
};
EditorBeatmap.AddRange([circle1, circle2]);
});
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("Reverse selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
[Test]
public void TestReverseSelectionThreeCircles()
{
OsuHitObject[] objects = null!;
bool[] newCombos = null!;
AddStep("Add circles", () =>
{
var circle1 = new HitCircle
{
StartTime = 0,
Position = new Vector2(208, 240)
};
var circle2 = new HitCircle
{
StartTime = 200,
Position = new Vector2(256, 144)
};
var circle3 = new HitCircle
{
StartTime = 400,
Position = new Vector2(304, 240)
};
EditorBeatmap.AddRange([circle1, circle2, circle3]);
});
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("Reverse selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
[Test]
public void TestReverseSelectionCircleAndSlider()
{
OsuHitObject[] objects = null!;
bool[] newCombos = null!;
Vector2 sliderHeadOldPosition = default;
Vector2 sliderTailOldPosition = default;
AddStep("Add objects", () =>
{
var circle = new HitCircle
{
StartTime = 0,
Position = new Vector2(208, 240)
};
var slider = new Slider
{
StartTime = 200,
Position = sliderHeadOldPosition = new Vector2(257, 144),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
sliderTailOldPosition = slider.EndPosition;
EditorBeatmap.AddRange([circle, slider]);
});
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("Reverse selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
AddAssert("Slider head is at slider tail", () =>
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).Position, sliderTailOldPosition) < 1);
AddAssert("Slider tail is at slider head", () =>
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1);
}
[Test]
public void TestReverseSelectionTwoCirclesAndSlider()
{
OsuHitObject[] objects = null!;
bool[] newCombos = null!;
Vector2 sliderHeadOldPosition = default;
Vector2 sliderTailOldPosition = default;
AddStep("Add objects", () =>
{
var circle1 = new HitCircle
{
StartTime = 0,
Position = new Vector2(208, 240)
};
var circle2 = new HitCircle
{
StartTime = 200,
Position = new Vector2(256, 144)
};
var slider = new Slider
{
StartTime = 200,
Position = sliderHeadOldPosition = new Vector2(304, 240),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
sliderTailOldPosition = slider.EndPosition;
EditorBeatmap.AddRange([circle1, circle2, slider]);
});
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("Reverse selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
AddAssert("Slider head is at slider tail", () =>
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).Position, sliderTailOldPosition) < 1);
AddAssert("Slider tail is at slider head", () =>
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1);
}
[Test]
public void TestReverseSelectionTwoCombos()
{
OsuHitObject[] objects = null!;
bool[] newCombos = null!;
AddStep("Add circles", () =>
{
var circle1 = new HitCircle
{
StartTime = 0,
Position = new Vector2(216, 240)
};
var circle2 = new HitCircle
{
StartTime = 200,
Position = new Vector2(120, 192)
};
var circle3 = new HitCircle
{
StartTime = 400,
Position = new Vector2(216, 144)
};
var circle4 = new HitCircle
{
StartTime = 646,
NewCombo = true,
Position = new Vector2(296, 240)
};
var circle5 = new HitCircle
{
StartTime = 846,
Position = new Vector2(392, 162)
};
var circle6 = new HitCircle
{
StartTime = 1046,
Position = new Vector2(296, 144)
};
EditorBeatmap.AddRange([circle1, circle2, circle3, circle4, circle5, circle6]);
});
AddStep("store objects & new combo data", () =>
{
objects = getObjects().ToArray();
newCombos = getObjectNewCombos().ToArray();
});
AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("Reverse selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
}
private IEnumerable<OsuHitObject> getObjects() => EditorBeatmap.HitObjects.OfType<OsuHitObject>();
private IEnumerable<bool> getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
}
}

View File

@ -172,6 +172,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPathType(4, null);
}
[Test]
public void TestStackingUpdatesPointsPosition()
{
createVisualiser(true);
Vector2[] points =
[
new Vector2(200),
new Vector2(300),
new Vector2(500, 300),
new Vector2(700, 200),
new Vector2(500, 100)
];
foreach (var point in points) addControlPointStep(point);
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);
for (int i = 0; i < points.Length; i++)
addAssertPointPositionChanged(points, i);
}
[Test]
public void TestStackingUpdatesConnectionPosition()
{
createVisualiser(true);
Vector2 connectionPosition;
addControlPointStep(connectionPosition = new Vector2(300));
addControlPointStep(new Vector2(600));
// Apply a big number in stacking so the person running the test can clearly see if it fails
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
AddAssert($"Connection at {connectionPosition} changed",
() => visualiser.Connections[0].Position,
() => !Is.EqualTo(connectionPosition)
);
}
private void addAssertPointPositionChanged(Vector2[] points, int index)
{
AddAssert($"Point at {points.ElementAt(index)} changed",
() => visualiser.Pieces[index].Position,
() => !Is.EqualTo(points.ElementAt(index))
);
}
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
{
Anchor = Anchor.Centre,

View File

@ -24,14 +24,38 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestHotkeyHandling()
{
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("deselect everything", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddUntilStep("no popover present", getPopover, () => Is.Null);
AddStep("select single circle",
() => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddAssert("only playfield centre origin rotation available", () =>
{
var popover = getPopover();
var buttons = popover.ChildrenOfType<EditorRadioButton>();
return buttons.Any(btn => btn.Text == "Selection centre" && !btn.Enabled.Value)
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", getPopover, () => Is.Null);
AddStep("select first three objects", () =>
{
@ -44,14 +68,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddAssert("both origin rotation available", () =>
{
var popover = getPopover();
var buttons = popover.ChildrenOfType<EditorRadioButton>();
return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value)
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddUntilStep("no popover present", getPopover, () => Is.Null);
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
}
[Test]

View File

@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("add hitsounds", () =>
{
if (slider is null) return;
if (slider == null) return;
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
slider.Samples.Add(sample.With());
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"move mouse to control point {index}", () =>
{
if (slider is null || visualiser is null) return;
if (slider == null || visualiser == null) return;
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
if (visualiser is null) return;
if (visualiser == null) return;
MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private SessionStatics statics { get; set; } = null!;
private ScoreAccessibleSoloPlayer currentPlayer = null!;
private readonly ManualClock manualClock = new ManualClock { Rate = 0 };
private readonly ManualClock manualClock = new ManualClock { Rate = 1 };
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@ -13,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osu.Game.Audio;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
@ -47,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
createTest(() =>
{
var skinContainer = new LegacySkinContainer(renderer, false);
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
createTest(() =>
{
var skinContainer = new LegacySkinContainer(renderer, true);
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
@ -70,6 +72,22 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
[Test]
public void TestLegacyDisjointCursorTrailViaNoCursor()
{
createTest(() =>
{
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false, provideCursor: false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
return skinContainer;
});
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
}
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
{
Clear();
@ -86,12 +104,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private partial class LegacySkinContainer : Container, ISkinSource
{
private readonly IRenderer renderer;
private readonly bool disjoint;
private readonly bool provideMiddle;
private readonly bool provideCursor;
public LegacySkinContainer(IRenderer renderer, bool disjoint)
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
{
this.renderer = renderer;
this.disjoint = disjoint;
this.provideMiddle = provideMiddle;
this.provideCursor = provideCursor;
RelativeSizeAxes = Axes.Both;
}
@ -102,15 +122,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{
switch (componentName)
{
case "cursortrail":
var tex = new Texture(renderer.WhitePixel);
case "cursor":
return provideCursor ? new Texture(renderer.WhitePixel) : null;
if (disjoint)
tex.ScaleAdjust = 1 / 25f;
return tex;
case "cursortrail":
return new Texture(renderer.WhitePixel);
case "cursormiddle":
return disjoint ? null : renderer.WhitePixel;
return provideMiddle ? null : renderer.WhitePixel;
}
return null;

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneResume : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false, AllowBackwardsSeeks);
[Test]
public void TestPauseViaKeyboard()
{
AddStep("move mouse to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
AddStep("press escape", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait for pause overlay", () => Player.ChildrenOfType<PauseOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("release escape", () => InputManager.ReleaseKey(Key.Escape));
AddStep("resume", () =>
{
InputManager.Key(Key.Down);
InputManager.Key(Key.Space);
});
AddUntilStep("pause overlay present", () => Player.DrawableRuleset.ResumeOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestPauseViaKeyboardWhenMouseOutsidePlayfield()
{
AddStep("move mouse outside playfield", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.BottomRight + new Vector2(1)));
AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
AddStep("press escape", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait for pause overlay", () => Player.ChildrenOfType<PauseOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("release escape", () => InputManager.ReleaseKey(Key.Escape));
AddStep("resume", () =>
{
InputManager.Key(Key.Down);
InputManager.Key(Key.Space);
});
AddUntilStep("pause overlay present", () => Player.DrawableRuleset.ResumeOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestPauseViaKeyboardWhenMouseOutsideScreen()
{
AddStep("move mouse outside playfield", () => InputManager.MoveMouseTo(new Vector2(-20)));
AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
AddStep("press escape", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait for pause overlay", () => Player.ChildrenOfType<PauseOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("release escape", () => InputManager.ReleaseKey(Key.Escape));
AddStep("resume", () =>
{
InputManager.Key(Key.Down);
InputManager.Key(Key.Space);
});
AddUntilStep("pause overlay not present", () => Player.DrawableRuleset.ResumeOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
}
}
}

View File

@ -6,11 +6,11 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Visual;
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene
{
private ManualOsuInputManager osuInputManager = null!;
private CursorContainer cursor = null!;
private GameplayCursorContainer cursor = null!;
private ResumeOverlay resume = null!;
private bool resumeFired;
@ -99,7 +99,17 @@ namespace osu.Game.Rulesets.Osu.Tests
private void loadContent()
{
Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } };
Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo)
{
Children = new Drawable[]
{
cursor = new GameplayCursorContainer(),
resume = new OsuResumeOverlay
{
GameplayCursor = cursor
},
}
};
resumeFired = false;
resume.ResumeAction = () => resumeFired = true;

View File

@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Tests
var skinnable = firstObject.ApproachCircle;
if (skin == null && skinnable?.Drawable is DefaultApproachCircle)
if (skin == null && skinnable.Drawable is DefaultApproachCircle)
// check for default skin provider
return true;
var text = skinnable?.Drawable as SpriteText;
var text = skinnable.Drawable as SpriteText;
return text?.Text == skin;
});

View File

@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
else if (score.Mods.Any(h => h is OsuModHidden))
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
else if (score.Mods.Any(m => m is OsuModHidden))
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
else if (score.Mods.Any(m => m is OsuModHidden))
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
accuracyValue *= 1.08;
if (score.Mods.Any(m => m is OsuModFlashlight))

View File

@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion;
private IBindable<int> stackHeight;
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
{
@ -56,6 +57,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
stackHeight.BindValueChanged(_ => updateConnectingPath());
updateConnectingPath();
}

View File

@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition;
private IBindable<float> hitObjectScale;
private IBindable<int> stackHeight;
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
{
@ -105,6 +106,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
stackHeight.BindValueChanged(_ => updateMarkerDisplay());
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
updateMarkerDisplay();

View File

@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
foreach (var splitPoint in controlPointsToSplitAt)
{
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null)
continue;
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
@ -403,7 +403,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
{
changeHandler?.BeginChange();
addControlPoint(rightClickPosition);
changeHandler?.EndChange();
}),
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
};

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckOsuAbnormalDifficultySettings : CheckAbnormalDifficultySettings
{
public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks osu relevant settings");
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
yield return issue;
if (OutOfRange("Approach rate", diff.ApproachRate, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue))
yield return issue;
if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue))
yield return issue;
if (OutOfRange("Circle size", diff.CircleSize, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
yield return issue;
if (OutOfRange("Drain rate", diff.DrainRate, out issue))
yield return issue;
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private partial class OsuEditorPlayfield : OsuPlayfield
{
protected override GameplayCursorContainer CreateCursor() => null;
protected override GameplayCursorContainer? CreateCursor() => null;
public OsuEditorPlayfield()
{

View File

@ -21,6 +21,9 @@ namespace osu.Game.Rulesets.Osu.Edit
new CheckTimeDistanceEquality(),
new CheckLowDiffOverlaps(),
new CheckTooShortSliders(),
// Settings
new CheckOsuAbnormalDifficultySettings(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -78,13 +78,21 @@ namespace osu.Game.Rulesets.Osu.Edit
public override bool HandleReverse()
{
var hitObjects = EditorBeatmap.SelectedHitObjects;
var hitObjects = EditorBeatmap.SelectedHitObjects
.OfType<OsuHitObject>()
.OrderBy(obj => obj.StartTime)
.ToList();
double endTime = hitObjects.Max(h => h.GetEndTime());
double startTime = hitObjects.Min(h => h.StartTime);
bool moreThanOneObject = hitObjects.Count > 1;
// the expectation is that even if the objects themselves are reversed temporally,
// the position of new combos in the selection should remain the same.
// preserve it for later before doing the reversal.
var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
foreach (var h in hitObjects)
{
if (moreThanOneObject)
@ -97,6 +105,12 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
// re-order objects by start time again after reversing, and restore new combo flag positioning
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
for (int i = 0; i < hitObjects.Count; ++i)
hitObjects[i].NewCombo = newComboOrder[i];
return true;
}

View File

@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private void updateState()
{
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
}
private OsuHitObject[]? objectsInRotation;

View File

@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
private RadioButton selectionCentreButton = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
{
this.rotationHandler = rotationHandler;
@ -59,13 +61,17 @@ namespace osu.Game.Rulesets.Osu.Edit
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Selection centre",
selectionCentreButton = new RadioButton("Selection centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
}
}
}
};
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
{
selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based rotation." : string.Empty;
};
}
protected override void LoadComplete()
@ -76,6 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select();
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
{
selectionCentreButton.Selected.Disabled = !e.NewValue;
}, true);
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);

View File

@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit
private EditorToolButton rotateButton = null!;
private Bindable<bool> canRotatePlayfieldOrigin = null!;
private Bindable<bool> canRotateSelectionOrigin = null!;
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public TransformToolboxGroup()
@ -51,9 +54,20 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.LoadComplete();
// aggregate two values into canRotate
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
void updateCanRotateAggregate()
{
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
}
// bindings to `Enabled` on the buttons are decoupled on purpose
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
canRotate.BindTo(RotationHandler.CanRotate);
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
}

View File

@ -95,12 +95,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary>
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
{
var oldHitAction = slider.HitArea.Hit;
slider.HitArea.Hit = () =>
{
oldHitAction?.Invoke();
return !slider.DrawableSlider.AllJudged;
};
slider.HitArea.CanBeHit = () => !slider.DrawableSlider.AllJudged;
}
private void applyEarlyFading(DrawableHitCircle circle)

View File

@ -1,16 +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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
@ -28,35 +24,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle
{
public OsuAction? HitAction => HitArea?.HitAction;
public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
public SkinnableDrawable ApproachCircle { get; private set; }
public HitReceptor HitArea { get; private set; }
public SkinnableDrawable CirclePiece { get; private set; }
public SkinnableDrawable ApproachCircle { get; private set; } = null!;
public HitReceptor HitArea { get; private set; } = null!;
public SkinnableDrawable CirclePiece { get; private set; } = null!;
protected override IEnumerable<Drawable> DimmablePieces => new[]
{
CirclePiece,
};
protected override IEnumerable<Drawable> DimmablePieces => new[] { CirclePiece };
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
private Container scaleContainer;
private InputManager inputManager;
private Container scaleContainer = null!;
private ShakeContainer shakeContainer = null!;
public DrawableHitCircle()
: this(null)
{
}
public DrawableHitCircle([CanBeNull] HitCircle h = null)
public DrawableHitCircle(HitCircle? h = null)
: base(h)
{
}
private ShakeContainer shakeContainer;
[BackgroundDependencyLoader]
private void load()
{
@ -73,14 +64,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
HitArea = new HitReceptor
{
Hit = () =>
{
if (AllJudged)
return false;
UpdateResult(true);
return true;
},
CanBeHit = () => !AllJudged,
Hit = () => UpdateResult(true)
},
shakeContainer = new ShakeContainer
{
@ -114,13 +99,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public override double LifetimeStart
{
get => base.LifetimeStart;
@ -155,7 +133,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyMinResult();
{
ApplyResult((r, position) =>
{
var circleResult = (OsuHitCircleJudgementResult)r;
circleResult.Type = r.Judgement.MinResult;
circleResult.CursorPositionAtHit = position;
}, computeHitPosition());
}
return;
}
@ -169,22 +155,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (result == HitResult.None || clickAction != ClickAction.Hit)
return;
Vector2? hitPosition = null;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
if (result.IsHit())
{
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
}
ApplyResult<(HitResult result, Vector2? position)>((r, state) =>
{
var circleResult = (OsuHitCircleJudgementResult)r;
circleResult.Type = state.result;
circleResult.CursorPositionAtHit = state.position;
}, (result, hitPosition));
}, (result, computeHitPosition()));
}
private Vector2? computeHitPosition()
{
if (HitArea.ClosestPressPosition is Vector2 screenSpaceHitPosition)
return HitObject.StackedPosition + (ToLocalSpace(screenSpaceHitPosition) - DrawSize / 2);
return null;
}
/// <summary>
@ -227,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
case ArmedState.Idle:
HitArea.HitAction = null;
HitArea.Reset();
break;
case ArmedState.Miss:
@ -247,9 +232,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// IsHovered is used
public override bool HandlePositionalInput => true;
public Func<bool> Hit;
/// <summary>
/// Whether the hitobject can still be hit at the current point in time.
/// </summary>
public required Func<bool> CanBeHit { get; set; }
public OsuAction? HitAction;
/// <summary>
/// An action that's invoked to perform the hit.
/// </summary>
public required Action Hit { get; set; }
/// <summary>
/// The <see cref="OsuAction"/> with which the hit was attempted.
/// </summary>
public OsuAction? HitAction { get; private set; }
/// <summary>
/// The closest position to the hit receptor at the point where the hit was attempted.
/// </summary>
public Vector2? ClosestPressPosition { get; private set; }
public HitReceptor()
{
@ -264,12 +265,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (!CanBeHit())
return false;
switch (e.Action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
if (IsHovered && (Hit?.Invoke() ?? false))
if (ClosestPressPosition is Vector2 curClosest)
{
float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre);
float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre);
if (newDist < oldDist)
ClosestPressPosition = e.ScreenSpaceMousePosition;
}
else
ClosestPressPosition = e.ScreenSpaceMousePosition;
if (IsHovered)
{
Hit();
HitAction ??= e.Action;
return true;
}
@ -283,13 +299,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
/// <summary>
/// Resets to a fresh state.
/// </summary>
public void Reset()
{
HitAction = null;
ClosestPressPosition = null;
}
}
private partial class ProxyableSkinnableDrawable : SkinnableDrawable
{
public override bool RemoveWhenNotAlive => false;
public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable> defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(lookup, defaultImplementation, confineMode)
{
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
private readonly SliderPath path = new SliderPath();
private readonly SliderPath path = new SliderPath { OptimiseCatmull = true };
public SliderPath Path
{

View File

@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Osu
: base(component)
{
}
protected override string RulesetPrefix => OsuRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly ISkin skin;
private const double disjoint_trail_time_separation = 1000 / 60.0;
private bool disjointTrail;
public bool DisjointTrail { get; private set; }
private double lastTrailTime;
private IBindable<float> cursorSize = null!;
@ -31,14 +31,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(OsuConfigManager config, ISkinSource skinSource)
{
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
if (disjointTrail)
// Cursor and cursor trail components are sourced from potentially different skin sources.
// Stable always chooses cursor trail disjoint behaviour based on the cursor texture lookup source, so we need to fetch where that occurred.
// See https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/SkinManager.cs#L269
var cursorProvider = skinSource.FindProvider(s => s.GetTexture("cursor") != null);
DisjointTrail = cursorProvider?.GetTexture("cursormiddle") == null;
if (DisjointTrail)
{
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
@ -57,19 +62,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
}
protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override double FadeDuration => DisjointTrail ? 150 : 500;
protected override float FadeExponent => 1;
protected override bool InterpolateMovements => !disjointTrail;
protected override bool InterpolateMovements => !DisjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override bool AvoidDrawingNearCursor => !disjointTrail;
protected override bool AvoidDrawingNearCursor => !DisjointTrail;
protected override void Update()
{
base.Update();
if (!disjointTrail || !currentPosition.HasValue)
if (!DisjointTrail || !currentPosition.HasValue)
return;
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!disjointTrail)
if (!DisjointTrail)
return base.OnMouseMove(e);
currentPosition = e.ScreenSpaceMousePosition;

View File

@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -191,16 +192,22 @@ namespace osu.Game.Rulesets.Osu.Statistics
for (int c = 0; c < points_per_dimension; c++)
{
HitPointType pointType = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius
? HitPointType.Hit
: HitPointType.Miss;
bool isHit = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius;
var point = new HitPoint(pointType, this)
if (isHit)
{
BaseColour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
};
points[r][c] = point;
points[r][c] = new HitPoint(this)
{
BaseColour = new Color4(102, 255, 204, 255)
};
}
else
{
points[r][c] = new MissPoint
{
BaseColour = new Color4(255, 102, 102, 255)
};
}
}
}
@ -250,40 +257,31 @@ namespace osu.Game.Rulesets.Osu.Statistics
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
float localRadius = localCentre.X * inner_portion * normalisedDistance;
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
// Find the most relevant hit point.
int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1);
int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1);
int r = (int)Math.Round(localPoint.Y);
int c = (int)Math.Round(localPoint.X);
PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment());
if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
}
private partial class HitPoint : Circle
private abstract partial class GridPoint : CompositeDrawable
{
/// <summary>
/// The base colour which will be lightened/darkened depending on the value of this <see cref="HitPoint"/>.
/// </summary>
public Color4 BaseColour;
private readonly HitPointType pointType;
private readonly AccuracyHeatmap heatmap;
public override bool IsPresent => Count > 0;
public override bool IsPresent => count > 0;
public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap)
{
this.pointType = pointType;
this.heatmap = heatmap;
RelativeSizeAxes = Axes.Both;
Alpha = 1;
}
private int count;
protected int Count { get; private set; }
/// <summary>
/// Increment the value of this point by one.
@ -291,7 +289,41 @@ namespace osu.Game.Rulesets.Osu.Statistics
/// <returns>The value after incrementing.</returns>
public int Increment()
{
return ++count;
return ++Count;
}
}
private partial class MissPoint : GridPoint
{
public MissPoint()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.Times
};
}
protected override void Update()
{
Alpha = 0.8f;
Colour = BaseColour;
}
}
private partial class HitPoint : GridPoint
{
private readonly AccuracyHeatmap heatmap;
public HitPoint(AccuracyHeatmap heatmap)
{
this.heatmap = heatmap;
RelativeSizeAxes = Axes.Both;
InternalChild = new Circle { RelativeSizeAxes = Axes.Both };
}
protected override void Update()
@ -307,10 +339,10 @@ namespace osu.Game.Rulesets.Osu.Statistics
float amount = 0;
// give some amount of alpha regardless of relative count
amount += non_relative_portion * Math.Min(1, count / 10f);
amount += non_relative_portion * Math.Min(1, Count / 10f);
// add relative portion
amount += (1 - non_relative_portion) * (count / heatmap.PeakValue);
amount += (1 - non_relative_portion) * (Count / heatmap.PeakValue);
// apply easing
amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
@ -318,15 +350,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
Debug.Assert(amount <= 1);
Alpha = Math.Min(amount / lighten_cutoff, 1);
if (pointType == HitPointType.Hit)
Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff));
Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff));
}
}
private enum HitPointType
{
Hit,
Miss
}
}
}

View File

@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -35,12 +35,16 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly JudgementPooler<DrawableOsuJudgement> judgementPooler;
// For osu! gameplay, everything is always on screen.
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer();
private readonly Container judgementAboveHitObjectLayer;
@ -81,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI
public IHitPolicy HitPolicy
{
get => hitPolicy;
[MemberNotNull(nameof(hitPolicy))]
set
{
hitPolicy = value ?? throw new ArgumentNullException(nameof(value));
@ -116,12 +121,12 @@ namespace osu.Game.Rulesets.Osu.UI
judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent);
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config, IBeatmap beatmap)
[BackgroundDependencyLoader]
private void load(OsuRulesetConfigManager? config, IBeatmap? beatmap)
{
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
var osuBeatmap = (OsuBeatmap)beatmap;
var osuBeatmap = (OsuBeatmap?)beatmap;
RegisterPool<HitCircle, DrawableHitCircle>(20, 100);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -12,6 +10,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osuTK.Graphics;
@ -19,15 +18,18 @@ namespace osu.Game.Rulesets.Osu.UI
{
public partial class OsuResumeOverlay : ResumeOverlay
{
private Container cursorScaleContainer;
private OsuClickToResumeCursor clickToResumeCursor;
private Container cursorScaleContainer = null!;
private OsuClickToResumeCursor clickToResumeCursor = null!;
private OsuCursorContainer localCursorContainer;
private OsuCursorContainer? localCursorContainer;
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
public override CursorContainer? LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
protected override LocalisableString Message => "Click the orange cursor to resume";
[Resolved]
private DrawableRuleset? drawableRuleset { get; set; }
[BackgroundDependencyLoader]
private void load()
{
@ -39,6 +41,13 @@ namespace osu.Game.Rulesets.Osu.UI
protected override void PopIn()
{
// Can't display if the cursor is outside the window.
if (GameplayCursor.LastFrameState == Visibility.Hidden || drawableRuleset?.Contains(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre) == false)
{
Resume();
return;
}
base.PopIn();
GameplayCursor.ActiveCursor.Hide();
@ -64,8 +73,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
public override bool HandlePositionalInput => true;
public Action ResumeRequested;
private Container scaleTransitionContainer;
public Action? ResumeRequested;
private Container scaleTransitionContainer = null!;
public OsuClickToResumeCursor()
{

View File

@ -4,7 +4,6 @@
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description>
<LangVersion>10</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Nuget">

View File

@ -0,0 +1,120 @@
// 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.Taiko.Edit.Checks;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Tests.Beatmaps;
using System.Linq;
namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks
{
[TestFixture]
public class CheckTaikoAbnormalDifficultySettingsTest
{
private CheckTaikoAbnormalDifficultySettings check = null!;
private readonly IBeatmap beatmap = new Beatmap<HitObject>();
[SetUp]
public void Setup()
{
check = new CheckTaikoAbnormalDifficultySettings();
beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo;
beatmap.Difficulty = new BeatmapDifficulty
{
OverallDifficulty = 5,
};
}
[Test]
public void TestNormalSettings()
{
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestOverallDifficultyTwoDecimals()
{
beatmap.Difficulty.OverallDifficulty = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestDrainRateTwoDecimals()
{
beatmap.Difficulty.DrainRate = 5.55f;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
}
[Test]
public void TestOverallDifficultyUnder()
{
beatmap.Difficulty.OverallDifficulty = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestOverallDifficultyOver()
{
beatmap.Difficulty.OverallDifficulty = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateUnder()
{
beatmap.Difficulty.DrainRate = -10;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
[Test]
public void TestDrainRateOver()
{
beatmap.Difficulty.DrainRate = 20;
var context = getContext();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
}
private BeatmapVerifierContext getContext()
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -95,6 +95,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
case SwellTick:
scoreIncrease = 300;
increaseCombo = false;
isBonus = true;
bonusResult = HitResult.IgnoreHit;
break;
case DrumRollTick:

View File

@ -0,0 +1,33 @@
// 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 osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Taiko.Edit.Checks
{
public class CheckTaikoAbnormalDifficultySettings : CheckAbnormalDifficultySettings
{
public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks taiko relevant settings");
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue))
yield return issue;
if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue))
yield return issue;
if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
yield return issue;
if (OutOfRange("Drain rate", diff.DrainRate, out issue))
yield return issue;
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Taiko.Edit.Checks;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class TaikoBeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckTaikoAbnormalDifficultySettings(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
return checks.SelectMany(check => check.Run(context));
}
}
}

View File

@ -188,6 +188,8 @@ namespace osu.Game.Rulesets.Taiko
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator();

View File

@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Taiko
: base(component)
{
}
protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
}
}

View File

@ -116,5 +116,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new TaikoReplayRecorder(score);
protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay();
}
}

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Taiko.UI
InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
{
animation.Origin = animation.Anchor = Anchor.BottomLeft;
animation.Scale = new Vector2(0.51f); // close enough to stable
// matches stable (https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34)
animation.Scale = new Vector2(0.6f);
});
RelativeSizeAxes = Axes.Both;

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
using osuTK;
@ -37,6 +38,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
[Test]
public void TestUnsupportedStoryboardEvents()
{
const string name = "Resources/storyboard_only_video.osu";
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1));
Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\""));
var memoryStream = encodeToLegacy(decoded);
var storyboard = new LegacyStoryboardDecoder().Decode(new LineBufferedReader(memoryStream));
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.EqualTo(1));
}
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name)
{

View File

@ -5,7 +5,9 @@ using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Legacy;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
@ -21,9 +23,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
{
var ruleset = new CatchRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
scoreInfo.Statistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 50,
@ -31,13 +33,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
[HitResult.Miss] = missCount,
[HitResult.LargeTickMiss] = largeTickMissCount
};
var score = new Score { ScoreInfo = scoreInfo };
var score = new Score { ScoreInfo = scoreInfo };
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
}
[Test]
public void ScoreWithMissIsNotPerfect()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
scoreInfo.Statistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 2,
[HitResult.Miss] = 1,
};
scoreInfo.MaximumStatistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 3
};
// Hit -> Miss -> Hit
scoreInfo.Combo = 1;
scoreInfo.MaxCombo = 1;
using (var ms = new MemoryStream())
{
new LegacyScoreEncoder(new Score { ScoreInfo = scoreInfo }, beatmap).Encode(ms, true);
ms.Seek(0, SeekOrigin.Begin);
using (var sr = new SerializationReader(ms))
{
sr.ReadByte(); // ruleset id
sr.ReadInt32(); // version
sr.ReadString(); // beatmap hash
sr.ReadString(); // username
sr.ReadString(); // score hash
sr.ReadInt16(); // count300
sr.ReadInt16(); // count100
sr.ReadInt16(); // count50
sr.ReadInt16(); // countGeki
sr.ReadInt16(); // countKatu
sr.ReadInt16(); // countMiss
sr.ReadInt32(); // total score
sr.ReadInt16(); // max combo
bool isPerfect = sr.ReadBoolean(); // full combo
Assert.That(isPerfect, Is.False);
}
}
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database
@ -77,6 +78,7 @@ namespace osu.Game.Tests.Database
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
using (new RealmRulesetStore(realm, storage))
{
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);

View File

@ -0,0 +1,128 @@
// 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.IO;
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckHitsoundsFormatTest
{
private CheckHitsoundsFormat check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckHitsoundsFormat();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile("wav") }
}
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
public void TestMp3Audio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateIncorrectFormat);
}
}
[Test]
public void TestOggAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestWavAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestWebmAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
}
}
[Test]
public void TestNotAnAudioFile()
{
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile("png") }
}
}
};
using (var resourceStream = TestResources.OpenResource("Textures/test-image.png"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestCorruptAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,112 @@
// 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.IO;
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public partial class CheckSongFormatTest
{
private CheckSongFormat check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckSongFormat();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile("mp3") }
}
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
public void TestMp3Audio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestOggAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestWavAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateIncorrectFormat);
}
}
[Test]
public void TestWebmAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
}
}
[Test]
public void TestCorruptAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -95,18 +95,6 @@ namespace osu.Game.Tests.Editing.Checks
}
}
[Test]
public void TestCorruptAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);

View File

@ -0,0 +1,145 @@
// 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.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Storyboards;
using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Tests.Editing.Checks
{
public class CheckUnusedAudioAtEndTest
{
private CheckUnusedAudioAtEnd check = null!;
private IBeatmap beatmapNotFullyMapped = null!;
private IBeatmap beatmapFullyMapped = null!;
[SetUp]
public void Setup()
{
check = new CheckUnusedAudioAtEnd();
beatmapNotFullyMapped = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 },
},
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
}
};
beatmapFullyMapped = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 9000 },
},
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" },
}
};
}
[Test]
public void TestEmptyBeatmap()
{
var context = getContext(new Beatmap<HitObject>());
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd);
}
[Test]
public void TestAudioNotFullyUsed()
{
var context = getContext(beatmapNotFullyMapped);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd);
}
[Test]
public void TestAudioNotFullyUsedWithVideo()
{
var storyboard = new Storyboard();
var video = new StoryboardVideo("abc123.mp4", 0);
storyboard.GetLayer("Video").Add(video);
var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard);
var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo);
}
[Test]
public void TestAudioNotFullyUsedWithStoryboardElement()
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
storyboard.GetLayer("Background").Add(sprite);
var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard);
var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo);
}
[Test]
public void TestAudioFullyUsed()
{
var context = getContext(beatmapFullyMapped);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
private BeatmapVerifierContext getContext(IBeatmap beatmap)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap, new Storyboard()).Object);
}
private BeatmapVerifierContext getContext(IBeatmap beatmap, Mock<IWorkingBeatmap> workingBeatmap)
{
return new BeatmapVerifierContext(beatmap, workingBeatmap.Object);
}
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard)
{
var mockTrack = new TrackVirtualStore(new FramedClock()).GetVirtual(10000, "virtual");
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack);
mockWorkingBeatmap.SetupGet(w => w.Storyboard).Returns(storyboard);
return mockWorkingBeatmap;
}
}
}

View File

@ -0,0 +1,89 @@
// 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.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckVideoResolutionTest
{
private CheckVideoResolution check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckVideoResolution();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files =
{
CheckTestHelpers.CreateMockFile("mp4"),
}
}
}
};
}
[Test]
public void TestNoVideo()
{
beatmap.BeatmapInfo.BeatmapSet?.Files.Clear();
var issues = check.Run(getContext(null)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestVideoAcceptableResolution()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestVideoHighResolution()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video-resolution-high.mp4"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckVideoResolution.IssueTemplateHighResolution);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var storyboard = new Storyboard();
var layer = storyboard.GetLayer("Video");
layer.Add(new StoryboardVideo("abc123.mp4", 0));
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
mockWorkingBeatmap.As<IWorkingBeatmap>().SetupGet(w => w.Storyboard).Returns(storyboard);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
@ -309,8 +312,10 @@ namespace osu.Game.Tests.NonVisual.Filtering
match = shouldMatch;
}
public bool Matches(BeatmapInfo beatmapInfo) => match;
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => match;
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods) => false;
}
}
}

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
@ -256,8 +259,8 @@ namespace osu.Game.Tests.NonVisual.Filtering
const string query = "status=r";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values);
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
}
[Test]
@ -268,10 +271,71 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim());
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values);
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
}
[Test]
public void TestApplyMultipleEqualityStatusQueries()
{
const string query = "status=ranked status=loved";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Is.Empty);
}
[Test]
public void TestApplyEqualStatusQueryWithMultipleValues()
{
const string query = "status=ranked,loved";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Is.Not.Empty);
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved));
}
[Test]
public void TestApplyRangeStatusMatches()
{
const string query = "status>=r";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Has.Count.EqualTo(4));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved));
}
[Test]
public void TestApplyRangeStatusWithMultipleMatchesQuery()
{
const string query = "status>=r,l";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Is.EquivalentTo(Enum.GetValues<BeatmapOnlineStatus>()));
}
[Test]
public void TestApplyTwoRangeStatusQuery()
{
const string query = "status>r status<l";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Has.Count.EqualTo(2));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified));
}
[Test]
public void TestApplyRangeAndEqualStatusQuery()
{
const string query = "status>r status=loved";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Is.Not.Empty);
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved));
}
[TestCase("creator")]
@ -441,7 +505,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
{
public string? CustomValue { get; set; }
public bool Matches(BeatmapInfo beatmapInfo) => true;
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => true;
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
{
@ -453,6 +517,8 @@ namespace osu.Game.Tests.NonVisual.Filtering
return false;
}
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods) => false;
}
private static readonly object[] correct_date_query_examples =

View File

@ -0,0 +1,235 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class TestSceneTimedDifficultyCalculation
{
[Test]
public void TestAttributesGeneratedForAllNonSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject { StartTime = 1 },
new TestHitObject
{
StartTime = 2,
Nested = 1
},
new TestHitObject { StartTime = 3 },
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(4));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object.
assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestAttributesNotGeneratedForSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
// The first object is usually skipped in all implementations
new TestHitObject
{
StartTime = 1,
Skip = true
},
// An intermediate skipped object.
new TestHitObject
{
StartTime = 2,
Skip = true
},
new TestHitObject { StartTime = 3 },
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestNestedObjectOnlyAddsParentOnce()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject
{
StartTime = 1,
Skip = true,
Nested = 2
},
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(2));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0]);
}
[Test]
public void TestSkippedLastObjectAddedInLastIteration()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject { StartTime = 1 },
new TestHitObject
{
StartTime = 2,
Skip = true
},
new TestHitObject
{
StartTime = 3,
Skip = true
},
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected)
{
Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected));
}
private class TestHitObject : HitObject
{
/// <summary>
/// Whether to skip generating a difficulty representation for this object.
/// </summary>
public bool Skip { get; set; }
/// <summary>
/// Whether to generate nested difficulty representations for this object, and if so, how many.
/// </summary>
public int Nested { get; set; }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
for (int i = 0; i < Nested; i++)
AddNested(new TestHitObject { StartTime = StartTime + 0.1 * i });
}
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => Enumerable.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PassThroughBeatmapConverter(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TestDifficultyCalculator(beatmap);
public override string Description => string.Empty;
public override string ShortName => string.Empty;
private class PassThroughBeatmapConverter : IBeatmapConverter
{
public event Action<HitObject, IEnumerable<HitObject>>? ObjectConverted
{
add { }
remove { }
}
public IBeatmap Beatmap { get; }
public PassThroughBeatmapConverter(IBeatmap beatmap)
{
Beatmap = beatmap;
}
public bool CanConvert() => true;
public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap;
}
}
private class TestDifficultyCalculator : DifficultyCalculator
{
public TestDifficultyCalculator(IWorkingBeatmap beatmap)
: base(new TestRuleset().RulesetInfo, beatmap)
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
=> new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() };
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
foreach (var obj in beatmap.HitObjects.OfType<TestHitObject>())
{
if (!obj.Skip)
objects.Add(new DifficultyHitObject(obj, obj, clockRate, objects, objects.Count));
foreach (var nested in obj.NestedHitObjects)
objects.Add(new DifficultyHitObject(nested, nested, clockRate, objects, objects.Count));
}
return objects;
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) };
private class PassThroughSkill : Skill
{
public PassThroughSkill(Mod[] mods)
: base(mods)
{
}
public override void Process(DifficultyHitObject current)
{
}
public override double DifficultyValue() => 1;
}
}
private class TestDifficultyAttributes : DifficultyAttributes
{
public HitObject[] Objects = Array.Empty<HitObject>();
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -173,6 +173,16 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu);
});
[Test]
public Task TestImportWithSubfolder() => runSkinTest(async osu =>
{
const string filename = "Archives/skin-with-subfolder-zip-entries.osk";
var import = await loadSkinIntoOsu(osu, new ImportTask(TestResources.OpenResource(filename), filename));
assertCorrectMetadata(import, $"Totally fully features skin [Real Skin with Real Features] [{filename[..^4]}]", "Unknown", 2.7m, osu);
Assert.That(import.PerformRead(r => r.Files.Count), Is.EqualTo(3));
});
#endregion
#region Cases where imports should be uniquely imported

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
namespace osu.Game.Tests.Skins
{
@ -21,6 +23,52 @@ namespace osu.Game.Tests.Skins
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Test]
public void TestRetrieveAndLegacyExportJapaneseFilename()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
// Ensure exporter encoding is correct (round trip)
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
}
[Test]
public void TestRetrieveAndNonLegacyExportJapaneseFilename()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
// Ensure exporter encoding is correct (round trip)
AddStep("export", () =>
{
outStream = new MemoryStream();
new BeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
}
[Test]
public void TestRetrieveOggAudio()
{
@ -45,6 +93,12 @@ namespace osu.Game.Tests.Skins
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
}
private IWorkingBeatmap importBeatmapFromStream(Stream stream)
{
var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
}
private IWorkingBeatmap importBeatmapFromArchives(string filename)
{
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();

View File

@ -89,13 +89,18 @@ namespace osu.Game.Tests.Visual.Background
setupUserSettings();
AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true })));
AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false);
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
AddStep("Trigger background preview", () =>
AddAssert("Background retained from song select", () =>
{
InputManager.MoveMouseTo(playerLoader.ScreenPos);
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
InputManager.MoveMouseTo(playerLoader);
return songSelect.IsBackgroundCurrent();
});
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddUntilStep("Screen is dimmed and blur applied", () =>
{
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
return songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied();
});
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur));
}

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing
{
this.getTargetContainer = getTargetContainer;
CanRotate.Value = true;
CanRotateSelectionOrigin.Value = true;
}
[CanBeNull]

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@ -83,6 +84,49 @@ namespace osu.Game.Tests.Visual.Editing
}
}
[Test]
public void TestDeleteDifficultyWithPendingChanges()
{
Guid deletedDifficultyID = Guid.Empty;
int countBeforeDeletion = 0;
string beatmapSetHashBefore = string.Empty;
AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true);
AddStep("store selected difficulty", () =>
{
deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID;
countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count;
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
});
AddStep("make change to difficulty", () =>
{
EditorBeatmap.BeginChange();
EditorBeatmap.BeatmapInfo.DifficultyName = "changin' things";
EditorBeatmap.EndChange();
});
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
AddAssert("dialog is deletion confirmation dialog", () => DialogOverlay.CurrentDialog, Is.InstanceOf<DeleteDifficultyConfirmationDialog>);
AddStep("confirm", () => InputManager.Key(Key.Number1));
AddUntilStep("no next dialog", () => DialogOverlay.CurrentDialog == null);
AddUntilStep("switched to different difficulty",
() => this.ChildrenOfType<EditorBeatmap>().SingleOrDefault() != null && EditorBeatmap.BeatmapInfo.ID != deletedDifficultyID);
AddAssert("difficulty is unattached from set",
() => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID));
AddAssert("beatmap set difficulty count decreased by one",
() => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1));
AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore));
AddAssert("difficulty is deleted from realm",
() => Realm.Run(r => r.Find<BeatmapInfo>(deletedDifficultyID)), () => Is.Null);
}
private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType<DrawableOsuMenuItem>()
.Single(item => item.ChildrenOfType<SpriteText>().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)));
}

View File

@ -0,0 +1,44 @@
// 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.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneDelayedResumeOverlay : OsuTestScene
{
private ResumeOverlay resume = null!;
private bool resumeFired;
[Cached]
private GameplayState gameplayState;
public TestSceneDelayedResumeOverlay()
{
gameplayState = TestGameplayState.Create(new OsuRuleset());
}
[SetUp]
public void SetUp() => Schedule(loadContent);
[Test]
public void TestResume()
{
AddStep("show", () => resume.Show());
AddUntilStep("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden);
}
private void loadContent()
{
Child = resume = new DelayedResumeOverlay();
resumeFired = false;
resume.ResumeAction = () => resumeFired = true;
}
}
}

View File

@ -10,6 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("disallow all lookups", () =>
{
storyboard.UseSkinSprites = false;
storyboard.AlwaysProvideTexture = false;
storyboard.ProvideResources = false;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow storyboard lookup", () =>
{
storyboard.UseSkinSprites = false;
storyboard.AlwaysProvideTexture = true;
storyboard.ProvideResources = true;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -67,13 +68,48 @@ namespace osu.Game.Tests.Visual.Gameplay
assertStoryboardSourced();
}
[TestCase(false)]
[TestCase(true)]
public void TestVideo(bool scaleTransformProvided)
{
AddStep("allow storyboard lookup", () =>
{
storyboard.ProvideResources = true;
});
AddStep("create video", () => SetContents(_ =>
{
var layer = storyboard.GetLayer("Video");
var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current);
if (scaleTransformProvided)
{
sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current + 1000, 1, 2);
sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1);
}
layer.Elements.Clear();
layer.Add(sprite);
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
storyboard.CreateDrawable()
}
};
}));
}
[Test]
public void TestSkinLookupPreferredOverStoryboard()
{
AddStep("allow all lookups", () =>
{
storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true;
storyboard.ProvideResources = true;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -91,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow skin lookup", () =>
{
storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = false;
storyboard.ProvideResources = false;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -109,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () =>
{
storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true;
storyboard.ProvideResources = true;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -127,7 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () =>
{
storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true;
storyboard.ProvideResources = true;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -142,7 +178,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () =>
{
storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true;
storyboard.ProvideResources = true;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -156,7 +192,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () =>
{
storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true;
storyboard.ProvideResources = true;
});
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -170,7 +206,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
}
private DrawableStoryboard createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
private Drawable createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
{
var layer = storyboard.GetLayer("Background");
@ -181,7 +217,14 @@ namespace osu.Game.Tests.Visual.Gameplay
layer.Elements.Clear();
layer.Add(sprite);
return storyboard.CreateDrawable().With(s => s.RelativeSizeAxes = Axes.Both);
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
storyboard.CreateDrawable()
}
};
}
private void assertStoryboardSourced()
@ -203,42 +246,52 @@ namespace osu.Game.Tests.Visual.Gameplay
return new TestDrawableStoryboard(this, mods);
}
public bool AlwaysProvideTexture { get; set; }
public bool ProvideResources { get; set; }
public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty;
public override string GetStoragePathFromStoryboardPath(string path) => ProvideResources ? path : string.Empty;
private partial class TestDrawableStoryboard : DrawableStoryboard
{
private readonly bool alwaysProvideTexture;
private readonly bool provideResources;
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
: base(storyboard, mods)
{
alwaysProvideTexture = storyboard.AlwaysProvideTexture;
provideResources = storyboard.ProvideResources;
}
protected override IResourceStore<byte[]> CreateResourceLookupStore() => alwaysProvideTexture
? new AlwaysReturnsTextureStore()
protected override IResourceStore<byte[]> CreateResourceLookupStore() => provideResources
? new ResourcesTextureStore()
: new ResourceStore<byte[]>();
internal class AlwaysReturnsTextureStore : IResourceStore<byte[]>
internal class ResourcesTextureStore : IResourceStore<byte[]>
{
private const string test_image = "Resources/Textures/test-image.png";
private readonly DllResourceStore store;
public AlwaysReturnsTextureStore()
public ResourcesTextureStore()
{
store = TestResources.GetStore();
}
public void Dispose() => store.Dispose();
public byte[] Get(string name) => store.Get(test_image);
public byte[] Get(string name) => store.Get(map(name));
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken);
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(map(name), cancellationToken);
public Stream GetStream(string name) => store.GetStream(test_image);
public Stream GetStream(string name) => store.GetStream(map(name));
private string map(string name)
{
switch (name)
{
case lookup_name:
return "Resources/Textures/test-image.png";
default:
return $"Resources/{name}";
}
}
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
}

View File

@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
protected override bool AllowBackwardsSeeks => true;
private bool seek;
[Test]
public void TestAllSamplesStopDuringSeek()
{
@ -42,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
if (!samples.Any(s => s.Playing))
return false;
Player.ChildrenOfType<GameplayClockContainer>().First().Seek(40000);
seek = true;
return true;
});
@ -55,10 +57,27 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playback still disabled", () => sampleDisabler.SamplePlaybackDisabled.Value);
AddStep("stop seeking", () => seek = false);
AddUntilStep("seek finished, sample playback enabled", () => !sampleDisabler.SamplePlaybackDisabled.Value);
AddUntilStep("any sample is playing", () => Player.ChildrenOfType<PausableSkinnableSound>().Any(s => s.IsPlaying));
}
protected override void Update()
{
base.Update();
if (seek)
{
// Frame stable playback is too fast to catch up these days.
//
// We want to keep seeking while asserting various test conditions, so
// continue to seek until we unset the flag.
var gameplayClockContainer = Player.ChildrenOfType<GameplayClockContainer>().First();
gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000);
}
}
private IEnumerable<PausableSkinnableSound> allSounds => Player.ChildrenOfType<PausableSkinnableSound>();
private IEnumerable<PausableSkinnableSound> allLoopingSounds => allSounds.Where(sound => sound.Looping);

View File

@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@ -24,9 +25,11 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@ -36,7 +39,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestPlayerLoader loader;
private TestPlayer player;
private bool epilepsyWarning;
private bool? epilepsyWarning;
private BeatmapOnlineStatus? onlineStatus;
[Resolved]
private AudioManager audioManager { get; set; }
@ -53,6 +57,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private readonly VolumeOverlay volumeOverlay;
[Cached]
private readonly OsuLogo logo;
[Cached(typeof(BatteryInfo))]
private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo();
@ -76,12 +83,24 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
},
changelogOverlay = new ChangelogOverlay()
changelogOverlay = new ChangelogOverlay(),
logo = new OsuLogo
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Scale = new Vector2(0.5f),
Position = new Vector2(128f),
},
});
}
[SetUp]
public void Setup() => Schedule(() => player = null);
public void Setup() => Schedule(() =>
{
player = null;
epilepsyWarning = null;
onlineStatus = null;
});
[SetUpSteps]
public override void SetUpSteps()
@ -118,8 +137,9 @@ namespace osu.Game.Tests.Visual.Gameplay
// Add intro time to test quick retry skipping (TestQuickRetry).
workingBeatmap.BeatmapInfo.AudioLeadIn = 60000;
// Turn on epilepsy warning to test warning display (TestEpilepsyWarning).
workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning;
// Set up data for testing disclaimer display.
workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false;
workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked;
Beatmap.Value = workingBeatmap;
@ -204,6 +224,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
}
[Test]
public void TestLoadNotBlockedOnOsuLogo()
{
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for load ready", () =>
{
moveMouse();
return player?.LoadState == LoadState.Ready;
});
// move mouse in logo while waiting for load to still proceed (it shouldn't be blocked when hovering logo).
AddUntilStep("move mouse in logo", () =>
{
moveMouse();
return !loader.IsCurrentScreen();
});
void moveMouse()
{
notificationOverlay.State.Value = Visibility.Hidden;
InputManager.MoveMouseTo(
logo.ScreenSpaceDrawQuad.TopLeft
+ (logo.ScreenSpaceDrawQuad.BottomRight - logo.ScreenSpaceDrawQuad.TopLeft)
* RNG.NextSingle(0.3f, 0.7f));
}
}
[Test]
public void TestLoadContinuation()
{
@ -334,13 +384,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning);
if (warning)
{
AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
}
AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Count(), () => Is.EqualTo(warning ? 1 : 0));
restoreVolumes();
}
@ -357,30 +401,45 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("epilepsy warning absent", () => getWarning() == null);
AddUntilStep("epilepsy warning absent", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Single().Alpha, () => Is.Zero);
restoreVolumes();
}
[Test]
public void TestEpilepsyWarningEarlyExit()
[TestCase(BeatmapOnlineStatus.Loved, 1)]
[TestCase(BeatmapOnlineStatus.Qualified, 1)]
[TestCase(BeatmapOnlineStatus.Graveyard, 0)]
public void TestStatusWarning(BeatmapOnlineStatus status, int expectedDisclaimerCount)
{
saveVolumes();
setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("disable epilepsy warning", () => epilepsyWarning = false);
AddStep("set beatmap status", () => onlineStatus = status);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0);
AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible);
AddAssert($"disclaimer count is {expectedDisclaimerCount}", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Count(), () => Is.EqualTo(expectedDisclaimerCount));
AddStep("exit early", () => loader.Exit());
restoreVolumes();
}
AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
[Test]
public void TestCombinedWarnings()
{
saveVolumes();
setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("disable epilepsy warning", () => epilepsyWarning = true);
AddStep("set beatmap status", () => onlineStatus = BeatmapOnlineStatus.Loved);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("disclaimer count is 2", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Count(), () => Is.EqualTo(2));
restoreVolumes();
}
@ -479,8 +538,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick());
}
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestSkinSourceContainer skinSource = null!;
private PausableSkinnableSound skinnableSound = null!;
private const string sample_lookup = "Gameplay/normal-sliderslide";
private const string sample_lookup = "Gameplay/Argon/normal-sliderslide";
[SetUpSteps]
public void SetUpSteps()

View File

@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneStoryboardWithIntro : PlayerTestScene
{
protected override bool HasCustomSteps => true;
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle { StartTime = firstObjectStartTime });
return beatmap;
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
{
return base.CreateWorkingBeatmap(beatmap, createStoryboard(storyboardStartTime));
}
private Storyboard createStoryboard(double startTime)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}
private double firstObjectStartTime;
private double storyboardStartTime;
[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 first hitobject time", () => firstObjectStartTime = 0);
AddStep("reset storyboard start time", () => storyboardStartTime = 0);
}
[TestCase(-5000, 0)]
[TestCase(-5000, 30000)]
public void TestStoryboardSingleSkip(double storyboardStart, double firstObject)
{
AddStep($"set storyboard start time to {storyboardStart}", () => storyboardStartTime = storyboardStart);
AddStep($"set first object start time to {firstObject}", () => firstObjectStartTime = firstObject);
CreateTest();
AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("skip performed", () => Player.ChildrenOfType<SkipOverlay>().Any(s => s.SkipCount == 1));
AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(firstObject - 2000));
}
[Test]
public void TestStoryboardDoubleSkip()
{
AddStep("set storyboard start time to -11000", () => storyboardStartTime = -11000);
AddStep("set first object start time to 11000", () => firstObjectStartTime = 11000);
CreateTest();
AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("skip performed", () => Player.ChildrenOfType<SkipOverlay>().Any(s => s.SkipCount == 1));
AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("skip performed", () => Player.ChildrenOfType<SkipOverlay>().Any(s => s.SkipCount == 2));
AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(9000));
}
}
}

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