1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 17:27:24 +08:00

Merge branch 'master' into estimation-removal

This commit is contained in:
Fina 2024-05-25 14:21:34 -07:00 committed by GitHub
commit 8dea601329
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
459 changed files with 11267 additions and 3531 deletions

View File

@ -21,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2023.1117.0", "version": "2024.517.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

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

1
.gitignore vendored
View File

@ -341,3 +341,4 @@ inspectcode
FodyWeavers.xsd FodyWeavers.xsd
.idea/.idea.osu.Desktop/.idea/misc.xml .idea/.idea.osu.Desktop/.idea/misc.xml
.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml

View File

@ -2,7 +2,6 @@
<Project> <Project>
<PropertyGroup Label="C#"> <PropertyGroup Label="C#">
<LangVersion>12.0</LangVersion> <LangVersion>12.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

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

View File

@ -1,76 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
namespace osu.Android
{
public partial class AndroidJoystickSettings : SettingsSubsection
{
protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
private readonly AndroidJoystickHandler joystickHandler;
private readonly Bindable<bool> enabled = new BindableBool(true);
private SettingsSlider<float> deadzoneSlider = null!;
private Bindable<float> handlerDeadzone = null!;
private Bindable<float> localDeadzone = null!;
public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
{
this.joystickHandler = joystickHandler;
}
[BackgroundDependencyLoader]
private void load()
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
localDeadzone = handlerDeadzone.GetUnboundCopy();
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = enabled
},
deadzoneSlider = new SettingsSlider<float>
{
LabelText = JoystickSettingsStrings.DeadzoneThreshold,
KeyboardStep = 0.01f,
DisplayAsPercentage = true,
Current = localDeadzone,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
enabled.BindTo(joystickHandler.Enabled);
enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
handlerDeadzone.BindValueChanged(val =>
{
bool disabled = localDeadzone.Disabled;
localDeadzone.Disabled = false;
localDeadzone.Value = val.NewValue;
localDeadzone.Disabled = disabled;
}, true);
localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
}
}
}

View File

@ -1,97 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android.OS;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Android
{
public partial class AndroidMouseSettings : SettingsSubsection
{
private readonly AndroidMouseHandler mouseHandler;
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
private Bindable<double> handlerSensitivity = null!;
private Bindable<double> localSensitivity = null!;
private Bindable<bool> relativeMode = null!;
public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
{
this.mouseHandler = mouseHandler;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig)
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
localSensitivity = handlerSensitivity.GetUnboundCopy();
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
// High precision/pointer capture is only available on Android 8.0 and up
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.HighPrecisionMouse,
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
Current = relativeMode,
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
},
new MouseSettings.SensitivitySetting
{
LabelText = MouseSettingsStrings.CursorSensitivity,
Current = localSensitivity,
},
});
}
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
});
}
protected override void LoadComplete()
{
base.LoadComplete();
relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
handlerSensitivity.BindValueChanged(val =>
{
bool disabled = localSensitivity.Disabled;
localSensitivity.Disabled = false;
localSensitivity.Value = val.NewValue;
localSensitivity.Disabled = disabled;
}, true);
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
}
}
}

View File

