mirror of
https://github.com/ppy/osu.git
synced 2026-05-21 18:10:40 +08:00
Merge branch 'master' into hitposition
This commit is contained in:
+10
-10
@@ -13,10 +13,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 8.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: dotnet restore osu.Desktop.slnf
|
||||
|
||||
- name: Restore inspectcode cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/inspectcode
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
|
||||
@@ -70,10 +70,10 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 8.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
@@ -99,16 +99,16 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: microsoft
|
||||
java-version: 11
|
||||
|
||||
- name: Install .NET 8.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
@@ -126,10 +126,10 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 8.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
|
||||
@@ -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: |
|
||||
@@ -140,7 +144,7 @@ jobs:
|
||||
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
|
||||
steps:
|
||||
- name: Checkout diffcalc-sheet-generator
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: ${{ env.EXECUTION_ID }}
|
||||
repository: 'smoogipoo/diffcalc-sheet-generator'
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Annotate CI run with test results
|
||||
uses: dorny/test-reporter@v1.6.0
|
||||
uses: dorny/test-reporter@v1.8.0
|
||||
with:
|
||||
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -13,23 +13,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install .NET 8.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Checkout ppy/osu
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: osu
|
||||
|
||||
- name: Checkout ppy/osu-tools
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ppy/osu-tools
|
||||
path: osu-tools
|
||||
|
||||
- name: Checkout ppy/osu-web
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ppy/osu-web
|
||||
path: osu-web
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
working-directory: ./osu-tools
|
||||
|
||||
- name: Create pull request with changes
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
title: Update mod definitions
|
||||
body: "This PR has been auto-generated to update the mod definitions to match ppy/osu@${{ github.ref_name }}."
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.217.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -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,7 +28,7 @@ namespace osu.Desktop
|
||||
{
|
||||
internal partial class DiscordRichPresence : Component
|
||||
{
|
||||
private const string client_id = "367827983903490050";
|
||||
private const string client_id = "1216669957799018608";
|
||||
|
||||
private DiscordRpcClient client = null!;
|
||||
|
||||
@@ -33,6 +40,15 @@ namespace osu.Desktop
|
||||
[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!;
|
||||
|
||||
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
||||
|
||||
@@ -40,7 +56,12 @@ namespace osu.Desktop
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -48,12 +69,18 @@ namespace osu.Desktop
|
||||
{
|
||||
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);
|
||||
// A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate.
|
||||
client.RegisterUriScheme();
|
||||
client.Subscribe(EventType.Join);
|
||||
client.OnJoin += onJoin;
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
|
||||
@@ -67,10 +94,11 @@ namespace osu.Desktop
|
||||
activity.BindTo(u.NewValue.Activity);
|
||||
}, true);
|
||||
|
||||
ruleset.BindValueChanged(_ => updateStatus());
|
||||
status.BindValueChanged(_ => updateStatus());
|
||||
activity.BindValueChanged(_ => updateStatus());
|
||||
privacyMode.BindValueChanged(_ => updateStatus());
|
||||
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
status.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||
|
||||
client.Initialize();
|
||||
}
|
||||
@@ -78,23 +106,44 @@ namespace osu.Desktop
|
||||
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.Online && activity.Value != null)
|
||||
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||
{
|
||||
client.ClearPresence();
|
||||
return;
|
||||
}
|
||||
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
||||
|
||||
updatePresence(hideIdentifiableInformation);
|
||||
client.SetPresence(presence);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private void updatePresence(bool hideIdentifiableInformation)
|
||||
{
|
||||
// user activity
|
||||
if (activity.Value != null)
|
||||
{
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited;
|
||||
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||
|
||||
@@ -120,7 +169,40 @@ 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,
|
||||
};
|
||||
|
||||
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
|
||||
@@ -131,16 +213,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;
|
||||
@@ -159,7 +268,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)
|
||||
{
|
||||
@@ -175,8 +308,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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Desktop.Performance;
|
||||
using osu.Desktop.Security;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
@@ -15,9 +16,11 @@ using osu.Framework;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Performance;
|
||||
using osu.Game.Utils;
|
||||
using SDL2;
|
||||
|
||||
@@ -28,6 +31,9 @@ namespace osu.Desktop
|
||||
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
|
||||
private ArchiveImportIPCChannel? archiveImportIPCChannel;
|
||||
|
||||
[Cached(typeof(IHighPerformanceSessionManager))]
|
||||
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
|
||||
|
||||
public OsuGameDesktop(string[]? args = null)
|
||||
: base(args)
|
||||
{
|
||||
@@ -86,8 +92,8 @@ namespace osu.Desktop
|
||||
[SupportedOSPlatform("windows")]
|
||||
private string? getStableInstallPathFromRegistry()
|
||||
{
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
|
||||
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Runtime;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Performance;
|
||||
|
||||
namespace osu.Desktop.Performance
|
||||
{
|
||||
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
|
||||
{
|
||||
private GCLatencyMode originalGCMode;
|
||||
|
||||
public IDisposable BeginSession()
|
||||
{
|
||||
enableHighPerformanceSession();
|
||||
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.disableHighPerformanceSession());
|
||||
}
|
||||
|
||||
private void enableHighPerformanceSession()
|
||||
{
|
||||
Logger.Log("Starting high performance session");
|
||||
|
||||
originalGCMode = GCSettings.LatencyMode;
|
||||
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
|
||||
|
||||
// Without doing this, the new GC mode won't kick in until the next GC, which could be at a more noticeable point in time.
|
||||
GC.Collect(0);
|
||||
}
|
||||
|
||||
private void disableHighPerformanceSession()
|
||||
{
|
||||
Logger.Log("Ending high performance session");
|
||||
|
||||
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
|
||||
GCSettings.LatencyMode = originalGCMode;
|
||||
|
||||
// No GC.Collect() as we were already collecting at a higher frequency in the old mode.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
@@ -69,7 +70,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;
|
||||
@@ -173,13 +175,16 @@ namespace osu.Desktop
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.InstallAssociations();
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UpdateAssociations();
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UninstallAssociations();
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
public static class Icons
|
||||
{
|
||||
/// <summary>
|
||||
/// Fully qualified path to the directory that contains icons (in the installation folder).
|
||||
/// </summary>
|
||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||
|
||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// 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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static class WindowsAssociationManager
|
||||
{
|
||||
private const string software_classes = @"Software\Classes";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the icon.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
|
||||
/// </summary>
|
||||
private const string default_icon = @"DefaultIcon";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the command line that the shell invokes.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
|
||||
/// </summary>
|
||||
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
|
||||
|
||||
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
|
||||
|
||||
/// <summary>
|
||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||
/// </summary>
|
||||
private const string program_id_prefix = "osu.File";
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||
};
|
||||
|
||||
private static readonly UriAssociation[] uri_associations =
|
||||
{
|
||||
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
||||
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Installs file and URI associations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void InstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates associations with latest definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void UpdateAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
|
||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
updateDescriptions(localisationManager);
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI association descriptions.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UninstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Uninstall();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Uninstall();
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to uninstall file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Installs or updates associations.
|
||||
/// </summary>
|
||||
private static void updateAssociations()
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Install();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Install();
|
||||
}
|
||||
|
||||
private static void updateDescriptions(LocalisationManager? localisation)
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
string getLocalisedString(LocalisableString s)
|
||||
{
|
||||
if (localisation == null)
|
||||
return s.ToString();
|
||||
|
||||
var b = localisation.GetLocalisedBindableString(s);
|
||||
b.UnbindAll();
|
||||
return b.Value;
|
||||
}
|
||||
}
|
||||
|
||||
#region Native interop
|
||||
|
||||
[DllImport("Shell32.dll")]
|
||||
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
private enum EventId
|
||||
{
|
||||
/// <summary>
|
||||
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
|
||||
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
|
||||
/// </summary>
|
||||
SHCNE_ASSOCCHANGED = 0x08000000
|
||||
}
|
||||
|
||||
private enum Flags : uint
|
||||
{
|
||||
SHCNF_IDLIST = 0x0000
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||
{
|
||||
private string programId => $@"{program_id_prefix}{Extension}";
|
||||
|
||||
/// <summary>
|
||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
// register a program id for the given extension
|
||||
using (var programKey = classes.CreateSubKey(programId))
|
||||
{
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
|
||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||
{
|
||||
// set ourselves as the default program
|
||||
extensionKey.SetValue(null, programId);
|
||||
|
||||
// add to the open with dialog
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey.SetValue(programId, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var programKey = classes.OpenSubKey(programId, true))
|
||||
programKey?.SetValue(null, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
|
||||
/// </summary>
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||
{
|
||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||
}
|
||||
|
||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||
{
|
||||
/// <summary>
|
||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public const string URL_PROTOCOL = @"URL Protocol";
|
||||
|
||||
/// <summary>
|
||||
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||
{
|
||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||
|
||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,7 @@
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Windows Icons">
|
||||
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
<file src="**.ico" target="lib\net45\"/>
|
||||
<file src="icon.png" target=""/>
|
||||
</files>
|
||||
</package>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 BenchmarkDotNet.Attributes;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkStringComparison
|
||||
{
|
||||
private string[] strings = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetUp()
|
||||
{
|
||||
strings = new string[10000];
|
||||
|
||||
for (int i = 0; i < strings.Length; ++i)
|
||||
strings[i] = Guid.NewGuid().ToString();
|
||||
|
||||
for (int i = 0; i < strings.Length; ++i)
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
strings[i] = strings[i].ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void OrdinalIgnoreCase() => compare(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
[Benchmark]
|
||||
public void OrdinalSortByCase() => compare(OrdinalSortByCaseStringComparer.DEFAULT);
|
||||
|
||||
[Benchmark]
|
||||
public void InvariantCulture() => compare(StringComparer.InvariantCulture);
|
||||
|
||||
private void compare(IComparer<string> comparer)
|
||||
{
|
||||
for (int i = 0; i < strings.Length; ++i)
|
||||
{
|
||||
for (int j = i + 1; j < strings.Length; ++j)
|
||||
_ = comparer.Compare(strings[i], strings[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("112643")]
|
||||
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Fruit(), 0.01, true],
|
||||
[new Droplet(), 0.01, true],
|
||||
[new TinyDroplet(), 0, false],
|
||||
[new Banana(), 0, false],
|
||||
[new BananaShower(), 0, false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,210 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: audio.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 65316
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 2
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 0
|
||||
|
||||
[Editor]
|
||||
DistanceSpacing: 1.4
|
||||
BeatDivisor: 4
|
||||
GridSize: 8
|
||||
TimelineZoom: 1.4
|
||||
|
||||
[Metadata]
|
||||
Title:Nanairo Symphony -TV Size-
|
||||
TitleUnicode:七色シンフォニー -TV Size-
|
||||
Artist:Coalamode.
|
||||
ArtistUnicode:コアラモード.
|
||||
Creator:Ascendance
|
||||
Version:Aru's Cup
|
||||
Source:四月は君の嘘
|
||||
Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
|
||||
BeatmapID:1041052
|
||||
BeatmapSetID:488149
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:3
|
||||
CircleSize:2.5
|
||||
OverallDifficulty:6
|
||||
ApproachRate:6
|
||||
SliderMultiplier:1.02
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
Video,500,"forty.avi"
|
||||
0,0,"cropped-1366-768-647733.jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
1155,387.096774193548,4,2,1,50,1,0
|
||||
15284,-100,4,2,1,60,0,0
|
||||
16638,-100,4,2,1,50,0,0
|
||||
41413,-100,4,2,1,60,0,0
|
||||
59993,-100,4,2,1,65,0,0
|
||||
66187,-100,4,2,1,70,0,1
|
||||
87284,-100,4,2,1,60,0,1
|
||||
87864,-100,4,2,1,70,0,0
|
||||
87961,-100,4,2,1,50,0,0
|
||||
88638,-100,4,2,1,30,0,0
|
||||
89413,-100,4,2,1,10,0,0
|
||||
89800,-100,4,2,1,5,0,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1 : 255,128,64
|
||||
Combo2 : 0,128,255
|
||||
Combo3 : 255,128,192
|
||||
Combo4 : 0,128,192
|
||||
|
||||
[HitObjects]
|
||||
208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
160,160,2122,1,0,0:0:0:0:
|
||||
272,160,2509,1,2,0:0:0:0:
|
||||
448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
384,96,4058,1,2,0:0:0:0:
|
||||
128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,64,5800,1,2,0:0:0:0:
|
||||
240,64,5993,1,2,0:0:0:0:
|
||||
288,64,6187,1,2,0:0:0:0:
|
||||
416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
|
||||
488,160,8122,2,0,L|376:160,1,102
|
||||
457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
400,288,10058,1,0,0:0:0:0:
|
||||
304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
|
||||
400,288,11606,1,0,0:0:0:0:
|
||||
240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
0,288,13154,1,0,0:0:0:0:
|
||||
112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
|
||||
288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
|
||||
312,208,15864,1,6,0:0:0:0:
|
||||
128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
432,176,20896,1,0,0:0:0:0:
|
||||
328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
328,176,22445,1,0,0:0:0:0:
|
||||
224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
224,176,23993,1,0,0:0:0:0:
|
||||
112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
416,176,25541,1,0,0:0:0:0:
|
||||
304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
480,112,27090,1,0,0:0:0:0:
|
||||
384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
432,112,28058,1,2,0:0:0:0:
|
||||
333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
272,272,30574,2,0,L|374:272,1,102
|
||||
424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
|
||||
224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
200,368,32703,1,2,0:0:0:0:
|
||||
376,368,33284,1,0,0:0:0:0:
|
||||
304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
280,16,36380,1,2,0:0:0:0:
|
||||
176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
176,224,37928,1,2,0:0:0:0:
|
||||
304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
144,264,38896,1,2,0:0:0:0:
|
||||
280,336,39477,2,0,L|336:336,1,51
|
||||
424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
|
||||
352,232,40445,1,4,0:1:0:0:
|
||||
160,224,41025,1,8,0:3:0:0:
|
||||
256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
400,40,41993,1,0,0:0:0:0:
|
||||
440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
|
||||
144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
56,136,45284,1,2,0:0:0:0:
|
||||
160,144,45671,1,8,0:0:0:0:
|
||||
264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
|
||||
360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
144,136,49154,1,2,0:0:0:0:
|
||||
248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
|
||||
256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
|
||||
256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
400,208,51284,1,0,0:0:0:0:
|
||||
352,240,51477,2,0,L|240:240,1,102
|
||||
128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
88,264,53025,1,2,0:0:0:0:
|
||||
168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
|
||||
432,120,54574,1,2,0:0:0:0:
|
||||
328,128,54961,1,8,0:0:0:0:
|
||||
224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
224,184,56509,1,8,0:3:0:0:
|
||||
328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
224,272,58445,5,2,0:0:0:0:
|
||||
320,296,58832,1,0,0:0:0:0:
|
||||
224,328,59219,1,2,0:0:0:0:
|
||||
120,328,59606,1,8,0:3:0:0:
|
||||
224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
160,112,64638,1,8,0:0:0:0:
|
||||
208,128,64832,1,8,0:0:0:0:
|
||||
256,144,65025,1,8,0:0:0:0:
|
||||
360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
176,208,68316,1,0,0:0:0:0:
|
||||
224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
|
||||
40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
|
||||
208,248,77800,1,8,0:0:0:0:
|
||||
320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
96,72,80703,1,0,0:0:0:0:
|
||||
40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
208,272,82445,1,10,0:0:0:0:
|
||||
312,272,82832,1,8,0:0:0:0:
|
||||
400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
|
||||
360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
|
||||
80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
|
||||
296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
|
||||
168,96,87864,1,4,0:0:0:0:
|
||||
256,192,88251,12,0,89800,0:0:0:0:
|
||||
@@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
float offsetPosition = hitObject.OriginalX;
|
||||
double startTime = hitObject.StartTime;
|
||||
|
||||
if (lastPosition == null)
|
||||
if (lastPosition == null ||
|
||||
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
|
||||
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
|
||||
// todo: should be revisited and corrected later probably.
|
||||
lastPosition == 0)
|
||||
{
|
||||
lastPosition = offsetPosition;
|
||||
lastStartTime = startTime;
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
private int fruitsHit;
|
||||
private int ticksHit;
|
||||
private int tinyTicksHit;
|
||||
private int tinyTicksMissed;
|
||||
private int misses;
|
||||
private int num300;
|
||||
private int num100;
|
||||
private int num50;
|
||||
private int numKatu;
|
||||
private int numMiss;
|
||||
|
||||
public CatchPerformanceCalculator()
|
||||
: base(new CatchRuleset())
|
||||
@@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
var catchAttributes = (CatchDifficultyAttributes)attributes;
|
||||
|
||||
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
||||
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
num300 = score.GetCount300() ?? 0; // HitResult.Great
|
||||
num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
|
||||
num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
|
||||
numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
|
||||
numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
|
||||
|
||||
// We are heavily relying on aim in catch the beat
|
||||
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||
@@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
||||
value *= lengthBonus;
|
||||
|
||||
value *= Math.Pow(0.97, misses);
|
||||
value *= Math.Pow(0.97, numMiss);
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
@@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
}
|
||||
|
||||
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
||||
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
|
||||
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
|
||||
private int totalComboHits() => misses + ticksHit + fruitsHit;
|
||||
private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
|
||||
private int totalSuccessfulHits() => num50 + num100 + num300;
|
||||
private int totalComboHits() => numMiss + num100 + num300;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@@ -21,6 +22,23 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
// matches stable.
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
|
||||
// the above early-return skips the failure check at the end of the same method:
|
||||
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
|
||||
// making it impossible to fail on a tiny droplet regardless of result.
|
||||
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);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
@@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
// No matter what, mania doesn't have passive HP drain.
|
||||
Assert.That(processor.DrainRate, Is.Zero);
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Note(), 0.01, true],
|
||||
[new HeadNote(), 0.01, true],
|
||||
[new TailNote(), 0.01, true],
|
||||
[new HoldNoteBody(), 0, true], // hold note break
|
||||
[new HoldNote(), 0, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup hierarchy", () => Child = new Container
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
Child = new Container
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@@ -17,9 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
|
||||
|
||||
protected ManiaSelectionBlueprint(T hitObject)
|
||||
@@ -28,14 +26,31 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
RelativeSizeAxes = Axes.None;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
private readonly IBindable<ScrollingDirection> directionBindable = new Bindable<ScrollingDirection>();
|
||||
|
||||
var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
directionBindable.BindTo(scrollingInfo.Direction);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
directionBindable.BindValueChanged(onDirectionChanged, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Anchor = Origin = anchor;
|
||||
foreach (var child in InternalChildren)
|
||||
child.Anchor = child.Origin = anchor;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
|
||||
Width = HitObjectContainer.DrawWidth;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -101,6 +102,14 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@@ -46,17 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key.
|
||||
upSprite = new Sprite
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Texture = skin.GetTexture(upImage),
|
||||
Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1
|
||||
},
|
||||
downSprite = new Sprite
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Texture = skin.GetTexture(downImage),
|
||||
Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Alpha = 0
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@@ -149,7 +148,18 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// Retrieves the total amount of columns across all stages in this playfield.
|
||||
/// </summary>
|
||||
public int TotalColumns => stages.Sum(s => s.Columns.Length);
|
||||
public int TotalColumns
|
||||
{
|
||||
get
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
foreach (var stage in stages)
|
||||
sum += stage.Columns.Length;
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
private Stage getStageByColumn(int column)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@@ -32,6 +33,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
[Test]
|
||||
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
||||
|
||||
[Test]
|
||||
public void TestPlayfieldBasedSize()
|
||||
{
|
||||
ModFlashlight mod = new OsuModFlashlight();
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = mod,
|
||||
PassCondition = () =>
|
||||
{
|
||||
var flashlightOverlay = Player.DrawableRuleset.Overlays
|
||||
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
|
||||
.First();
|
||||
|
||||
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("adjust playfield scale", () =>
|
||||
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDimsOnlyAfterStartTime()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new HitCircle(), 0.01, true],
|
||||
[new SliderHeadCircle(), 0.01, true],
|
||||
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SliderTick(), 0.01, true],
|
||||
[new SliderRepeat(new Slider()), 0.01, true],
|
||||
[new SliderTailCircle(new Slider()), 0, true],
|
||||
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new Slider(), 0, true],
|
||||
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SpinnerTick(), 0, false],
|
||||
[new SpinnerBonusTick(), 0, false],
|
||||
[new Spinner(), 0.01, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -457,6 +457,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertMidSliderJudgementFail();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindHandling()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Position = new Vector2(0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
|
||||
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = 3250 },
|
||||
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
|
||||
}, new Slider
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(250, 0),
|
||||
}, 250),
|
||||
});
|
||||
|
||||
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||
|
||||
AddStep("rewind to middle of slider", () => currentPlayer.Seek(time_during_slide_4));
|
||||
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||
}
|
||||
|
||||
private void assertAllMaxJudgements()
|
||||
{
|
||||
AddAssert("All judgements max", () =>
|
||||
|
||||
@@ -58,9 +58,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
|
||||
{
|
||||
if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be greater than {nameof(endIndex)}.");
|
||||
if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be less than 0.");
|
||||
if (endIndex < 0) throw new ArgumentOutOfRangeException(nameof(endIndex), $"{nameof(endIndex)} cannot be less than 0.");
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(endIndex);
|
||||
|
||||
int extendedEndIndex = endIndex;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -334,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
|
||||
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
|
||||
{
|
||||
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
|
||||
|
||||
int sourceIndex = -1;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Judgements
|
||||
{
|
||||
public class OsuSliderJudgementResult : OsuJudgementResult
|
||||
{
|
||||
public readonly Stack<(double time, bool tracking)> TrackingHistory = new Stack<(double, bool)>();
|
||||
|
||||
public OsuSliderJudgementResult(HitObject hitObject, Judgement judgement)
|
||||
: base(hitObject, judgement)
|
||||
{
|
||||
TrackingHistory.Push((double.NegativeInfinity, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
|
||||
// some spinners may not complete due to very minor decimal loss during calculation
|
||||
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
|
||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -80,11 +80,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
foreach (var piece in DimmablePieces)
|
||||
{
|
||||
// if the specified dimmable piece is a DHO, it is generally not safe to tack transforms onto it directly
|
||||
// as they may be cleared via the `updateState()` DHO flow,
|
||||
// so use `ApplyCustomUpdateState` instead. which does not have this pitfall.
|
||||
if (piece is DrawableHitObject drawableObjectPiece)
|
||||
{
|
||||
// this method can be called multiple times, and we don't want to subscribe to the event more than once,
|
||||
// so this is what it is going to have to be...
|
||||
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
|
||||
}
|
||||
else
|
||||
applyDim(piece);
|
||||
}
|
||||
|
||||
void applyDim(Drawable piece)
|
||||
{
|
||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
piece.FadeColour(Color4.White, 100);
|
||||
}
|
||||
|
||||
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||
}
|
||||
|
||||
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||
|
||||
@@ -14,8 +14,10 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
@@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
public new OsuSliderJudgementResult Result => (OsuSliderJudgementResult)base.Result;
|
||||
|
||||
public DrawableSliderHead HeadCircle => headContainer.Child;
|
||||
public DrawableSliderTail TailCircle => tailContainer.Child;
|
||||
|
||||
@@ -39,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override IEnumerable<Drawable> DimmablePieces => new Drawable[]
|
||||
{
|
||||
HeadCircle,
|
||||
// HeadCircle should not be added to this list, as it handles dimming itself
|
||||
TailCircle,
|
||||
repeatContainer,
|
||||
Body,
|
||||
@@ -134,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new OsuSliderJudgementResult(HitObject, judgement);
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.OnApply();
|
||||
|
||||
Position = HitObject.Position - DrawableSlider.Position;
|
||||
hasRotation = false;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
|
||||
@@ -146,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
break;
|
||||
}
|
||||
|
||||
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
float aimRotation = float.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
|
||||
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
|
||||
|
||||
|
||||
@@ -279,10 +279,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (HandleUserInput)
|
||||
{
|
||||
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
|
||||
bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
|
||||
|
||||
RotationTracker.Tracking = !Result.HasResult
|
||||
&& correctButtonPressed
|
||||
&& correctButtonPressed()
|
||||
&& isValidSpinningTime;
|
||||
}
|
||||
|
||||
@@ -292,11 +291,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// Ticks can theoretically be judged at any point in the spinner's duration.
|
||||
// A tick must be alive to correctly play back samples,
|
||||
// but for performance reasons, we only want to keep the next tick alive.
|
||||
var next = NestedHitObjects.FirstOrDefault(h => !h.Judged);
|
||||
DrawableHitObject nextTick = null;
|
||||
|
||||
foreach (var nested in NestedHitObjects)
|
||||
{
|
||||
if (!nested.Judged)
|
||||
{
|
||||
nextTick = nested;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// See default `LifetimeStart` as set in `DrawableSpinnerTick`.
|
||||
if (next?.LifetimeStart == double.MaxValue)
|
||||
next.LifetimeStart = HitObject.StartTime;
|
||||
if (nextTick?.LifetimeStart == double.MaxValue)
|
||||
nextTick.LifetimeStart = HitObject.StartTime;
|
||||
}
|
||||
|
||||
private bool correctButtonPressed()
|
||||
{
|
||||
if (OsuActionInputManager == null)
|
||||
return false;
|
||||
|
||||
foreach (var action in OsuActionInputManager.PressedActions)
|
||||
{
|
||||
if (action == OsuAction.LeftButton || action == OsuAction.RightButton)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
|
||||
@@ -5,11 +5,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public bool Tracking { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private IGameplayClock? gameplayClock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
|
||||
///
|
||||
@@ -49,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public SliderInputManager(DrawableSlider slider)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.slider.HitObjectApplied += resetState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -208,6 +215,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
|
||||
private void updateTracking(bool isValidTrackingPosition)
|
||||
{
|
||||
if (gameplayClock?.IsRewinding == true)
|
||||
{
|
||||
var trackingHistory = slider.Result.TrackingHistory;
|
||||
while (trackingHistory.TryPeek(out var historyEntry) && Time.Current < historyEntry.time)
|
||||
trackingHistory.Pop();
|
||||
|
||||
Debug.Assert(trackingHistory.Count > 0);
|
||||
|
||||
Tracking = trackingHistory.Peek().tracking;
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasTracking = Tracking;
|
||||
|
||||
// from the point at which the head circle is hit, this will be non-null.
|
||||
// it may be null if the head circle was missed.
|
||||
OsuAction? headCircleHitAction = getInitialHitAction();
|
||||
@@ -247,6 +268,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
&& isValidTrackingPosition
|
||||
// valid action
|
||||
&& validTrackingAction;
|
||||
|
||||
if (wasTracking != Tracking)
|
||||
slider.Result.TrackingHistory.Push((Time.Current, Tracking));
|
||||
}
|
||||
|
||||
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
|
||||
@@ -264,5 +288,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
|
||||
}
|
||||
|
||||
private void resetState(DrawableHitObject obj)
|
||||
{
|
||||
Tracking = false;
|
||||
timeToAcceptAnyKeyAfter = null;
|
||||
lastPressedActions.Clear();
|
||||
screenSpaceMousePosition = null;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
slider.HitObjectApplied -= resetState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +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 System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
public partial class OsuInputManager : RulesetInputManager<OsuAction>
|
||||
{
|
||||
public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
|
||||
public SlimReadOnlyListWrapper<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
|
||||
|
||||
/// <summary>
|
||||
/// Whether gameplay input buttons should be allowed.
|
||||
|
||||
@@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
// 0.05 rad/ms, or ~477 RPM, as per stable.
|
||||
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
|
||||
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
|
||||
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
|
||||
float radsPerMillisecond = float.DegreesToRadians(spin_rpm * 360) / 60000;
|
||||
|
||||
switch (h)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
@@ -111,42 +110,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||
fadeCounterOnTimeStart();
|
||||
updateSpmAlpha();
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
fadeCounterOnTimeStart();
|
||||
}
|
||||
|
||||
private void fadeCounterOnTimeStart()
|
||||
private void updateSpmAlpha()
|
||||
{
|
||||
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSpinner.IsNotNull())
|
||||
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
|
||||
else
|
||||
spmContainer.Alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Origin = Anchor.Centre,
|
||||
Colour = Color4.White.Opacity(0.25f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = 90 - arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
background.Alpha = spinner.Progress >= 1 ? 0 : 1;
|
||||
|
||||
fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(90 - fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(90 - fill.Progress * 180);
|
||||
}
|
||||
|
||||
private partial class ProgressFill : CircularProgress
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = -arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
base.Update();
|
||||
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(-fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(-fill.Progress * 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
|
||||
|
||||
// account for the sprite being used for the default approach circle being taken from stable,
|
||||
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
|
||||
// In triangles and argon skins, we expanded hitcircles to take up the full 128 px which are clickable,
|
||||
// but still use the old approach circle sprite. To make it feel correct (ie. disappear as it collides
|
||||
// with the hitcircle, *not when it overlaps the border*) we need to expand it slightly.
|
||||
Scale = new Vector2(128 / 118f);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
@@ -117,42 +116,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||
fadeCounterOnTimeStart();
|
||||
updateSpmAlpha();
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
fadeCounterOnTimeStart();
|
||||
}
|
||||
|
||||
private void fadeCounterOnTimeStart()
|
||||
private void updateSpmAlpha()
|
||||
{
|
||||
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSpinner.IsNotNull())
|
||||
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
|
||||
else
|
||||
spmContainer.Alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
SnakingOut.BindTo(configSnakingOut);
|
||||
|
||||
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
||||
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
if (mousePosition is Vector2 pos)
|
||||
{
|
||||
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||
float thisAngle = -float.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||
float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
|
||||
|
||||
// Normalise the delta to -180 .. 180
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -33,14 +32,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
drawableSpinner.HitObjectApplied += resetState;
|
||||
}
|
||||
|
||||
private RotationRecord lastRecord;
|
||||
|
||||
public void SetRotation(float currentRotation)
|
||||
{
|
||||
// If we've gone back in time, it's fine to work with a fresh set of records for now
|
||||
if (records.Count > 0 && Time.Current < records.Last().Time)
|
||||
if (records.Count > 0 && Time.Current < lastRecord.Time)
|
||||
records.Clear();
|
||||
|
||||
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
||||
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time))
|
||||
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, lastRecord.Time))
|
||||
return;
|
||||
|
||||
if (records.Count > 0)
|
||||
@@ -52,11 +53,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
||||
}
|
||||
|
||||
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||
records.Enqueue(lastRecord = new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||
}
|
||||
|
||||
private void resetState(DrawableHitObject hitObject)
|
||||
{
|
||||
lastRecord = default;
|
||||
result.Value = 0;
|
||||
records.Clear();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
@@ -26,10 +25,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
var texture = skin.GetTexture(@"approachcircle");
|
||||
Debug.Assert(texture != null);
|
||||
Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
|
||||
|
||||
// account for the sprite being used for the default approach circle being taken from stable,
|
||||
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
|
||||
Scale = new Vector2(128 / 118f);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
|
||||
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
|
||||
this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
|
||||
this.ScaleTo(1f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
|
||||
.FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
|
||||
this.FadeOut();
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
|
||||
this.FadeInFromZero(spinner.TimeFadeIn / 2);
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
|
||||
this.FadeInFromZero(spinner.TimeFadeIn);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -23,29 +23,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private partial class LegacyDrawableSliderPath : DrawableSliderPath
|
||||
{
|
||||
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
|
||||
|
||||
protected new float CalculatedBorderPortion
|
||||
// Roughly matches osu!stable's slider border portions.
|
||||
=> base.CalculatedBorderPortion * 0.77f;
|
||||
|
||||
protected override Color4 ColourAt(float position)
|
||||
{
|
||||
float realBorderPortion = shadow_portion + CalculatedBorderPortion;
|
||||
float realGradientPortion = 1 - realBorderPortion;
|
||||
|
||||
if (position <= shadow_portion)
|
||||
return new Color4(0f, 0f, 0f, 0.25f * position / shadow_portion);
|
||||
|
||||
if (position <= realBorderPortion)
|
||||
return BorderColour;
|
||||
|
||||
position -= realBorderPortion;
|
||||
// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L99
|
||||
// float aaWidth = Math.Min(Math.Max(0.5f / PathRadius, 3.0f / 256.0f), 1.0f / 16.0f);
|
||||
// applying the aa_width constant from stable makes sliders blurry, especially on CS>5. set to zero for now.
|
||||
// this might be related to SmoothPath applying AA internally, but disabling that does not seem to have much of an effect.
|
||||
const float aa_width = 0f;
|
||||
|
||||
Color4 shadow = new Color4(0, 0, 0, 0.25f);
|
||||
Color4 outerColour = AccentColour.Darken(0.1f);
|
||||
Color4 innerColour = lighten(AccentColour, 0.5f);
|
||||
|
||||
return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
|
||||
// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L59-L70
|
||||
const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
|
||||
const float border_portion = 0.1875f;
|
||||
|
||||
if (position <= shadow_portion - aa_width)
|
||||
return LegacyUtils.InterpolateNonLinear(position, Color4.Black.Opacity(0f), shadow, 0, shadow_portion - aa_width);
|
||||
|
||||
if (position <= shadow_portion + aa_width)
|
||||
return LegacyUtils.InterpolateNonLinear(position, shadow, BorderColour, shadow_portion - aa_width, shadow_portion + aa_width);
|
||||
|
||||
if (position <= border_portion - aa_width)
|
||||
return BorderColour;
|
||||
|
||||
if (position <= border_portion + aa_width)
|
||||
return LegacyUtils.InterpolateNonLinear(position, BorderColour, outerColour, border_portion - aa_width, border_portion + aa_width);
|
||||
|
||||
return LegacyUtils.InterpolateNonLinear(position, outerColour, innerColour, border_portion + aa_width, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public enum OsuSkinConfiguration
|
||||
{
|
||||
SliderBorderSize,
|
||||
SliderPathRadius,
|
||||
CursorCentre,
|
||||
CursorExpand,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -228,7 +227,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
|
||||
|
||||
points.Clear();
|
||||
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
|
||||
|
||||
for (int i = firstVisiblePointIndex; i < futurePointIndex; i++)
|
||||
points.Add(Source.SmokePoints[i]);
|
||||
}
|
||||
|
||||
protected sealed override void Draw(IRenderer renderer)
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,10 +235,13 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
Vector2 localPoint = FindRelativeHitPosition(start, end, hitPoint, radius, new Vector2(points_per_dimension - 1) / 2, inner_portion, rotation);
|
||||
|
||||
// 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();
|
||||
}
|
||||
@@ -259,35 +269,23 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
|
||||
//
|
||||
// We also need to apply the anti-clockwise rotation.
|
||||
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
|
||||
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
|
||||
|
||||
float localRadius = localCentre.X * innerPortion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
|
||||
float localRadius = localCentre.X * innerPortion * normalisedDistance;
|
||||
return localCentre + localRadius * rotatedCoordinate;
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -295,7 +293,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()
|
||||
@@ -311,10 +343,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));
|
||||
@@ -322,15 +354,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -39,6 +39,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 || !Contains(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre))
|
||||
{
|
||||
Resume();
|
||||
return;
|
||||
}
|
||||
|
||||
base.PopIn();
|
||||
|
||||
GameplayCursor.ActiveCursor.Hide();
|
||||
|
||||
@@ -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">
|
||||
|
||||
+120
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissHitAndHitSwell()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[1].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, fail expected after miss
|
||||
[new Hit(), true],
|
||||
[new Hit.StrongNestedHit(new Hit()), false],
|
||||
[new DrumRollTick(new DrumRoll()), false],
|
||||
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
|
||||
[new DrumRoll(), false],
|
||||
[new SwellTick(), false],
|
||||
[new Swell(), false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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.Rulesets.Taiko.Skinning.Legacy;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public partial class TestSceneTaikoPlayerScroller : LegacySkinPlayerTestScene
|
||||
{
|
||||
private Storyboard? currentStoryboard;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[Test]
|
||||
public void TestForegroundSpritesHidesScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller not present", () => !this.ChildrenOfType<LegacyTaikoScroller>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlaySpritesKeepsScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller present", () => this.ChildrenOfType<LegacyTaikoScroller>().Single().IsPresent);
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> base.CreateWorkingBeatmap(beatmap, currentStoryboard ?? storyboard);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
case SwellTick:
|
||||
scoreIncrease = 300;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.IgnoreHit;
|
||||
break;
|
||||
|
||||
case DrumRollTick:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
@@ -39,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
[CanBeNull]
|
||||
private SkinnableDrawable scroller;
|
||||
|
||||
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
@@ -48,16 +53,24 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] GameplayState gameplayState)
|
||||
{
|
||||
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar));
|
||||
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
var spriteElements = gameplayState?.Storyboard.Layers.Where(l => l.Name != @"Overlay")
|
||||
.SelectMany(l => l.Elements)
|
||||
.OfType<StoryboardSprite>()
|
||||
.DistinctBy(e => e.Path) ?? Enumerable.Empty<StoryboardSprite>();
|
||||
|
||||
if (spriteElements.Count() < 10)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
}
|
||||
|
||||
KeyBindingInputManager.Add(new DrumTouchInputArea());
|
||||
}
|
||||
@@ -76,7 +89,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
|
||||
if (scroller != null)
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
}
|
||||
|
||||
public MultiplierControlPoint ControlPointAt(double time)
|
||||
@@ -101,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
|
||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
public class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private readonly int beatmapVersion;
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using 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;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
public class LegacyScoreEncoderTest
|
||||
{
|
||||
[TestCase(1, 3)]
|
||||
[TestCase(1, 0)]
|
||||
[TestCase(0, 3)]
|
||||
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,
|
||||
[HitResult.LargeTickHit] = 5,
|
||||
[HitResult.Miss] = missCount,
|
||||
[HitResult.LargeTickMiss] = largeTickMissCount
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
var encoder = new LegacyScoreEncoder(score, beatmap);
|
||||
encoder.Encode(encodeStream);
|
||||
|
||||
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
||||
|
||||
var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
|
||||
var decodedAfterEncode = decoder.Parse(decodeStream);
|
||||
return decodedAfterEncode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user