Merge branch 'master' into slider-end-animation-2
@ -21,7 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2023.1117.0",
|
"version": "2024.517.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
]
|
||||||
|
24
.github/workflows/diffcalc.yml
vendored
@ -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
@ -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
|
||||||
|
@ -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>
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
BIN
assets/lazer.png
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 438 KiB |
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.221.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.
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 49 KiB |
@ -5,14 +5,21 @@ using System;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using DiscordRPC;
|
using DiscordRPC;
|
||||||
using DiscordRPC.Message;
|
using DiscordRPC.Message;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using LogLevel = osu.Framework.Logging.LogLevel;
|
using LogLevel = osu.Framework.Logging.LogLevel;
|
||||||
@ -21,39 +28,78 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
internal partial class DiscordRichPresence : Component
|
internal partial class DiscordRichPresence : Component
|
||||||
{
|
{
|
||||||
private const string client_id = "367827983903490050";
|
private const string client_id = "1216669957799018608";
|
||||||
|
|
||||||
private DiscordRpcClient client = null!;
|
private DiscordRpcClient client = null!;
|
||||||
|
|
||||||
[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!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGame game { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private LoginOverlay? login { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
|
private readonly IBindable<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
|
||||||
{
|
{
|
||||||
Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
|
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
|
||||||
|
Secrets = new Secrets
|
||||||
|
{
|
||||||
|
JoinSecret = null,
|
||||||
|
SpectateSecret = null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private IBindable<APIUser>? user;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load()
|
||||||
{
|
{
|
||||||
client = new DiscordRpcClient(client_id)
|
client = new DiscordRpcClient(client_id)
|
||||||
{
|
{
|
||||||
SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady.
|
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
||||||
|
// to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady).
|
||||||
|
SkipIdenticalPresence = true
|
||||||
};
|
};
|
||||||
|
|
||||||
client.OnReady += onReady;
|
client.OnReady += onReady;
|
||||||
|
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
|
||||||
|
|
||||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
try
|
||||||
|
{
|
||||||
|
client.RegisterUriScheme();
|
||||||
|
client.Subscribe(EventType.Join);
|
||||||
|
client.OnJoin += onJoin;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// This is known to fail in at least the following sandboxed environments:
|
||||||
|
// - macOS (when packaged as an app bundle)
|
||||||
|
// - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170)
|
||||||
|
// There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now.
|
||||||
|
Logger.Log($"Failed to register Discord URI scheme: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||||
|
|
||||||
@ -67,21 +113,32 @@ namespace osu.Desktop
|
|||||||
activity.BindTo(u.NewValue.Activity);
|
activity.BindTo(u.NewValue.Activity);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
ruleset.BindValueChanged(_ => updateStatus());
|
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
status.BindValueChanged(_ => updateStatus());
|
status.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
activity.BindValueChanged(_ => updateStatus());
|
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
privacyMode.BindValueChanged(_ => updateStatus());
|
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
|
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||||
client.Initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onReady(object _, ReadyMessage __)
|
private void onReady(object _, ReadyMessage __)
|
||||||
{
|
{
|
||||||
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
|
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
|
||||||
updateStatus();
|
|
||||||
|
// when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence).
|
||||||
|
if (client.CurrentPresence != null)
|
||||||
|
client.SetPresence(null);
|
||||||
|
|
||||||
|
schedulePresenceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatus()
|
private void onRoomUpdated() => schedulePresenceUpdate();
|
||||||
|
|
||||||
|
private ScheduledDelegate? presenceUpdateDelegate;
|
||||||
|
|
||||||
|
private void schedulePresenceUpdate()
|
||||||
|
{
|
||||||
|
presenceUpdateDelegate?.Cancel();
|
||||||
|
presenceUpdateDelegate = Scheduler.AddDelayed(() =>
|
||||||
{
|
{
|
||||||
if (!client.IsInitialized)
|
if (!client.IsInitialized)
|
||||||
return;
|
return;
|
||||||
@ -92,11 +149,23 @@ namespace osu.Desktop
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.Value == UserStatus.Online && activity.Value != null)
|
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
||||||
|
|
||||||
|
updatePresence(hideIdentifiableInformation);
|
||||||
|
client.SetPresence(presence);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePresence(bool hideIdentifiableInformation)
|
||||||
{
|
{
|
||||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited;
|
if (user == null)
|
||||||
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
|
return;
|
||||||
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
|
||||||
|
// user activity
|
||||||
|
if (activity.Value != null)
|
||||||
|
{
|
||||||
|
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
@ -120,7 +189,42 @@ namespace osu.Desktop
|
|||||||
presence.Details = string.Empty;
|
presence.Details = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update user information
|
// user party
|
||||||
|
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
|
||||||
|
{
|
||||||
|
MultiplayerRoom room = multiplayerClient.Room;
|
||||||
|
|
||||||
|
presence.Party = new Party
|
||||||
|
{
|
||||||
|
Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private,
|
||||||
|
ID = room.RoomID.ToString(),
|
||||||
|
// technically lobbies can have infinite users, but Discord needs this to be set to something.
|
||||||
|
// to make party display sensible, assign a powers of two above participants count (8 at minimum).
|
||||||
|
Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))),
|
||||||
|
Size = room.Users.Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomSecret roomSecret = new RoomSecret
|
||||||
|
{
|
||||||
|
RoomID = room.RoomID,
|
||||||
|
Password = room.Settings.Password,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (client.HasRegisteredUriScheme)
|
||||||
|
presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret);
|
||||||
|
|
||||||
|
// discord cannot handle both secrets and buttons at the same time, so we need to choose something.
|
||||||
|
// the multiplayer room seems more important.
|
||||||
|
presence.Buttons = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
presence.Party = null;
|
||||||
|
presence.Secrets.JoinSecret = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// game images:
|
||||||
|
// large image tooltip
|
||||||
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
|
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
|
||||||
presence.Assets.LargeImageText = string.Empty;
|
presence.Assets.LargeImageText = string.Empty;
|
||||||
else
|
else
|
||||||
@ -131,17 +235,55 @@ 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;
|
||||||
|
|
||||||
client.SetPresence(presence);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() =>
|
||||||
|
{
|
||||||
|
game.Window?.Raise();
|
||||||
|
|
||||||
|
if (!api.IsLoggedIn)
|
||||||
|
{
|
||||||
|
login?.Show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug);
|
||||||
|
|
||||||
|
// Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other.
|
||||||
|
// Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion.
|
||||||
|
if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password))
|
||||||
|
{
|
||||||
|
Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new GetRoomRequest(roomId);
|
||||||
|
request.Success += room => Schedule(() =>
|
||||||
|
{
|
||||||
|
game.PresentMultiplayerMatch(room, password);
|
||||||
|
});
|
||||||
|
request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important);
|
||||||
|
api.Queue(request);
|
||||||
|
});
|
||||||
|
|
||||||
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
||||||
|
|
||||||
private 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;
|
||||||
|
|
||||||
@ -159,7 +301,31 @@ namespace osu.Desktop
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private int? getBeatmapID(UserActivity activity)
|
private static bool tryParseRoomSecret(string secretJson, out long roomId, out string? password)
|
||||||
|
{
|
||||||
|
roomId = 0;
|
||||||
|
password = null;
|
||||||
|
|
||||||
|
RoomSecret? roomSecret;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
roomSecret = JsonConvert.DeserializeObject<RoomSecret>(secretJson);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomSecret == null) return false;
|
||||||
|
|
||||||
|
roomId = roomSecret.RoomID;
|
||||||
|
password = roomSecret.Password;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? getBeatmapID(UserActivity activity)
|
||||||
{
|
{
|
||||||
switch (activity)
|
switch (activity)
|
||||||
{
|
{
|
||||||
@ -175,8 +341,20 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
|
if (multiplayerClient.IsNotNull())
|
||||||
|
multiplayerClient.RoomUpdated -= onRoomUpdated;
|
||||||
|
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class RoomSecret
|
||||||
|
{
|
||||||
|
[JsonProperty(@"roomId", Required = Required.Always)]
|
||||||
|
public long RoomID { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"password", Required = Required.AllowNull)]
|
||||||
|
public string? Password { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@
|
|||||||
using System;
|
using System;
|
||||||
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,
|
||||||
|
@ -7,6 +7,7 @@ using System.IO;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
using osu.Desktop.Performance;
|
||||||
using osu.Desktop.Security;
|
using osu.Desktop.Security;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
@ -15,11 +16,12 @@ using osu.Framework;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
using osu.Desktop.Windows;
|
using osu.Desktop.Windows;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.IPC;
|
using osu.Game.IPC;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Performance;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using SDL2;
|
|
||||||
|
|
||||||
namespace osu.Desktop
|
namespace osu.Desktop
|
||||||
{
|
{
|
||||||
@ -28,6 +30,9 @@ namespace osu.Desktop
|
|||||||
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
|
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
|
||||||
private ArchiveImportIPCChannel? archiveImportIPCChannel;
|
private ArchiveImportIPCChannel? archiveImportIPCChannel;
|
||||||
|
|
||||||
|
[Cached(typeof(IHighPerformanceSessionManager))]
|
||||||
|
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
|
||||||
|
|
||||||
public OsuGameDesktop(string[]? args = null)
|
public OsuGameDesktop(string[]? args = null)
|
||||||
: base(args)
|
: base(args)
|
||||||
{
|
{
|
||||||
@ -86,8 +91,8 @@ namespace osu.Desktop
|
|||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
private string? getStableInstallPathFromRegistry()
|
private string? getStableInstallPathFromRegistry()
|
||||||
{
|
{
|
||||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
|
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
||||||
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override UpdateManager CreateUpdateManager()
|
protected override UpdateManager CreateUpdateManager()
|
||||||
@ -155,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)
|
||||||
{
|
{
|
||||||
@ -163,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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
osu.Desktop/Performance/HighPerformanceSessionManager.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime;
|
||||||
|
using System.Threading;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Performance;
|
||||||
|
|
||||||
|
namespace osu.Desktop.Performance
|
||||||
|
{
|
||||||
|
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
|
||||||
|
{
|
||||||
|
public bool IsSessionActive => activeSessions > 0;
|
||||||
|
|
||||||
|
private int activeSessions;
|
||||||
|
|
||||||
|
private GCLatencyMode originalGCMode;
|
||||||
|
|
||||||
|
public IDisposable BeginSession()
|
||||||
|
{
|
||||||
|
enterSession();
|
||||||
|
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.exitSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enterSession()
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref activeSessions) > 1)
|
||||||
|
{
|
||||||
|
Logger.Log($"High performance session requested ({activeSessions} running in total)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Log("Starting high performance session");
|
||||||
|
|
||||||
|
originalGCMode = GCSettings.LatencyMode;
|
||||||
|
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
|
||||||
|
|
||||||
|
// Without doing this, the new GC mode won't kick in until the next GC, which could be at a more noticeable point in time.
|
||||||
|
GC.Collect(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exitSession()
|
||||||
|
{
|
||||||
|
if (Interlocked.Decrement(ref activeSessions) > 0)
|
||||||
|
{
|
||||||
|
Logger.Log($"High performance session finished ({activeSessions} others remain)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Log("Ending high performance session");
|
||||||
|
|
||||||
|
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
|
||||||
|
GCSettings.LatencyMode = originalGCMode;
|
||||||
|
|
||||||
|
// No GC.Collect() as we were already collecting at a higher frequency in the old mode.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using osu.Desktop.LegacyIpc;
|
using osu.Desktop.LegacyIpc;
|
||||||
|
using osu.Desktop.Windows;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Development;
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
@ -12,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
|
||||||
@ -50,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();
|
||||||
}
|
}
|
||||||
@ -69,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
|
||||||
@ -102,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)
|
||||||
{
|
{
|
||||||
@ -173,13 +184,16 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
tools.CreateShortcutForThisExe();
|
tools.CreateShortcutForThisExe();
|
||||||
tools.CreateUninstallerRegistryEntry();
|
tools.CreateUninstallerRegistryEntry();
|
||||||
|
WindowsAssociationManager.InstallAssociations();
|
||||||
}, onAppUpdate: (_, tools) =>
|
}, onAppUpdate: (_, tools) =>
|
||||||
{
|
{
|
||||||
tools.CreateUninstallerRegistryEntry();
|
tools.CreateUninstallerRegistryEntry();
|
||||||
|
WindowsAssociationManager.UpdateAssociations();
|
||||||
}, onAppUninstall: (_, tools) =>
|
}, onAppUninstall: (_, tools) =>
|
||||||
{
|
{
|
||||||
tools.RemoveShortcutForThisExe();
|
tools.RemoveShortcutForThisExe();
|
||||||
tools.RemoveUninstallerRegistryEntry();
|
tools.RemoveUninstallerRegistryEntry();
|
||||||
|
WindowsAssociationManager.UninstallAssociations();
|
||||||
}, onEveryRun: (_, _, _) =>
|
}, onEveryRun: (_, _, _) =>
|
||||||
{
|
{
|
||||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||||
|
25
osu.Desktop/SDL2BatteryInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
27
osu.Desktop/SDL3BatteryInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
17
osu.Desktop/Windows/Icons.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace osu.Desktop.Windows
|
||||||
|
{
|
||||||
|
public static class Icons
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fully qualified path to the directory that contains icons (in the installation folder).
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||||
|
|
||||||
|
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||||
|
}
|
||||||
|
}
|
295
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
// 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.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
|
namespace osu.Desktop.Windows
|
||||||
|
{
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public static class WindowsAssociationManager
|
||||||
|
{
|
||||||
|
private const string software_classes = @"Software\Classes";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sub key for setting the icon.
|
||||||
|
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
|
||||||
|
/// </summary>
|
||||||
|
private const string default_icon = @"DefaultIcon";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sub key for setting the command line that the shell invokes.
|
||||||
|
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
|
||||||
|
/// </summary>
|
||||||
|
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
|
||||||
|
|
||||||
|
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||||
|
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||||
|
/// </summary>
|
||||||
|
private const string program_id_prefix = "osu.File";
|
||||||
|
|
||||||
|
private static readonly FileAssociation[] file_associations =
|
||||||
|
{
|
||||||
|
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||||
|
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||||
|
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||||
|
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly UriAssociation[] uri_associations =
|
||||||
|
{
|
||||||
|
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
||||||
|
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs file and URI associations.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||||
|
/// </remarks>
|
||||||
|
public static void InstallAssociations()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updateAssociations();
|
||||||
|
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||||
|
NotifyShellUpdate();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates associations with latest definitions.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||||
|
/// </remarks>
|
||||||
|
public static void UpdateAssociations()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updateAssociations();
|
||||||
|
|
||||||
|
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||||
|
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||||
|
|
||||||
|
NotifyShellUpdate();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, @"Failed to update file and URI associations.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updateDescriptions(localisationManager);
|
||||||
|
NotifyShellUpdate();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, @"Failed to update file and URI association descriptions.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UninstallAssociations()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var association in file_associations)
|
||||||
|
association.Uninstall();
|
||||||
|
|
||||||
|
foreach (var association in uri_associations)
|
||||||
|
association.Uninstall();
|
||||||
|
|
||||||
|
NotifyShellUpdate();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, @"Failed to uninstall file and URI associations.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs or updates associations.
|
||||||
|
/// </summary>
|
||||||
|
private static void updateAssociations()
|
||||||
|
{
|
||||||
|
foreach (var association in file_associations)
|
||||||
|
association.Install();
|
||||||
|
|
||||||
|
foreach (var association in uri_associations)
|
||||||
|
association.Install();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateDescriptions(LocalisationManager? localisation)
|
||||||
|
{
|
||||||
|
foreach (var association in file_associations)
|
||||||
|
association.UpdateDescription(getLocalisedString(association.Description));
|
||||||
|
|
||||||
|
foreach (var association in uri_associations)
|
||||||
|
association.UpdateDescription(getLocalisedString(association.Description));
|
||||||
|
|
||||||
|
string getLocalisedString(LocalisableString s)
|
||||||
|
{
|
||||||
|
if (localisation == null)
|
||||||
|
return s.ToString();
|
||||||
|
|
||||||
|
var b = localisation.GetLocalisedBindableString(s);
|
||||||
|
b.UnbindAll();
|
||||||
|
return b.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Native interop
|
||||||
|
|
||||||
|
[DllImport("Shell32.dll")]
|
||||||
|
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private enum EventId
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
|
||||||
|
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
|
||||||
|
/// </summary>
|
||||||
|
SHCNE_ASSOCCHANGED = 0x08000000
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private enum Flags : uint
|
||||||
|
{
|
||||||
|
SHCNF_IDLIST = 0x0000
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||||
|
{
|
||||||
|
private string programId => $@"{program_id_prefix}{Extension}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||||
|
/// </summary>
|
||||||
|
public void Install()
|
||||||
|
{
|
||||||
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
|
if (classes == null) return;
|
||||||
|
|
||||||
|
// register a program id for the given extension
|
||||||
|
using (var programKey = classes.CreateSubKey(programId))
|
||||||
|
{
|
||||||
|
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||||
|
defaultIconKey.SetValue(null, IconPath);
|
||||||
|
|
||||||
|
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||||
|
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||||
|
{
|
||||||
|
// set ourselves as the default program
|
||||||
|
extensionKey.SetValue(null, programId);
|
||||||
|
|
||||||
|
// add to the open with dialog
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||||
|
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||||
|
openWithKey.SetValue(programId, string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDescription(string description)
|
||||||
|
{
|
||||||
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
|
if (classes == null) return;
|
||||||
|
|
||||||
|
using (var programKey = classes.OpenSubKey(programId, true))
|
||||||
|
programKey?.SetValue(null, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
|
||||||
|
/// </summary>
|
||||||
|
public void Uninstall()
|
||||||
|
{
|
||||||
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
|
if (classes == null) return;
|
||||||
|
|
||||||
|
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||||
|
{
|
||||||
|
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||||
|
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||||
|
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||||
|
extensionKey.SetValue(null, string.Empty);
|
||||||
|
|
||||||
|
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||||
|
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||||
|
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||||
|
/// </summary>
|
||||||
|
public const string URL_PROTOCOL = @"URL Protocol";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||||
|
/// </summary>
|
||||||
|
public void Install()
|
||||||
|
{
|
||||||
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
|
if (classes == null) return;
|
||||||
|
|
||||||
|
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||||
|
{
|
||||||
|
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||||
|
|
||||||
|
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||||
|
defaultIconKey.SetValue(null, IconPath);
|
||||||
|
|
||||||
|
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||||
|
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDescription(string description)
|
||||||
|
{
|
||||||
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
|
if (classes == null) return;
|
||||||
|
|
||||||
|
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||||
|
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Uninstall()
|
||||||
|
{
|
||||||
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
|
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@ -31,4 +31,7 @@
|
|||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup Label="Windows Icons">
|
||||||
|
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
<file src="**.dll" target="lib\net45\"/>
|
<file src="**.dll" target="lib\net45\"/>
|
||||||
<file src="**.config" target="lib\net45\"/>
|
<file src="**.config" target="lib\net45\"/>
|
||||||
<file src="**.json" target="lib\net45\"/>
|
<file src="**.json" target="lib\net45\"/>
|
||||||
|
<file src="**.ico" target="lib\net45\"/>
|
||||||
<file src="icon.png" target=""/>
|
<file src="icon.png" target=""/>
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||||
[TestCase("112643")]
|
[TestCase("112643")]
|
||||||
|
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||||
|
|
||||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||||
|
59
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Judgements;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CatchHealthProcessorTest
|
||||||
|
{
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
[
|
||||||
|
// hitobject, starting HP, fail expected after miss
|
||||||
|
[new Fruit(), 0.01, true],
|
||||||
|
[new Droplet(), 0.01, true],
|
||||||
|
[new TinyDroplet(), 0, false],
|
||||||
|
[new Banana(), 0, false],
|
||||||
|
[new BananaShower(), 0, false]
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
|
||||||
|
{
|
||||||
|
var healthProcessor = new CatchHealthProcessor(0);
|
||||||
|
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||||
|
{
|
||||||
|
HitObjects = { hitObject }
|
||||||
|
});
|
||||||
|
healthProcessor.Health.Value = startingHealth;
|
||||||
|
|
||||||
|
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||||
|
result.Type = result.Judgement.MinResult;
|
||||||
|
healthProcessor.ApplyResult(result);
|
||||||
|
|
||||||
|
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
|
||||||
|
{
|
||||||
|
var healthProcessor = new CatchHealthProcessor(0);
|
||||||
|
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||||
|
{
|
||||||
|
HitObjects = { hitObject }
|
||||||
|
});
|
||||||
|
healthProcessor.Health.Value = startingHealth;
|
||||||
|
|
||||||
|
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||||
|
result.Type = result.Judgement.MaxResult;
|
||||||
|
healthProcessor.ApplyResult(result);
|
||||||
|
|
||||||
|
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CheckCatchAbnormalDifficultySettingsTest
|
||||||
|
{
|
||||||
|
private CheckCatchAbnormalDifficultySettings check = null!;
|
||||||
|
|
||||||
|
private readonly IBeatmap beatmap = new Beatmap<HitObject>();
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckCatchAbnormalDifficultySettings();
|
||||||
|
|
||||||
|
beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo;
|
||||||
|
beatmap.Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
ApproachRate = 5,
|
||||||
|
CircleSize = 5,
|
||||||
|
DrainRate = 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNormalSettings()
|
||||||
|
{
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApproachRateTwoDecimals()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.ApproachRate = 5.55f;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCircleSizeTwoDecimals()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.CircleSize = 5.55f;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainRateTwoDecimals()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.DrainRate = 5.55f;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApproachRateUnder()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.ApproachRate = -10;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCircleSizeUnder()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.CircleSize = -10;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainRateUnder()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.DrainRate = -10;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApproachRateOver()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.ApproachRate = 20;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCircleSizeOver()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.CircleSize = 20;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainRateOver()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.DrainRate = 20;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BeatmapVerifierContext getContext()
|
||||||
|
{
|
||||||
|
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,253 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneCatchReverseSelection : TestSceneEditor
|
||||||
|
{
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReverseSelectionTwoFruits()
|
||||||
|
{
|
||||||
|
CatchHitObject[] objects = null!;
|
||||||
|
bool[] newCombos = null!;
|
||||||
|
|
||||||
|
addObjects([
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 200,
|
||||||
|
X = 0,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 400,
|
||||||
|
X = 20,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
AddStep("store objects & new combo data", () =>
|
||||||
|
{
|
||||||
|
objects = getObjects().ToArray();
|
||||||
|
newCombos = getObjectNewCombos().ToArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectEverything();
|
||||||
|
reverseSelection();
|
||||||
|
|
||||||
|
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||||
|
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReverseSelectionThreeFruits()
|
||||||
|
{
|
||||||
|
CatchHitObject[] objects = null!;
|
||||||
|
bool[] newCombos = null!;
|
||||||
|
|
||||||
|
addObjects([
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 200,
|
||||||
|
X = 0,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 400,
|
||||||
|
X = 20,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 600,
|
||||||
|
X = 40,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
AddStep("store objects & new combo data", () =>
|
||||||
|
{
|
||||||
|
objects = getObjects().ToArray();
|
||||||
|
newCombos = getObjectNewCombos().ToArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectEverything();
|
||||||
|
reverseSelection();
|
||||||
|
|
||||||
|
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||||
|
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReverseSelectionFruitAndJuiceStream()
|
||||||
|
{
|
||||||
|
CatchHitObject[] objects = null!;
|
||||||
|
bool[] newCombos = null!;
|
||||||
|
|
||||||
|
addObjects([
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 200,
|
||||||
|
X = 0,
|
||||||
|
},
|
||||||
|
new JuiceStream
|
||||||
|
{
|
||||||
|
StartTime = 400,
|
||||||
|
X = 20,
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(50))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
AddStep("store objects & new combo data", () =>
|
||||||
|
{
|
||||||
|
objects = getObjects().ToArray();
|
||||||
|
newCombos = getObjectNewCombos().ToArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectEverything();
|
||||||
|
reverseSelection();
|
||||||
|
|
||||||
|
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||||
|
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReverseSelectionTwoFruitsAndJuiceStream()
|
||||||
|
{
|
||||||
|
CatchHitObject[] objects = null!;
|
||||||
|
bool[] newCombos = null!;
|
||||||
|
|
||||||
|
addObjects([
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 200,
|
||||||
|
X = 0,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 400,
|
||||||
|
X = 20,
|
||||||
|
},
|
||||||
|
new JuiceStream
|
||||||
|
{
|
||||||
|
StartTime = 600,
|
||||||
|
X = 40,
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(50))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
AddStep("store objects & new combo data", () =>
|
||||||
|
{
|
||||||
|
objects = getObjects().ToArray();
|
||||||
|
newCombos = getObjectNewCombos().ToArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectEverything();
|
||||||
|
reverseSelection();
|
||||||
|
|
||||||
|
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||||
|
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReverseSelectionTwoCombos()
|
||||||
|
{
|
||||||
|
CatchHitObject[] objects = null!;
|
||||||
|
bool[] newCombos = null!;
|
||||||
|
|
||||||
|
addObjects([
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 200,
|
||||||
|
X = 0,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 400,
|
||||||
|
X = 20,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 600,
|
||||||
|
X = 40,
|
||||||
|
},
|
||||||
|
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 800,
|
||||||
|
NewCombo = true,
|
||||||
|
X = 60,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
X = 80,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
StartTime = 1200,
|
||||||
|
X = 100,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
AddStep("store objects & new combo data", () =>
|
||||||
|
{
|
||||||
|
objects = getObjects().ToArray();
|
||||||
|
newCombos = getObjectNewCombos().ToArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
selectEverything();
|
||||||
|
reverseSelection();
|
||||||
|
|
||||||
|
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||||
|
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addObjects(CatchHitObject[] hitObjects) => AddStep("Add objects", () => EditorBeatmap.AddRange(hitObjects));
|
||||||
|
|
||||||
|
private IEnumerable<CatchHitObject> getObjects() => EditorBeatmap.HitObjects.OfType<CatchHitObject>();
|
||||||
|
|
||||||
|
private IEnumerable<bool> getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
|
||||||
|
|
||||||
|
private void selectEverything()
|
||||||
|
{
|
||||||
|
AddStep("Select everything", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reverseSelection()
|
||||||
|
{
|
||||||
|
AddStep("Reverse selection", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.LControl);
|
||||||
|
InputManager.Key(Key.G);
|
||||||
|
InputManager.ReleaseKey(Key.LControl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
AudioFilename: audio.mp3
|
||||||
|
AudioLeadIn: 0
|
||||||
|
PreviewTime: 65316
|
||||||
|
Countdown: 0
|
||||||
|
SampleSet: Soft
|
||||||
|
StackLeniency: 0.7
|
||||||
|
Mode: 2
|
||||||
|
LetterboxInBreaks: 0
|
||||||
|
WidescreenStoryboard: 0
|
||||||
|
|
||||||
|
[Editor]
|
||||||
|
DistanceSpacing: 1.4
|
||||||
|
BeatDivisor: 4
|
||||||
|
GridSize: 8
|
||||||
|
TimelineZoom: 1.4
|
||||||
|
|
||||||
|
[Metadata]
|
||||||
|
Title:Nanairo Symphony -TV Size-
|
||||||
|
TitleUnicode:七色シンフォニー -TV Size-
|
||||||
|
Artist:Coalamode.
|
||||||
|
ArtistUnicode:コアラモード.
|
||||||
|
Creator:Ascendance
|
||||||
|
Version:Aru's Cup
|
||||||
|
Source:四月は君の嘘
|
||||||
|
Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
|
||||||
|
BeatmapID:1041052
|
||||||
|
BeatmapSetID:488149
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:3
|
||||||
|
CircleSize:2.5
|
||||||
|
OverallDifficulty:6
|
||||||
|
ApproachRate:6
|
||||||
|
SliderMultiplier:1.02
|
||||||
|
SliderTickRate:2
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
//Background and Video events
|
||||||
|
Video,500,"forty.avi"
|
||||||
|
0,0,"cropped-1366-768-647733.jpg",0,0
|
||||||
|
//Break Periods
|
||||||
|
//Storyboard Layer 0 (Background)
|
||||||
|
//Storyboard Layer 1 (Fail)
|
||||||
|
//Storyboard Layer 2 (Pass)
|
||||||
|
//Storyboard Layer 3 (Foreground)
|
||||||
|
//Storyboard Sound Samples
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
1155,387.096774193548,4,2,1,50,1,0
|
||||||
|
15284,-100,4,2,1,60,0,0
|
||||||
|
16638,-100,4,2,1,50,0,0
|
||||||
|
41413,-100,4,2,1,60,0,0
|
||||||
|
59993,-100,4,2,1,65,0,0
|
||||||
|
66187,-100,4,2,1,70,0,1
|
||||||
|
87284,-100,4,2,1,60,0,1
|
||||||
|
87864,-100,4,2,1,70,0,0
|
||||||
|
87961,-100,4,2,1,50,0,0
|
||||||
|
88638,-100,4,2,1,30,0,0
|
||||||
|
89413,-100,4,2,1,10,0,0
|
||||||
|
89800,-100,4,2,1,5,0,0
|
||||||
|
|
||||||
|
|
||||||
|
[Colours]
|
||||||
|
Combo1 : 255,128,64
|
||||||
|
Combo2 : 0,128,255
|
||||||
|
Combo3 : 255,128,192
|
||||||
|
Combo4 : 0,128,192
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
160,160,2122,1,0,0:0:0:0:
|
||||||
|
272,160,2509,1,2,0:0:0:0:
|
||||||
|
448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
384,96,4058,1,2,0:0:0:0:
|
||||||
|
128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
192,64,5800,1,2,0:0:0:0:
|
||||||
|
240,64,5993,1,2,0:0:0:0:
|
||||||
|
288,64,6187,1,2,0:0:0:0:
|
||||||
|
416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
|
||||||
|
488,160,8122,2,0,L|376:160,1,102
|
||||||
|
457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
400,288,10058,1,0,0:0:0:0:
|
||||||
|
304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
400,288,11606,1,0,0:0:0:0:
|
||||||
|
240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
0,288,13154,1,0,0:0:0:0:
|
||||||
|
112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
|
||||||
|
288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
|
||||||
|
312,208,15864,1,6,0:0:0:0:
|
||||||
|
128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
432,176,20896,1,0,0:0:0:0:
|
||||||
|
328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
328,176,22445,1,0,0:0:0:0:
|
||||||
|
224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
224,176,23993,1,0,0:0:0:0:
|
||||||
|
112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
416,176,25541,1,0,0:0:0:0:
|
||||||
|
304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
480,112,27090,1,0,0:0:0:0:
|
||||||
|
384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
432,112,28058,1,2,0:0:0:0:
|
||||||
|
333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||||
|
224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
272,272,30574,2,0,L|374:272,1,102
|
||||||
|
424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
|
||||||
|
224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
200,368,32703,1,2,0:0:0:0:
|
||||||
|
376,368,33284,1,0,0:0:0:0:
|
||||||
|
304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
280,16,36380,1,2,0:0:0:0:
|
||||||
|
176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
176,224,37928,1,2,0:0:0:0:
|
||||||
|
304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
144,264,38896,1,2,0:0:0:0:
|
||||||
|
280,336,39477,2,0,L|336:336,1,51
|
||||||
|
424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
|
||||||
|
352,232,40445,1,4,0:1:0:0:
|
||||||
|
160,224,41025,1,8,0:3:0:0:
|
||||||
|
256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||||
|
400,40,41993,1,0,0:0:0:0:
|
||||||
|
440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
|
||||||
|
144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
56,136,45284,1,2,0:0:0:0:
|
||||||
|
160,144,45671,1,8,0:0:0:0:
|
||||||
|
264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
|
||||||
|
360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
144,136,49154,1,2,0:0:0:0:
|
||||||
|
248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
|
||||||
|
256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
|
||||||
|
256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
400,208,51284,1,0,0:0:0:0:
|
||||||
|
352,240,51477,2,0,L|240:240,1,102
|
||||||
|
128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||||
|
88,264,53025,1,2,0:0:0:0:
|
||||||
|
168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||||
|
248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
|
||||||
|
432,120,54574,1,2,0:0:0:0:
|
||||||
|
328,128,54961,1,8,0:0:0:0:
|
||||||
|
224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
224,184,56509,1,8,0:3:0:0:
|
||||||
|
328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
224,272,58445,5,2,0:0:0:0:
|
||||||
|
320,296,58832,1,0,0:0:0:0:
|
||||||
|
224,328,59219,1,2,0:0:0:0:
|
||||||
|
120,328,59606,1,8,0:3:0:0:
|
||||||
|
224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||||
|
80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||||
|
376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
160,112,64638,1,8,0:0:0:0:
|
||||||
|
208,128,64832,1,8,0:0:0:0:
|
||||||
|
256,144,65025,1,8,0:0:0:0:
|
||||||
|
360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||||
|
344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||||
|
152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
176,208,68316,1,0,0:0:0:0:
|
||||||
|
224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||||
|
176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||||
|
120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||||
|
296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
|
||||||
|
208,248,77800,1,8,0:0:0:0:
|
||||||
|
320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||||
|
456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||||
|
288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
96,72,80703,1,0,0:0:0:0:
|
||||||
|
40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||||
|
248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||||
|
208,272,82445,1,10,0:0:0:0:
|
||||||
|
312,272,82832,1,8,0:0:0:0:
|
||||||
|
400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
|
||||||
|
360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
|
||||||
|
80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||||
|
104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||||
|
376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
|
||||||
|
296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
|
||||||
|
168,96,87864,1,4,0:0:0:0:
|
||||||
|
256,192,88251,12,0,89800,0:0:0:0:
|
@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
float offsetPosition = hitObject.OriginalX;
|
float offsetPosition = hitObject.OriginalX;
|
||||||
double startTime = hitObject.StartTime;
|
double startTime = hitObject.StartTime;
|
||||||
|
|
||||||
if (lastPosition == null)
|
if (lastPosition == null ||
|
||||||
|
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
|
||||||
|
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
|
||||||
|
// todo: should be revisited and corrected later probably.
|
||||||
|
lastPosition == 0)
|
||||||
{
|
{
|
||||||
lastPosition = offsetPosition;
|
lastPosition = offsetPosition;
|
||||||
lastStartTime = startTime;
|
lastStartTime = startTime;
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,21 @@
|
|||||||
// 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.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
public class CatchPerformanceCalculator : PerformanceCalculator
|
public class CatchPerformanceCalculator : PerformanceCalculator
|
||||||
{
|
{
|
||||||
private int fruitsHit;
|
private int num300;
|
||||||
private int ticksHit;
|
private int num100;
|
||||||
private int tinyTicksHit;
|
private int num50;
|
||||||
private int tinyTicksMissed;
|
private int numKatu;
|
||||||
private int misses;
|
private int numMiss;
|
||||||
|
|
||||||
public CatchPerformanceCalculator()
|
public CatchPerformanceCalculator()
|
||||||
: base(new CatchRuleset())
|
: base(new CatchRuleset())
|
||||||
@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
var catchAttributes = (CatchDifficultyAttributes)attributes;
|
var catchAttributes = (CatchDifficultyAttributes)attributes;
|
||||||
|
|
||||||
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
|
num300 = score.GetCount300() ?? 0; // HitResult.Great
|
||||||
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
|
||||||
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
|
||||||
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
|
||||||
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
|
||||||
|
|
||||||
// We are heavily relying on aim in catch the beat
|
// We are heavily relying on aim in catch the beat
|
||||||
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||||
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
||||||
value *= lengthBonus;
|
value *= lengthBonus;
|
||||||
|
|
||||||
value *= Math.Pow(0.97, misses);
|
value *= Math.Pow(0.97, numMiss);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if (catchAttributes.MaxCombo > 0)
|
if (catchAttributes.MaxCombo > 0)
|
||||||
@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
||||||
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
|
private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
|
||||||
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
|
private int totalSuccessfulHits() => num50 + num100 + num300;
|
||||||
private int totalComboHits() => misses + ticksHit + fruitsHit;
|
private int totalComboHits() => numMiss + num100 + num300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||||
|
{
|
||||||
|
public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySettings
|
||||||
|
{
|
||||||
|
public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks catch relevant settings");
|
||||||
|
|
||||||
|
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||||
|
{
|
||||||
|
var diff = context.Beatmap.Difficulty;
|
||||||
|
Issue? issue;
|
||||||
|
|
||||||
|
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
|
||||||
|
yield return issue;
|
||||||
|
|
||||||
|
if (OutOfRange("Approach rate", diff.ApproachRate, out issue))
|
||||||
|
yield return issue;
|
||||||
|
|
||||||
|
if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue))
|
||||||
|
yield return issue;
|
||||||
|
|
||||||
|
if (OutOfRange("Circle size", diff.CircleSize, out issue))
|
||||||
|
yield return issue;
|
||||||
|
|
||||||
|
if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
|
||||||
|
yield return issue;
|
||||||
|
|
||||||
|
if (OutOfRange("Drain rate", diff.DrainRate, out issue))
|
||||||
|
yield return issue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
@ -21,6 +22,23 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
|||||||
|
|
||||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||||
|
|
||||||
|
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||||
|
{
|
||||||
|
// matches stable.
|
||||||
|
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
|
||||||
|
// the above early-return skips the failure check at the end of the same method:
|
||||||
|
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
|
||||||
|
// making it impossible to fail on a tiny droplet regardless of result.
|
||||||
|
if (result.Type == HitResult.SmallTickMiss)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// on stable, banana showers don't exist as concrete objects themselves, so they can't cause a fail.
|
||||||
|
if (result.HitObject is BananaShower)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return base.CheckDefaultFailCondition(result);
|
||||||
|
}
|
||||||
|
|
||||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
double increase = 0;
|
double increase = 0;
|
||||||
|
@ -16,6 +16,8 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
{
|
{
|
||||||
@ -52,5 +54,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
|
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
|
||||||
|
|
||||||
public override DrawableHitObject<CatchHitObject>? CreateDrawableRepresentation(CatchHitObject h) => null;
|
public override DrawableHitObject<CatchHitObject>? CreateDrawableRepresentation(CatchHitObject h) => null;
|
||||||
|
|
||||||
|
protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Edit.Checks;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CheckKeyCountTest
|
||||||
|
{
|
||||||
|
private CheckKeyCount check = null!;
|
||||||
|
|
||||||
|
private IBeatmap beatmap = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckKeyCount();
|
||||||
|
|
||||||
|
beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Ruleset = new ManiaRuleset().RulesetInfo
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKeycountFour()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.CircleSize = 4;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKeycountSmallerThanFour()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.CircleSize = 1;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BeatmapVerifierContext getContext()
|
||||||
|
{
|
||||||
|
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CheckManiaAbnormalDifficultySettingsTest
|
||||||
|
{
|
||||||
|
private CheckManiaAbnormalDifficultySettings check = null!;
|
||||||
|
|
||||||
|
private readonly IBeatmap beatmap = new Beatmap<HitObject>();
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckManiaAbnormalDifficultySettings();
|
||||||
|
|
||||||
|
beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo;
|
||||||
|
beatmap.Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
OverallDifficulty = 5,
|
||||||
|
DrainRate = 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNormalSettings()
|
||||||
|
{
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverallDifficultyTwoDecimals()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.OverallDifficulty = 5.55f;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainRateTwoDecimals()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.DrainRate = 5.55f;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverallDifficultyUnder()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.OverallDifficulty = -10;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainRateUnder()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.DrainRate = -10;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverallDifficultyOver()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.OverallDifficulty = 20;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainRateOver()
|
||||||
|
{
|
||||||
|
beatmap.Difficulty.DrainRate = 20;
|
||||||
|
|
||||||
|
var context = getContext();
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BeatmapVerifierContext getContext()
|
||||||
|
{
|
||||||
|
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Scoring;
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
// No matter what, mania doesn't have passive HP drain.
|
// No matter what, mania doesn't have passive HP drain.
|
||||||
Assert.That(processor.DrainRate, Is.Zero);
|
Assert.That(processor.DrainRate, Is.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
[
|
||||||
|
// hitobject, starting HP, fail expected after miss
|
||||||
|
[new Note(), 0.01, true],
|
||||||
|
[new HeadNote(), 0.01, true],
|
||||||
|
[new TailNote(), 0.01, true],
|
||||||
|
[new HoldNoteBody(), 0, true], // hold note break
|
||||||
|
[new HoldNote(), 0, true],
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
|
||||||
|
{
|
||||||
|
var healthProcessor = new ManiaHealthProcessor(0);
|
||||||
|
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||||
|
{
|
||||||
|
HitObjects = { hitObject }
|
||||||
|
});
|
||||||
|
healthProcessor.Health.Value = startingHealth;
|
||||||
|
|
||||||
|
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||||
|
result.Type = result.Judgement.MinResult;
|
||||||
|
healthProcessor.ApplyResult(result);
|
||||||
|
|
||||||
|
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
|
||||||
|
{
|
||||||
|
var healthProcessor = new ManiaHealthProcessor(0);
|
||||||
|
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||||
|
{
|
||||||
|
HitObjects = { hitObject }
|
||||||
|
});
|
||||||
|
healthProcessor.Health.Value = startingHealth;
|
||||||
|
|
||||||
|
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||||
|
result.Type = result.Judgement.MaxResult;
|
||||||
|
healthProcessor.ApplyResult(result);
|
||||||
|
|
||||||
|
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("setup hierarchy", () => Child = new Container
|
AddStep("setup hierarchy", () =>
|
||||||
|
{
|
||||||
|
Child = new Container
|
||||||
{
|
{
|
||||||
Clock = new FramedClock(clock = new ManualClock()),
|
Clock = new FramedClock(clock = new ManualClock()),
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -44,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drawableRuleset.AllowBackwardsSeeks = true;
|
||||||
});
|
});
|
||||||
AddStep("retrieve config bindable", () =>
|
AddStep("retrieve config bindable", () =>
|
||||||
{
|
{
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
39
osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Mania.Skinning;
|
using osu.Game.Rulesets.Mania.Skinning;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -101,6 +102,14 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
|
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (skin.IsNotNull())
|
||||||
|
skin.SourceChanged -= onSkinChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
@ -46,17 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
// Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key.
|
||||||
upSprite = new Sprite
|
upSprite = new Sprite
|
||||||
{
|
{
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
Texture = skin.GetTexture(upImage),
|
Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default),
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 1
|
Width = 1
|
||||||
},
|
},
|
||||||
downSprite = new Sprite
|
downSprite = new Sprite
|
||||||
{
|
{
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
Texture = skin.GetTexture(downImage),
|
Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default),
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 1,
|
Width = 1,
|
||||||
Alpha = 0
|
Alpha = 0
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -26,10 +26,12 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
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>
|
||||||
@ -42,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;
|
||||||
|
|
||||||
@ -102,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;
|
||||||
@ -164,6 +168,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
|
protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
|
||||||
|
|
||||||
|
protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay();
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
216
osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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", () =>
|
||||||
|
@ -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]
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
@ -32,6 +33,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayfieldBasedSize()
|
||||||
|
{
|
||||||
|
ModFlashlight mod = new OsuModFlashlight();
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = mod,
|
||||||
|
PassCondition = () =>
|
||||||
|
{
|
||||||
|
var flashlightOverlay = Player.DrawableRuleset.Overlays
|
||||||
|
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
|
||||||
|
.First();
|
||||||
|
|
||||||
|
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("adjust playfield scale", () =>
|
||||||
|
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSliderDimsOnlyAfterStartTime()
|
public void TestSliderDimsOnlyAfterStartTime()
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class OsuHealthProcessorTest
|
||||||
|
{
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
[
|
||||||
|
// hitobject, starting HP, fail expected after miss
|
||||||
|
[new HitCircle(), 0.01, true],
|
||||||
|
[new SliderHeadCircle(), 0.01, true],
|
||||||
|
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
|
||||||
|
[new SliderTick(), 0.01, true],
|
||||||
|
[new SliderRepeat(new Slider()), 0.01, true],
|
||||||
|
[new SliderTailCircle(new Slider()), 0, true],
|
||||||
|
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
|
||||||
|
[new Slider(), 0, true],
|
||||||
|
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
|
||||||
|
[new SpinnerTick(), 0, false],
|
||||||
|
[new SpinnerBonusTick(), 0, false],
|
||||||
|
[new Spinner(), 0.01, true],
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
|
||||||
|
{
|
||||||
|
var healthProcessor = new OsuHealthProcessor(0);
|
||||||
|
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||||
|
{
|
||||||
|
HitObjects = { hitObject }
|
||||||
|
});
|
||||||
|
healthProcessor.Health.Value = startingHealth;
|
||||||
|
|
||||||
|
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||||
|
result.Type = result.Judgement.MinResult;
|
||||||
|
healthProcessor.ApplyResult(result);
|
||||||
|
|
||||||
|
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
|
||||||
|
{
|
||||||
|
var healthProcessor = new OsuHealthProcessor(0);
|
||||||
|
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||||
|
{
|
||||||
|
HitObjects = { hitObject }
|
||||||
|
});
|
||||||
|
healthProcessor.Health.Value = startingHealth;
|
||||||
|
|
||||||
|
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||||
|
result.Type = result.Judgement.MaxResult;
|
||||||
|
healthProcessor.ApplyResult(result);
|
||||||
|
|
||||||
|
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
69
osu.Game.Rulesets.Osu.Tests/TestSceneResume.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,11 +6,11 @@ using NUnit.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.Framework.Graphics.Cursor;
|
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
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 osu.Game.Tests.Gameplay;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene
|
public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private ManualOsuInputManager osuInputManager = null!;
|
private ManualOsuInputManager osuInputManager = null!;
|
||||||
private CursorContainer cursor = null!;
|
private GameplayCursorContainer cursor = null!;
|
||||||
private ResumeOverlay resume = null!;
|
private ResumeOverlay resume = null!;
|
||||||
|
|
||||||
private bool resumeFired;
|
private bool resumeFired;
|
||||||
@ -99,7 +99,17 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
private void loadContent()
|
private void loadContent()
|
||||||
{
|
{
|
||||||
Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } };
|
Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
cursor = new GameplayCursorContainer(),
|
||||||
|
resume = new OsuResumeOverlay
|
||||||
|
{
|
||||||
|
GameplayCursor = cursor
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
resumeFired = false;
|
resumeFired = false;
|
||||||
resume.ResumeAction = () => resumeFired = true;
|
resume.ResumeAction = () => resumeFired = true;
|
||||||
|
@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var skinnable = firstObject.ApproachCircle;
|
var skinnable = firstObject.ApproachCircle;
|
||||||
|
|
||||||
if (skin == null && skinnable?.Drawable is DefaultApproachCircle)
|
if (skin == null && skinnable.Drawable is DefaultApproachCircle)
|
||||||
// check for default skin provider
|
// check for default skin provider
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var text = skinnable?.Drawable as SpriteText;
|
var text = skinnable.Drawable as SpriteText;
|
||||||
|
|
||||||
return text?.Text == skin;
|
return text?.Text == skin;
|
||||||
});
|
});
|
||||||
|
@ -457,6 +457,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
assertMidSliderJudgementFail();
|
assertMidSliderJudgementFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRewindHandling()
|
||||||
|
{
|
||||||
|
performTest(new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Position = new Vector2(0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
|
||||||
|
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = 3250 },
|
||||||
|
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
|
||||||
|
}, new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_slider_start,
|
||||||
|
Position = new Vector2(0, 0),
|
||||||
|
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(250, 0),
|
||||||
|
}, 250),
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
|
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||||
|
|
||||||
|
AddStep("rewind to middle of slider", () => currentPlayer.Seek(time_during_slide_4));
|
||||||
|
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
|
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||||
|
}
|
||||||
|
|
||||||
private void assertAllMaxJudgements()
|
private void assertAllMaxJudgements()
|
||||||
{
|
{
|
||||||
AddAssert("All judgements max", () =>
|
AddAssert("All judgements max", () =>
|
||||||
|
@ -58,9 +58,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
|
|
||||||
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
|
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
|
||||||
{
|
{
|
||||||
if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be greater than {nameof(endIndex)}.");
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||||
if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be less than 0.");
|
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
||||||
if (endIndex < 0) throw new ArgumentOutOfRangeException(nameof(endIndex), $"{nameof(endIndex)} cannot be less than 0.");
|
ArgumentOutOfRangeException.ThrowIfNegative(endIndex);
|
||||||
|
|
||||||
int extendedEndIndex = endIndex;
|
int extendedEndIndex = endIndex;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -111,7 +111,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);
|
||||||
@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
// 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);
|
||||||
@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
// 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))
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|