@ -5,13 +5,9 @@ using System;
using Android.App; using Android.App;
using Microsoft.Maui.Devices; using Microsoft.Maui.Devices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input.Handlers;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
@ -88,24 +84,6 @@ namespace osu.Android
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
switch (handler)
{
case AndroidMouseHandler mh:
return new AndroidMouseSettings(mh);
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
case AndroidTouchHandler th:
return new TouchSettings(th);
default:
return base.CreateSettingsSubsectionFor(handler);
}
}
private class AndroidBatteryInfo : BatteryInfo private class AndroidBatteryInfo : BatteryInfo
{ {
public override double? ChargeLevel => Battery.ChargeLevel; public override double? ChargeLevel => Battery.ChargeLevel;

View File

@ -1,24 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector android:height="108dp" android:viewportHeight="434"
android:width="108dp" android:viewportWidth="434" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
android:height="108dp" <path android:fillColor="#000000" android:pathData="M299.36,178.05C303.08,178.05 305.62,180.81 305.62,184.62V219.92C305.62,223.74 303.08,226.5 299.36,226.5C295.55,226.5 293.11,223.74 293.11,219.92V184.62C293.11,180.81 295.55,178.05 299.36,178.05ZM299.36,248.97C294.81,248.97 291.2,245.37 291.2,240.81C291.2,236.35 294.81,232.75 299.36,232.75C303.92,232.75 307.53,236.35 307.53,240.81C307.53,245.37 303.92,248.97 299.36,248.97Z"/>
android:viewportWidth="108" <path android:fillColor="#000000" android:pathData="M276.52,195.12C280.34,195.12 282.77,197.87 282.77,201.58V225.01C282.77,242.29 272.12,248.97 259.19,248.97C246.15,248.97 235.49,242.29 235.49,225.01V201.58C235.49,197.87 237.93,195.12 241.75,195.12C245.46,195.12 248,197.87 248,201.58V224.16C248,233.6 251.98,237.31 259.19,237.31C266.29,237.31 270.27,233.6 270.27,224.16V201.58C270.27,197.87 272.81,195.12 276.52,195.12Z"/>
android:viewportHeight="108"> <path android:fillColor="#000000" android:pathData="M200.02,209.43C200.02,212.93 203.63,214.2 210.52,215.79C220.06,218.12 229.18,220.56 229.18,232.33C229.18,243.78 220.7,248.97 208.51,248.97C198.43,248.97 191.12,245.47 187.73,241.44C185.08,238.26 185.4,235.51 187.94,233.07C191.12,229.99 193.88,231.27 195.68,232.86C198.54,235.51 202.04,238.26 208.82,238.26C213.91,238.26 217.09,236.57 217.09,233.18C217.09,229.78 213.7,228.62 204.8,226.18C196,223.74 188.15,221.41 188.15,210.91C188.15,199.15 197.69,194.27 208.29,194.27C214.34,194.27 221.23,195.86 225.47,200.42C227.27,202.22 228.65,204.76 225.47,208.26C222.29,211.55 219.96,210.6 217.73,208.9C215.71,207.41 212.22,204.87 206.92,204.87C203.31,204.87 200.02,206.04 200.02,209.43Z"/>
<path <path android:fillColor="#000000" android:pathData="M153.74,248.97C138.46,248.97 127.5,237.27 127.5,221.53C127.5,205.68 138.46,194.09 153.74,194.09C169.03,194.09 179.99,205.68 179.99,221.53C179.99,237.27 169.03,248.97 153.74,248.97ZM153.74,237.27C162.59,237.27 168.12,230.46 168.12,221.53C168.12,212.6 162.59,205.68 153.74,205.68C144.89,205.68 139.36,212.6 139.36,221.53C139.36,230.46 144.89,237.27 153.74,237.27Z"/>
android:pathData="M73.92,44.43C74.82,44.43 75.43,45.1 75.43,46.02V54.54C75.43,55.46 74.82,56.13 73.92,56.13C73,56.13 72.41,55.46 72.41,54.54V46.02C72.41,45.1 73,44.43 73.92,44.43ZM73.92,61.55C72.82,61.55 71.95,60.68 71.95,59.58C71.95,58.51 72.82,57.64 73.92,57.64C75.02,57.64 75.89,58.51 75.89,59.58C75.89,60.68 75.02,61.55 73.92,61.55Z" <path android:fillColor="#000000" android:pathData="M349,217.5C349,290.13 290.13,349 217.5,349C144.88,349 86,290.13 86,217.5C86,144.88 144.88,86 217.5,86C290.13,86 349,144.88 349,217.5ZM99.15,217.5C99.15,282.86 152.14,335.85 217.5,335.85C282.86,335.85 335.85,282.86 335.85,217.5C335.85,152.14 282.86,99.15 217.5,99.15C152.14,99.15 99.15,152.14 99.15,217.5Z"/>
android:fillColor="#000000"/>
<path
android:pathData="M68.41,48.55C69.33,48.55 69.92,49.22 69.92,50.11V55.77C69.92,59.94 67.35,61.55 64.22,61.55C61.08,61.55 58.5,59.94 58.5,55.77V50.11C58.5,49.22 59.09,48.55 60.01,48.55C60.91,48.55 61.52,49.22 61.52,50.11V55.56C61.52,57.84 62.48,58.74 64.22,58.74C65.94,58.74 66.9,57.84 66.9,55.56V50.11C66.9,49.22 67.51,48.55 68.41,48.55Z"
android:fillColor="#000000"/>
<path
android:pathData="M49.94,52.01C49.94,52.85 50.81,53.16 52.47,53.54C54.78,54.1 56.98,54.69 56.98,57.53C56.98,60.3 54.93,61.55 51.99,61.55C49.56,61.55 47.79,60.71 46.97,59.73C46.33,58.97 46.41,58.3 47.02,57.71C47.79,56.97 48.46,57.28 48.89,57.66C49.58,58.3 50.43,58.97 52.07,58.97C53.29,58.97 54.06,58.56 54.06,57.74C54.06,56.92 53.24,56.64 51.09,56.05C48.97,55.46 47.08,54.9 47.08,52.36C47.08,49.52 49.38,48.35 51.94,48.35C53.4,48.35 55.06,48.73 56.08,49.83C56.52,50.27 56.85,50.88 56.08,51.72C55.32,52.52 54.75,52.29 54.22,51.88C53.73,51.52 52.88,50.91 51.6,50.91C50.73,50.91 49.94,51.19 49.94,52.01Z"
android:fillColor="#000000"/>
<path
android:pathData="M38.79,61.55C34.9,61.55 32.11,58.74 32.11,54.95C32.11,51.14 34.9,48.35 38.79,48.35C42.68,48.35 45.47,51.14 45.47,54.95C45.47,58.74 42.68,61.55 38.79,61.55ZM38.79,58.74C41.04,58.74 42.45,57.1 42.45,54.95C42.45,52.8 41.04,51.14 38.79,51.14C36.54,51.14 35.13,52.8 35.13,54.95C35.13,57.1 36.54,58.74 38.79,58.74Z"
android:fillColor="#000000"/>
<path
android:pathData="M86,54C86,71.67 71.67,86 54,86C36.33,86 22,71.67 22,54C22,36.33 36.33,22 54,22C71.67,22 86,36.33 86,54ZM25.2,54C25.2,69.91 38.09,82.8 54,82.8C69.91,82.8 82.8,69.91 82.8,54C82.8,38.09 69.91,25.2 54,25.2C38.09,25.2 25.2,38.09 25.2,54Z"
android:fillColor="#000000"/>
<path
android:pathData="M36.78,54.99C36.78,56.09 37.65,56.96 38.75,56.96C39.85,56.96 40.72,56.09 40.72,54.99C40.72,53.91 39.85,53.04 38.75,53.04C37.65,53.04 36.78,53.91 36.78,54.99Z"
android:fillColor="#000000"/>
</vector> </vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -35,8 +35,6 @@ namespace osu.Desktop
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private IBindable<APIUser> user = null!;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
@ -49,9 +47,11 @@ namespace osu.Desktop
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!; private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>(); private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>(); private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
private readonly RichPresence presence = new RichPresence private readonly RichPresence presence = new RichPresence
@ -64,8 +64,10 @@ namespace osu.Desktop
}, },
}; };
private IBindable<APIUser>? user;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load()
{ {
client = new DiscordRpcClient(client_id) client = new DiscordRpcClient(client_id)
{ {
@ -75,12 +77,29 @@ namespace osu.Desktop
}; };
client.OnReady += onReady; client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
// 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. try
{
client.RegisterUriScheme(); client.RegisterUriScheme();
client.Subscribe(EventType.Join); client.Subscribe(EventType.Join);
client.OnJoin += onJoin; client.OnJoin += onJoin;
}
catch (Exception ex)
{
// This is known to fail in at least the following sandboxed environments:
// - macOS (when packaged as an app bundle)
// - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170)
// There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now.
Logger.Log($"Failed to register Discord URI scheme: {ex}");
}
client.Initialize();
}
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
@ -94,13 +113,11 @@ namespace osu.Desktop
activity.BindTo(u.NewValue.Activity); activity.BindTo(u.NewValue.Activity);
}, true); }, true);
ruleset.BindValueChanged(_ => updatePresence()); ruleset.BindValueChanged(_ => schedulePresenceUpdate());
status.BindValueChanged(_ => updatePresence()); status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => updatePresence()); activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => updatePresence()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated; multiplayerClient.RoomUpdated += onRoomUpdated;
client.Initialize();
} }
private void onReady(object _, ReadyMessage __) private void onReady(object _, ReadyMessage __)
@ -111,14 +128,14 @@ namespace osu.Desktop
if (client.CurrentPresence != null) if (client.CurrentPresence != null)
client.SetPresence(null); client.SetPresence(null);
updatePresence(); schedulePresenceUpdate();
} }
private void onRoomUpdated() => updatePresence(); private void onRoomUpdated() => schedulePresenceUpdate();
private ScheduledDelegate? presenceUpdateDelegate; private ScheduledDelegate? presenceUpdateDelegate;
private void updatePresence() private void schedulePresenceUpdate()
{ {
presenceUpdateDelegate?.Cancel(); presenceUpdateDelegate?.Cancel();
presenceUpdateDelegate = Scheduler.AddDelayed(() => presenceUpdateDelegate = Scheduler.AddDelayed(() =>
@ -134,20 +151,21 @@ namespace osu.Desktop
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
updatePresenceStatus(hideIdentifiableInformation); updatePresence(hideIdentifiableInformation);
updatePresenceParty(hideIdentifiableInformation);
updatePresenceAssets();
client.SetPresence(presence); client.SetPresence(presence);
}, 200); }, 200);
} }
private void updatePresenceStatus(bool hideIdentifiableInformation) private void updatePresence(bool hideIdentifiableInformation)
{ {
if (user == null)
return;
// user activity
if (activity.Value != null) if (activity.Value != null)
{ {
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
{ {
@ -170,10 +188,8 @@ namespace osu.Desktop
presence.State = "Idle"; presence.State = "Idle";
presence.Details = string.Empty; presence.Details = string.Empty;
} }
}
private void updatePresenceParty(bool hideIdentifiableInformation) // user party
{
if (!hideIdentifiableInformation && multiplayerClient.Room != null) if (!hideIdentifiableInformation && multiplayerClient.Room != null)
{ {
MultiplayerRoom room = multiplayerClient.Room; MultiplayerRoom room = multiplayerClient.Room;
@ -194,18 +210,21 @@ namespace osu.Desktop
Password = room.Settings.Password, Password = room.Settings.Password,
}; };
if (client.HasRegisteredUriScheme)
presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); 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 else
{ {
presence.Party = null; presence.Party = null;
presence.Secrets.JoinSecret = null; presence.Secrets.JoinSecret = null;
} }
}
private void updatePresenceAssets() // game images:
{ // large image tooltip
// update user information
if (privacyMode.Value == DiscordRichPresenceMode.Limited) if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
@ -216,7 +235,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); 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.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name; presence.Assets.SmallImageText = ruleset.Value.Name;
} }
@ -252,8 +271,19 @@ namespace osu.Desktop
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
private static string truncate(string str) private static string clampLength(string str)
{ {
// Empty strings are fine to discord even though single-character strings are not. Make it make sense.
if (string.IsNullOrEmpty(str))
return str;
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
if (str.Length < 2)
return str.PadRight(2, '\u200B');
if (Encoding.UTF8.GetByteCount(str) <= 128) if (Encoding.UTF8.GetByteCount(str) <= 128)
return str; return str;

View File

@ -8,11 +8,13 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Desktop namespace osu.Desktop
{ {
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
[SupportedOSPlatform("windows")]
internal static class NVAPI internal static class NVAPI
{ {
private const string osu_filename = "osu!.exe"; private const string osu_filename = "osu!.exe";
@ -487,6 +489,7 @@ namespace osu.Desktop
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16); public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvStatus internal enum NvStatus
{ {
OK = 0, // Success. Request is completed. OK = 0, // Success. Request is completed.
@ -609,6 +612,7 @@ namespace osu.Desktop
FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported. FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvSystemType internal enum NvSystemType
{ {
UNKNOWN = 0, UNKNOWN = 0,
@ -616,6 +620,7 @@ namespace osu.Desktop
DESKTOP = 2 DESKTOP = 2
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvGpuType internal enum NvGpuType
{ {
UNKNOWN = 0, UNKNOWN = 0,
@ -623,6 +628,7 @@ namespace osu.Desktop
DGPU = 2, // Discrete DGPU = 2, // Discrete
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvSettingID : uint internal enum NvSettingID : uint
{ {
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C, OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
@ -715,6 +721,7 @@ namespace osu.Desktop
INVALID_SETTING_ID = 0xFFFFFFFF INVALID_SETTING_ID = 0xFFFFFFFF
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvShimSetting : uint internal enum NvShimSetting : uint
{ {
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000, SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
@ -729,6 +736,7 @@ namespace osu.Desktop
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvThreadControlSetting : uint internal enum NvThreadControlSetting : uint
{ {
OGL_THREAD_CONTROL_ENABLE = 0x00000001, OGL_THREAD_CONTROL_ENABLE = 0x00000001,

View File

@ -22,7 +22,6 @@ using osu.Game.IPC;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Performance; using osu.Game.Performance;
using osu.Game.Utils; using osu.Game.Utils;
using SDL2;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -161,7 +160,7 @@ namespace osu.Desktop
host.Window.Title = Name; host.Window.Title = Name;
} }
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
@ -169,23 +168,5 @@ namespace osu.Desktop
osuSchemeLinkIPCChannel?.Dispose(); osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose(); archiveImportIPCChannel?.Dispose();
} }
private class SDL2BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
SDL.SDL_GetPowerInfo(out _, out int percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL.SDL_GetPowerInfo(out _, out _) == SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
} }
} }

View File

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

View File

@ -13,7 +13,7 @@ using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Tournament; using osu.Game.Tournament;
using SDL2; using SDL;
using Squirrel; using Squirrel;
namespace osu.Desktop namespace osu.Desktop
@ -51,18 +51,21 @@ namespace osu.Desktop
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher. // While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/ // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
unsafe
{ {
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
// disabling it ourselves. // disabling it ourselves.
// We could also better detect compatibility mode if required: // We could also better detect compatibility mode if required:
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!", "Your operating system is too old to run osu!"u8,
"This version of osu! requires at least Windows 8.1 to run.\n" "This version of osu! requires at least Windows 8.1 to run.\n"u8
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n" + "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero); + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
return; return;
} }
}
setupSquirrel(); setupSquirrel();
} }
@ -70,6 +73,7 @@ namespace osu.Desktop
// NVIDIA profiles are based on the executable name of a process. // NVIDIA profiles are based on the executable name of a process.
// Lazer and stable share the same executable name. // 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. // 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.
if (OperatingSystem.IsWindows())
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
// Back up the cwd before DesktopGameHost changes it // Back up the cwd before DesktopGameHost changes it
@ -103,7 +107,13 @@ namespace osu.Desktop
} }
} }
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null })) var hostOptions = new HostOptions
{
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
FriendlyGameName = OsuGameBase.GAME_NAME,
};
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions))
{ {
if (!host.IsPrimaryInstance) if (!host.IsPrimaryInstance)
{ {

View File

@ -0,0 +1,25 @@
// 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.Utils;
namespace osu.Desktop
{
internal class SDL2BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}

View File

@ -0,0 +1,27 @@
// 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.Utils;
using SDL;
namespace osu.Desktop
{
internal unsafe class SDL3BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
@ -163,6 +164,7 @@ namespace osu.Desktop.Windows
[DllImport("Shell32.dll")] [DllImport("Shell32.dll")]
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2); private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum EventId private enum EventId
{ {
/// <summary> /// <summary>
@ -172,6 +174,7 @@ namespace osu.Desktop.Windows
SHCNE_ASSOCCHANGED = 0x08000000 SHCNE_ASSOCCHANGED = 0x08000000
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum Flags : uint private enum Flags : uint
{ {
SHCNF_IDLIST = 0x0000 SHCNF_IDLIST = 0x0000

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -76,21 +76,38 @@ namespace osu.Game.Rulesets.Catch.Edit
public override bool HandleReverse() public override bool HandleReverse()
{ {
var hitObjects = EditorBeatmap.SelectedHitObjects
.OfType<CatchHitObject>()
.OrderBy(obj => obj.StartTime)
.ToList();
double selectionStartTime = SelectedItems.Min(h => h.StartTime); double selectionStartTime = SelectedItems.Min(h => h.StartTime);
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime()); double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
EditorBeatmap.PerformOnSelection(hitObject => // the expectation is that even if the objects themselves are reversed temporally,
{ // the position of new combos in the selection should remain the same.
hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime); // 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.Path.Reverse(out Vector2 positionalOffset);
juiceStream.OriginalX += positionalOffset.X; juiceStream.OriginalX += positionalOffset.X;
juiceStream.LegacyConvertedY += positionalOffset.Y; juiceStream.LegacyConvertedY += positionalOffset.Y;
EditorBeatmap.Update(juiceStream); 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; return true;
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchAreaNotInitiallyVisible()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestPressReceptors()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getReceptor(index).Action.Value));
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
}
}
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Mania.Edit.Checks
{
public class CheckKeyCount : ICheck
{
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Check mania keycount.");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateKeycountTooLow(this),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
if (diff.CircleSize < 4)
{
yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize);
}
}
public class IssueTemplateKeycountTooLow : IssueTemplate
{
public IssueTemplateKeycountTooLow(ICheck check)
: base(check, IssueType.Problem, "Key count is {0} and must be 4 or higher.")
{
}
public Issue Create(float current) => new Issue(this, current);
}
}
}

View File

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

View File

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

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
} }
public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); public new ManiaPlayfield Playfield => drawableRuleset.Playfield;
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;

View File

@ -1,9 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -14,9 +19,9 @@ namespace osu.Game.Rulesets.Mania
{ {
private FilterCriteria.OptionalRange<float> keys; private FilterCriteria.OptionalRange<float> keys;
public bool Matches(BeatmapInfo beatmapInfo) public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{ {
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo))); return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
} }
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
@ -30,5 +35,20 @@ namespace osu.Game.Rulesets.Mania
return false; return false;
} }
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
if (keys.HasFilter)
{
// Interpreting as the Mod type is required for equality comparison.
HashSet<Mod> oldSet = mods.OldValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
HashSet<Mod> newSet = mods.NewValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
if (!oldSet.SetEquals(newSet))
return true;
}
return false;
}
} }
} }

View File

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

View File

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

View File

@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
// (see `Stage.columnBackgrounds`). // (see `Stage.columnBackgrounds`).
BackgroundContainer, BackgroundContainer,
TopLevelContainer, TopLevelContainer
new ColumnTouchInputArea(this)
}; };
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
@ -181,38 +180,5 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
public partial class ColumnTouchInputArea : Drawable
{
private readonly Column column;
[Resolved(canBeNull: true)]
private ManiaInputManager maniaInputManager { get; set; }
private KeyBindingContainer<ManiaAction> keyBindingContainer;
public ColumnTouchInputArea(Column column)
{
RelativeSizeAxes = Axes.Both;
this.column = column;
}
protected override void LoadComplete()
{
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
keyBindingContainer?.TriggerReleased(column.Action.Value);
}
}
} }
} }

View File

@ -3,15 +3,12 @@
#nullable disable #nullable disable
using System;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -62,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
onSkinChanged(); onSkinChanged();
} }
protected override void LoadComplete()
{
base.LoadComplete();
updateMobileSizing();
}
private void onSkinChanged() private void onSkinChanged()
{ {
for (int i = 0; i < stageDefinition.Columns; i++) for (int i = 0; i < stageDefinition.Columns; i++)
@ -92,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI
columns[i].Width = width.Value; columns[i].Width = width.Value;
} }
updateMobileSizing();
} }
/// <summary> /// <summary>
@ -106,31 +95,6 @@ namespace osu.Game.Rulesets.Mania.UI
Content[column] = columns[column].Child = content; Content[column] = columns[column].Child = content;
} }
private void updateMobileSizing()
{
if (!IsLoaded || !RuntimeInfo.IsMobile)
return;
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
// Will be null in tests.
if (containingCell == null)
return;
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
// 1.92 is a "reference" mobile screen aspect ratio for phones.
// We should scale it back for cases like tablets which aren't so extreme.
mobileAdjust *= aspectRatio / 1.92f;
// Best effort until we have better mobile support.
for (int i = 0; i < stageDefinition.Columns; i++)
columns[i].Width *= mobileAdjust;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -31,6 +31,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
[Cached]
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject> public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{ {
/// <summary> /// <summary>
@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// </summary> /// </summary>
public const double MAX_TIME_RANGE = 11485; public const double MAX_TIME_RANGE = 11485;
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield; public new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
@ -103,6 +104,8 @@ namespace osu.Game.Rulesets.Mania.UI
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint)); configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value); TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea());
} }
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;

View File

@ -0,0 +1,216 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// An overlay that captures and displays osu!mania mouse and touch input.
/// </summary>
public partial class ManiaTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
[SettingSource("Spacing", "The spacing between receptors.")]
public BindableFloat Spacing { get; } = new BindableFloat(10)
{
Precision = 1,
MinValue = 0,
MaxValue = 100,
};
[SettingSource("Opacity", "The receptor opacity.")]
public BindableFloat Opacity { get; } = new BindableFloat(1)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 1
};
[Resolved]
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
private GridContainer gridContainer = null!;
public ManiaTouchInputArea()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Height = 0.5f;
}
[BackgroundDependencyLoader]
private void load()
{
List<Drawable> receptorGridContent = new List<Drawable>();
List<Dimension> receptorGridDimensions = new List<Dimension>();
bool first = true;
foreach (var stage in drawableRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
if (!first)
{
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
}
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
receptorGridDimensions.Add(new Dimension());
first = false;
}
}
InternalChild = gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Content = new[] { receptorGridContent.ToArray() },
ColumnDimensions = receptorGridDimensions.ToArray()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Show();
return true;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
return true;
}
protected override void PopIn()
{
gridContainer.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
gridContainer.FadeOut(300);
}
public partial class ColumnInputReceptor : CompositeDrawable
{
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
private readonly Box highlightOverlay;
[Resolved]
private ManiaInputManager? inputManager { get; set; }
private bool isPressed;
public ColumnInputReceptor()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.15f,
},
highlightOverlay = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
}
}
};
}
protected override bool OnTouchDown(TouchDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateButton(false);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)
return;
isPressed = press;
if (press)
{
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
}
else
{
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
}
}
}
private partial class Gutter : Drawable
{
public readonly IBindable<float> Spacing = new Bindable<float>();
public Gutter()
{
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
}
}
}
}

View File

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

View File

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

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -52,6 +53,65 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
}
[Test]
public void TestDistanceSnapAdjustDoesNotHideTheGridIfStartingEnabled()
{
double distanceSnap = double.PositiveInfinity;
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value);
AddStep("increase distance", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.PressKey(Key.ControlLeft);
InputManager.ScrollVerticalBy(1);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.AltLeft);
});
AddUntilStep("distance snap increased", () => this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
}
[Test]
public void TestDistanceSnapAdjustShowsGridMomentarilyIfStartingDisabled()
{
double distanceSnap = double.PositiveInfinity;
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value);
AddStep("start increasing distance", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.PressKey(Key.ControlLeft);
});
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("finish increasing distance", () =>
{
InputManager.ScrollVerticalBy(1);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.AltLeft);
});
AddUntilStep("distance snap increased", () => this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
AddUntilStep("distance snap hidden in the end", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
} }
[Test] [Test]

View File

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

View File

@ -30,23 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}); });
[Test]
public void TestAddOverlappingControlPoints()
{
createVisualiser(true);
addControlPointStep(new Vector2(200));
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
AddAssert("last connection displayed", () =>
{
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
return lastConnection.DrawWidth > 50;
});
}
[Test] [Test]
public void TestPerfectCurveTooManyPoints() public void TestPerfectCurveTooManyPoints()
{ {
@ -172,6 +155,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPathType(4, null); assertControlPointPathType(4, null);
} }
[Test]
public void TestStackingUpdatesPointsPosition()
{
createVisualiser(true);
Vector2[] points =
[
new Vector2(200),
new Vector2(300),
new Vector2(500, 300),
new Vector2(700, 200),
new Vector2(500, 100)
];
foreach (var point in points) addControlPointStep(point);
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);
for (int i = 0; i < points.Length; i++)
addAssertPointPositionChanged(points, i);
}
private void addAssertPointPositionChanged(Vector2[] points, int index)
{
AddAssert($"Point at {points.ElementAt(index)} changed",
() => visualiser.Pieces[index].Position,
() => !Is.EqualTo(points.ElementAt(index))
);
}
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection) private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -578,6 +579,24 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1); assertKeyCounter(1, 1);
} }
[Test]
public void TestTouchJudgedCircle()
{
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
beginTouch(TouchSource.Touch1);
endTouch(TouchSource.Touch1);
// Hold the second touch (this becomes the primary touch).
beginTouch(TouchSource.Touch2);
// Touch again on the first circle.
// Because it's been judged, the cursor should not move here.
beginTouch(TouchSource.Touch1);
checkPosition(TouchSource.Touch2);
}
private void addHitCircleAt(TouchSource source) private void addHitCircleAt(TouchSource source)
{ {
AddStep($"Add circle at {source}", () => AddStep($"Add circle at {source}", () =>
@ -590,6 +609,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Clock = new FramedClock(new ManualClock()), Clock = new FramedClock(new ManualClock()),
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)), Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
CheckHittable = (_, _, _) => ClickAction.Hit
}); });
}); });
} }

View File

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

View File

@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max) while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
rhythmStart++; rhythmStart++;
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
for (int i = rhythmStart; i > 0; i--) for (int i = rhythmStart; i > 0; i--)
{ {
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1);
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
@ -66,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
} }
else else
{ {
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125; effectiveRatio *= 0.125;
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25; effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
startRatio = effectiveRatio; startRatio = effectiveRatio;
islandSize = 1; islandSize = 1;
} }
lastObj = prevObj;
prevObj = currObj;
} }
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)

View File

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

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
/// <summary>
/// A visualisation of the lines between <see cref="PathControlPointPiece{T}"/>s.
/// </summary>
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnection{T}"/> visualises.</typeparam>
public partial class PathControlPointConnection<T> : SmoothPath where T : OsuHitObject, IHasPath
{
private readonly T hitObject;
private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion;
private IBindable<int> stackHeight;
public PathControlPointConnection(T hitObject)
{
this.hitObject = hitObject;
PathRadius = 1;
}
protected override void LoadComplete()
{
base.LoadComplete();
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
stackHeight.BindValueChanged(_ => updateConnectingPath());
updateConnectingPath();
}
/// <summary>
/// Updates the path connecting this control point to the next one.
/// </summary>
private void updateConnectingPath()
{
Position = hitObject.StackedPosition;
ClearVertices();
foreach (var controlPoint in hitObject.Path.ControlPoints)
AddVertex(controlPoint.Position);
OriginPosition = PositionInBoundingBox(Vector2.Zero);
}
}
}

View File

@ -1,81 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
/// <summary>
/// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s.
/// </summary>
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam>
public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath
{
public readonly PathControlPoint ControlPoint;
private readonly Path path;
private readonly T hitObject;
public int ControlPointIndex { get; set; }
private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion;
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
{
this.hitObject = hitObject;
ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
InternalChild = path = new SmoothPath
{
Anchor = Anchor.Centre,
PathRadius = 1
};
}
protected override void LoadComplete()
{
base.LoadComplete();
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
updateConnectingPath();
}
/// <summary>
/// Updates the path connecting this control point to the next one.
/// </summary>
private void updateConnectingPath()
{
Position = hitObject.StackedPosition + ControlPoint.Position;
path.ClearVertices();
int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count)
return;
path.AddVertex(Vector2.Zero);
path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
}
}

View File

@ -8,9 +8,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
private readonly T hitObject; private readonly T hitObject;
private readonly Container marker; private readonly Circle circle;
private readonly Drawable markerRing; private readonly Drawable markerRing;
[Resolved] [Resolved]
@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition; private IBindable<Vector2> hitObjectPosition;
private IBindable<float> hitObjectScale; private IBindable<float> hitObjectScale;
private IBindable<int> stackHeight;
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint) public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
{ {
@ -59,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChildren = new[]
{ {
marker = new Container circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(20), Size = new Vector2(20),
}, },
markerRing = new CircularContainer markerRing = new CircularProgress
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(28), Size = new Vector2(28),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0, Alpha = 0,
Child = new Box InnerRadius = 0.1f,
{ Progress = 1
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
} }
}; };
} }
@ -105,13 +90,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy(); hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
hitObjectScale.BindValueChanged(_ => updateMarkerDisplay()); hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
stackHeight.BindValueChanged(_ => updateMarkerDisplay());
IsSelected.BindValueChanged(_ => updateMarkerDisplay()); IsSelected.BindValueChanged(_ => updateMarkerDisplay());
updateMarkerDisplay(); updateMarkerDisplay();
} }
// The connecting path is excluded from positional input // The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
@ -205,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (IsHovered || IsSelected.Value) if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1); colour = colour.Lighten(1);
marker.Colour = colour; Colour = colour;
marker.Scale = new Vector2(hitObject.Scale); Scale = new Vector2(hitObject.Scale);
} }
private Color4 getColourFromNodeType() private Color4 getColourFromNodeType()

View File

@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
internal readonly Container<PathControlPointPiece<T>> Pieces; internal readonly Container<PathControlPointPiece<T>> Pieces;
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>(); private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly T hitObject; private readonly T hitObject;
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both }, new PathControlPointConnection<T>(hitObject),
Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both } Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
}; };
} }
@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(hitObject.Path.ControlPoints); controlPoints.BindTo(hitObject.Path.ControlPoints);
} }
// Generally all the control points are within the visible area all the time.
public override bool UpdateSubTreeMasking() => true;
/// <summary> /// <summary>
/// Handles correction of invalid path types. /// Handles correction of invalid path types.
/// </summary> /// </summary>
@ -185,17 +187,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null); Debug.Assert(e.NewItems != null);
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.NewStartingIndex)
connection.ControlPointIndex += e.NewItems.Count;
}
}
for (int i = 0; i < e.NewItems.Count; i++) for (int i = 0; i < e.NewItems.Count; i++)
{ {
var point = (PathControlPoint)e.NewItems[i]; var point = (PathControlPoint)e.NewItems[i];
@ -209,8 +200,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
d.DragInProgress = DragInProgress; d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded; d.DragEnded = DragEnded;
})); }));
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
} }
break; break;
@ -222,19 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately(); piece.RemoveAndDisposeImmediately();
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
connection.RemoveAndDisposeImmediately();
}
// If removing before the end of the path,
// update indices of connections after remove location
if (e.OldStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.OldStartingIndex)
connection.ControlPointIndex -= e.OldItems.Count;
}
} }
break; break;
@ -459,10 +435,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
{ {
changeHandler?.BeginChange();
foreach (var p in Pieces.Where(p => p.IsSelected.Value)) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type); updatePathType(p, type);
EnsureValidPathTypes(); EnsureValidPathTypes();
changeHandler?.EndChange();
}); });
if (countOfState == totalCount) if (countOfState == totalCount)

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public SliderBodyPiece() public SliderBodyPiece()
{ {
InternalChild = body = new ManualSliderBody AutoSizeAxes = Axes.Both;
{
AccentColour = Color4.Transparent
};
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur. // SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling. // Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
AlwaysPresent = true; AlwaysPresent = true;
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.SetVertices(vertices); body.SetVertices(vertices);
} }
Size = body.Size;
OriginPosition = body.PathOffset; OriginPosition = body.PathOffset;
} }

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuSelectionHandler : EditorSelectionHandler public partial class OsuSelectionHandler : EditorSelectionHandler
{ {
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?>? referencePathTypes;
protected override void OnSelectionChanged() protected override void OnSelectionChanged()
{ {
base.OnSelectionChanged(); base.OnSelectionChanged();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanFlipY = quad.Height > 0;
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
} }
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referencePathTypes = null;
}
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
@ -78,13 +61,21 @@ namespace osu.Game.Rulesets.Osu.Edit
public override bool HandleReverse() 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 endTime = hitObjects.Max(h => h.GetEndTime());
double startTime = hitObjects.Min(h => h.StartTime); double startTime = hitObjects.Min(h => h.StartTime);
bool moreThanOneObject = hitObjects.Count > 1; 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) foreach (var h in hitObjects)
{ {
if (moreThanOneObject) if (moreThanOneObject)
@ -97,6 +88,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; return true;
} }
@ -135,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return didFlip; return didFlip;
} }
public override bool HandleScale(Vector2 scale, Anchor reference)
{
adjustScaleFromAnchor(ref scale, reference);
var hitObjects = selectedMovableObjects;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
scaleSlider(slider, scale);
else
scaleHitObjects(hitObjects, reference, scale);
moveSelectionInBounds();
return true;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
private void scaleSlider(Slider slider, Vector2 scale) public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
Vector2 pathRelativeDeltaScale = new Vector2(
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
foreach (var point in slider.Path.ControlPoints)
{
oldControlPoints.Enqueue(point.Position);
point.Position *= pathRelativeDeltaScale;
}
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
foreach (var point in slider.Path.ControlPoints)
point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
}
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
{
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
return (xInBounds, yInBounds);
}
private void moveSelectionInBounds() private void moveSelectionInBounds()
{ {
@ -248,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += delta; h.Position += delta;
} }
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary>
/// <param name="hitObjects">The hitobjects to be scaled</param>
/// <param name="reference">The anchor from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
//max Size -> playfield bounds
if (scaledQuad.TopLeft.X < 0)
scale.X += scaledQuad.TopLeft.X;
if (scaledQuad.TopLeft.Y < 0)
scale.Y += scaledQuad.TopLeft.Y;
if (scaledQuad.BottomRight.X > DrawWidth)
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
if (scaledQuad.BottomRight.Y > DrawHeight)
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
Vector2 scaledSize = selectionQuad.Size + scale;
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
return scale;
}
/// <summary> /// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled. /// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary> /// </summary>

View File

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

View File

@ -0,0 +1,220 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState();
updateState();
}
private void updateState()
{
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanScaleX.Value = quad.Width > 0;
CanScaleY.Value = quad.Height > 0;
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
}
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
private Vector2? defaultOrigin;
public override void Begin()
{
if (objectsInScale != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
{
var originalInfo = objectsInScale[slider];
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
}
else
{
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
}
}
moveSelectionInBounds();
}
public override void Commit()
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
objectsInScale = null;
OriginalSurroundingQuad = null;
defaultOrigin = null;
}
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
{
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
}
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
{
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
return (xInBounds, yInBounds);
}
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary>
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale)
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0))
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0))
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
}
private void moveSelectionInBounds()
{
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
Vector2 delta = Vector2.Zero;
if (quad.TopLeft.X < 0)
delta.X -= quad.TopLeft.X;
if (quad.TopLeft.Y < 0)
delta.Y -= quad.TopLeft.Y;
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
foreach (var (h, _) in objectsInScale!)
h.Position += delta;
}
private struct OriginalHitObjectState
{
public Vector2 Position { get; }
public Vector2[]? PathControlPointPositions { get; }
public PathType?[]? PathControlPointTypes { get; }
public OriginalHitObjectState(OsuHitObject hitObject)
{
Position = hitObject.Position;
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
}
}
}
}

View File

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

View File

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

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Put your faith in the approach circles..."; public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };

View File

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

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu
// //
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected), // Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can. // this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition)); NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.CanBeHit() && c.ReceivePositionalInputAt(screenSpacePosition));
public OsuInputManager(RulesetInfo ruleset) public OsuInputManager(RulesetInfo ruleset)
: base(ruleset, 0, SimultaneousBindingMode.Unique) : base(ruleset, 0, SimultaneousBindingMode.Unique)

View File

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

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// </summary> /// </summary>
public partial class ManualSliderBody : SliderBody public partial class ManualSliderBody : SliderBody
{ {
public new void SetVertices(IReadOnlyList<Vector2> vertices) public ManualSliderBody()
{ {
base.SetVertices(vertices); AutoSizeAxes = Axes.Both;
Size = Path.Size; }
}
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
} }
} }

View File

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

View File

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
// For osu! gameplay, everything is always on screen. // 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) // 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 override bool UpdateSubTreeMasking() => false;
public SmokeContainer Smoke { get; } public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; } public FollowPointRenderer FollowPoints { get; }

View File

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

View File

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

View File

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

View File

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

View File

@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true); ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
} }
protected override double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
return base.ComputeTimeRange() / multiplier;
}
} }
} }

View File

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

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
{
public override string Name => "Constant Speed";
public override string Acronym => "CS";
public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override ModType Type => ModType.Conversion;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
}
}
}

View File

@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModClassic(), new TaikoModClassic(),
new TaikoModSwap(), new TaikoModSwap(),
new TaikoModSingleTap(), new TaikoModSingleTap(),
new TaikoModConstantSpeed(),
}; };
case ModType.Automation: case ModType.Automation:
@ -188,6 +189,8 @@ namespace osu.Game.Rulesets.Taiko
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); 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 DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator(); public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator();

View File

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

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