Compare commits
2828 Commits
@@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2023.1117.0",
|
||||
"version": "2024.802.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
||||
@@ -196,6 +196,9 @@ csharp_style_prefer_switch_expression = false:none
|
||||
|
||||
csharp_style_namespace_declarations = block_scoped:warning
|
||||
|
||||
#Style - C# 12 features
|
||||
csharp_style_prefer_primary_constructors = false
|
||||
|
||||
[*.{yaml,yml}]
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
|
||||
@@ -64,10 +64,11 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- { prettyname: Windows, fullname: windows-latest }
|
||||
- { prettyname: macOS, fullname: macos-latest }
|
||||
# macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
|
||||
# - { prettyname: macOS, fullname: macos-latest }
|
||||
- { prettyname: Linux, fullname: ubuntu-latest }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -120,9 +121,7 @@ jobs:
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
|
||||
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -136,8 +135,5 @@ jobs:
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install maui-ios
|
||||
|
||||
- name: Select Xcode 15.2
|
||||
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS
|
||||
|
||||
@@ -110,10 +110,14 @@ jobs:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
|
||||
steps:
|
||||
- name: Check permissions
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0
|
||||
with:
|
||||
require: 'write'
|
||||
run: |
|
||||
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte)
|
||||
for i in "${ALLOWED_USERS[@]}"; do
|
||||
if [[ "${{ github.actor }}" == "$i" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
|
||||
create-comment:
|
||||
name: Create PR comment
|
||||
@@ -122,7 +126,7 @@ jobs:
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
message: |
|
||||
@@ -249,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
@@ -280,7 +284,7 @@ jobs:
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
@@ -354,7 +358,7 @@ jobs:
|
||||
steps:
|
||||
- name: Update comment on success
|
||||
if: ${{ needs.generator.result == 'success' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
@@ -365,7 +369,7 @@ jobs:
|
||||
|
||||
- name: Update comment on failure
|
||||
if: ${{ needs.generator.result == 'failure' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
@@ -375,7 +379,7 @@ jobs:
|
||||
|
||||
- name: Update comment on cancellation
|
||||
if: ${{ needs.generator.result == 'cancelled' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: delete
|
||||
|
||||
@@ -265,6 +265,8 @@ __pycache__/
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/*/.idea/projectSettingsUpdater.xml
|
||||
.idea/*/.idea/encodings.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
@@ -340,4 +342,5 @@ inspectcode
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
||||
.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="vcsConfiguration" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="vcsConfiguration" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="vcsConfiguration" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="vcsConfiguration" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
||||
|
||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
|
||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> ins
|
||||
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
|
||||
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<Project>
|
||||
<PropertyGroup Label="C#">
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 438 KiB |
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.904.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- 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 Microsoft.Maui.Devices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Android.Input;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Input.Handlers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Settings.Sections.Input;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -84,28 +80,10 @@ namespace osu.Android
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
|
||||
|
||||
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
|
||||
{
|
||||
public override double? ChargeLevel => Battery.ChargeLevel;
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
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"
|
||||
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 android:height="108dp" android:viewportHeight="434"
|
||||
android:viewportWidth="434" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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 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"/>
|
||||
<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"/>
|
||||
</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 DiscordRPC;
|
||||
using DiscordRPC.Message;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Users;
|
||||
using LogLevel = osu.Framework.Logging.LogLevel;
|
||||
@@ -21,39 +28,78 @@ namespace osu.Desktop
|
||||
{
|
||||
internal partial class DiscordRichPresence : Component
|
||||
{
|
||||
private const string client_id = "367827983903490050";
|
||||
private const string client_id = "1216669957799018608";
|
||||
|
||||
private DiscordRpcClient client = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
private IBindable<APIUser> user = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private LoginOverlay? login { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
||||
|
||||
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
|
||||
|
||||
private readonly RichPresence presence = new RichPresence
|
||||
{
|
||||
Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
|
||||
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
|
||||
Secrets = new Secrets
|
||||
{
|
||||
JoinSecret = null,
|
||||
SpectateSecret = null,
|
||||
},
|
||||
};
|
||||
|
||||
private IBindable<APIUser>? user;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
private void load()
|
||||
{
|
||||
client = new DiscordRpcClient(client_id)
|
||||
{
|
||||
SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady.
|
||||
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
||||
// to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady).
|
||||
SkipIdenticalPresence = true
|
||||
};
|
||||
|
||||
client.OnReady += onReady;
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
|
||||
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
||||
try
|
||||
{
|
||||
client.RegisterUriScheme();
|
||||
client.Subscribe(EventType.Join);
|
||||
client.OnJoin += onJoin;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This is known to fail in at least the following sandboxed environments:
|
||||
// - macOS (when packaged as an app bundle)
|
||||
// - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170)
|
||||
// There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now.
|
||||
Logger.Log($"Failed to register Discord URI scheme: {ex}");
|
||||
}
|
||||
|
||||
client.Initialize();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
|
||||
@@ -67,37 +113,59 @@ namespace osu.Desktop
|
||||
activity.BindTo(u.NewValue.Activity);
|
||||
}, true);
|
||||
|
||||
ruleset.BindValueChanged(_ => updateStatus());
|
||||
status.BindValueChanged(_ => updateStatus());
|
||||
activity.BindValueChanged(_ => updateStatus());
|
||||
privacyMode.BindValueChanged(_ => updateStatus());
|
||||
|
||||
client.Initialize();
|
||||
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
status.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||
}
|
||||
|
||||
private void onReady(object _, ReadyMessage __)
|
||||
{
|
||||
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
|
||||
updateStatus();
|
||||
|
||||
// when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence).
|
||||
if (client.CurrentPresence != null)
|
||||
client.SetPresence(null);
|
||||
|
||||
schedulePresenceUpdate();
|
||||
}
|
||||
|
||||
private void updateStatus()
|
||||
private void onRoomUpdated() => schedulePresenceUpdate();
|
||||
|
||||
private ScheduledDelegate? presenceUpdateDelegate;
|
||||
|
||||
private void schedulePresenceUpdate()
|
||||
{
|
||||
if (!client.IsInitialized)
|
||||
return;
|
||||
|
||||
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||
presenceUpdateDelegate?.Cancel();
|
||||
presenceUpdateDelegate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
client.ClearPresence();
|
||||
return;
|
||||
}
|
||||
if (!client.IsInitialized)
|
||||
return;
|
||||
|
||||
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||
{
|
||||
client.ClearPresence();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activity.Value != null)
|
||||
{
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
||||
|
||||
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||
updatePresence(hideIdentifiableInformation);
|
||||
client.SetPresence(presence);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private void updatePresence(bool hideIdentifiableInformation)
|
||||
{
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -121,7 +189,42 @@ namespace osu.Desktop
|
||||
presence.Details = string.Empty;
|
||||
}
|
||||
|
||||
// update user information
|
||||
// user party
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
|
||||
{
|
||||
MultiplayerRoom room = multiplayerClient.Room;
|
||||
|
||||
presence.Party = new Party
|
||||
{
|
||||
Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private,
|
||||
ID = room.RoomID.ToString(),
|
||||
// technically lobbies can have infinite users, but Discord needs this to be set to something.
|
||||
// to make party display sensible, assign a powers of two above participants count (8 at minimum).
|
||||
Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))),
|
||||
Size = room.Users.Count,
|
||||
};
|
||||
|
||||
RoomSecret roomSecret = new RoomSecret
|
||||
{
|
||||
RoomID = room.RoomID,
|
||||
Password = room.Settings.Password,
|
||||
};
|
||||
|
||||
if (client.HasRegisteredUriScheme)
|
||||
presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret);
|
||||
|
||||
// discord cannot handle both secrets and buttons at the same time, so we need to choose something.
|
||||
// the multiplayer room seems more important.
|
||||
presence.Buttons = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
presence.Party = null;
|
||||
presence.Secrets.JoinSecret = null;
|
||||
}
|
||||
|
||||
// game images:
|
||||
// large image tooltip
|
||||
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
|
||||
presence.Assets.LargeImageText = string.Empty;
|
||||
else
|
||||
@@ -132,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);
|
||||
}
|
||||
|
||||
// update ruleset
|
||||
// small image
|
||||
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
|
||||
presence.Assets.SmallImageText = ruleset.Value.Name;
|
||||
|
||||
client.SetPresence(presence);
|
||||
}
|
||||
|
||||
private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() =>
|
||||
{
|
||||
game.Window?.Raise();
|
||||
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
login?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug);
|
||||
|
||||
// Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other.
|
||||
// Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion.
|
||||
if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password))
|
||||
{
|
||||
Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important);
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new GetRoomRequest(roomId);
|
||||
request.Success += room => Schedule(() =>
|
||||
{
|
||||
game.PresentMultiplayerMatch(room, password);
|
||||
});
|
||||
request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important);
|
||||
api.Queue(request);
|
||||
});
|
||||
|
||||
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
||||
|
||||
private string truncate(string str)
|
||||
private static string 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)
|
||||
return str;
|
||||
|
||||
@@ -160,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)
|
||||
{
|
||||
@@ -176,8 +341,20 @@ namespace osu.Desktop
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (multiplayerClient.IsNotNull())
|
||||
multiplayerClient.RoomUpdated -= onRoomUpdated;
|
||||
|
||||
client.Dispose();
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private class RoomSecret
|
||||
{
|
||||
[JsonProperty(@"roomId", Required = Required.Always)]
|
||||
public long RoomID { get; set; }
|
||||
|
||||
[JsonProperty(@"password", Required = Required.AllowNull)]
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal static class NVAPI
|
||||
{
|
||||
private const string osu_filename = "osu!.exe";
|
||||
@@ -487,6 +489,7 @@ namespace osu.Desktop
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum NvStatus
|
||||
{
|
||||
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.
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum NvSystemType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
@@ -616,6 +620,7 @@ namespace osu.Desktop
|
||||
DESKTOP = 2
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum NvGpuType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
@@ -623,6 +628,7 @@ namespace osu.Desktop
|
||||
DGPU = 2, // Discrete
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum NvSettingID : uint
|
||||
{
|
||||
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
|
||||
@@ -715,6 +721,7 @@ namespace osu.Desktop
|
||||
INVALID_SETTING_ID = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum NvShimSetting : uint
|
||||
{
|
||||
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
|
||||
@@ -729,6 +736,7 @@ namespace osu.Desktop
|
||||
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum NvThreadControlSetting : uint
|
||||
{
|
||||
OGL_THREAD_CONTROL_ENABLE = 0x00000001,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using osu.Desktop.Performance;
|
||||
using osu.Desktop.Security;
|
||||
@@ -22,7 +22,6 @@ using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Performance;
|
||||
using osu.Game.Utils;
|
||||
using SDL2;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
@@ -103,35 +102,13 @@ namespace osu.Desktop
|
||||
if (!string.IsNullOrEmpty(packageManaged))
|
||||
return new NoActionUpdateManager();
|
||||
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
case RuntimeInfo.Platform.Windows:
|
||||
Debug.Assert(OperatingSystem.IsWindows());
|
||||
|
||||
return new SquirrelUpdateManager();
|
||||
|
||||
default:
|
||||
return new SimpleUpdateManager();
|
||||
}
|
||||
return new VelopackUpdateManager();
|
||||
}
|
||||
|
||||
public override bool RestartAppWhenExited()
|
||||
{
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
case RuntimeInfo.Platform.Windows:
|
||||
Debug.Assert(OperatingSystem.IsWindows());
|
||||
|
||||
// Of note, this is an async method in squirrel that adds an arbitrary delay before returning
|
||||
// likely to ensure the external process is in a good state.
|
||||
//
|
||||
// We're not waiting on that here, but the outro playing before the actual exit should be enough
|
||||
// to cover this.
|
||||
Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RestartAppWhenExited();
|
||||
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -161,7 +138,7 @@ namespace osu.Desktop
|
||||
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)
|
||||
{
|
||||
@@ -169,23 +146,5 @@ namespace osu.Desktop
|
||||
osuSchemeLinkIPCChannel?.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Performance;
|
||||
@@ -11,16 +12,26 @@ namespace osu.Desktop.Performance
|
||||
{
|
||||
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
|
||||
{
|
||||
public bool IsSessionActive => activeSessions > 0;
|
||||
|
||||
private int activeSessions;
|
||||
|
||||
private GCLatencyMode originalGCMode;
|
||||
|
||||
public IDisposable BeginSession()
|
||||
{
|
||||
enableHighPerformanceSession();
|
||||
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.disableHighPerformanceSession());
|
||||
enterSession();
|
||||
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.exitSession());
|
||||
}
|
||||
|
||||
private void enableHighPerformanceSession()
|
||||
private void enterSession()
|
||||
{
|
||||
if (Interlocked.Increment(ref activeSessions) > 1)
|
||||
{
|
||||
Logger.Log($"High performance session requested ({activeSessions} running in total)");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log("Starting high performance session");
|
||||
|
||||
originalGCMode = GCSettings.LatencyMode;
|
||||
@@ -30,8 +41,14 @@ namespace osu.Desktop.Performance
|
||||
GC.Collect(0);
|
||||
}
|
||||
|
||||
private void disableHighPerformanceSession()
|
||||
private void exitSession()
|
||||
{
|
||||
if (Interlocked.Decrement(ref activeSessions) > 0)
|
||||
{
|
||||
Logger.Log($"High performance session finished ({activeSessions} others remain)");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log("Ending high performance session");
|
||||
|
||||
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework;
|
||||
@@ -13,8 +12,8 @@ using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Tournament;
|
||||
using SDL2;
|
||||
using Squirrel;
|
||||
using SDL;
|
||||
using Velopack;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
@@ -31,19 +30,11 @@ namespace osu.Desktop
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
/*
|
||||
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
||||
*
|
||||
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
||||
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
|
||||
* namely by checking loaded assemblies:
|
||||
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
|
||||
*
|
||||
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
|
||||
* the app will then do completely broken things like:
|
||||
* - not creating system shortcuts (as the logic is if'd out if "running tests")
|
||||
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
|
||||
*/
|
||||
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
||||
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
|
||||
// last time has been fixed, let's not tempt fate.
|
||||
setupVelopack();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var windowsVersion = Environment.OSVersion.Version;
|
||||
@@ -52,25 +43,27 @@ namespace osu.Desktop
|
||||
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
|
||||
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
|
||||
{
|
||||
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
|
||||
// disabling it ourselves.
|
||||
// We could also better detect compatibility mode if required:
|
||||
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
||||
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||
"Your operating system is too old to run osu!",
|
||||
"This version of osu! requires at least Windows 8.1 to run.\n"
|
||||
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"
|
||||
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
|
||||
return;
|
||||
unsafe
|
||||
{
|
||||
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
|
||||
// disabling it ourselves.
|
||||
// We could also better detect compatibility mode if required:
|
||||
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
||||
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||
"Your operating system is too old to run osu!"u8,
|
||||
"This version of osu! requires at least Windows 8.1 to run.\n"u8
|
||||
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
|
||||
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setupSquirrel();
|
||||
}
|
||||
|
||||
// NVIDIA profiles are based on the executable name of a process.
|
||||
// Lazer and stable share the same executable name.
|
||||
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
|
||||
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
if (OperatingSystem.IsWindows())
|
||||
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
@@ -103,7 +96,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)
|
||||
{
|
||||
@@ -167,32 +166,14 @@ namespace osu.Desktop
|
||||
return false;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void setupSquirrel()
|
||||
private static void setupVelopack()
|
||||
{
|
||||
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.InstallAssociations();
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UpdateAssociations();
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UninstallAssociations();
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
// causes the right-click context menu to function incorrectly.
|
||||
//
|
||||
// This may turn out to be non-required after an alternative solution is implemented.
|
||||
// see https://github.com/clowd/Clowd.Squirrel/issues/24
|
||||
// tools.SetProcessAppUserModelId();
|
||||
});
|
||||
VelopackApp
|
||||
.Build()
|
||||
.WithFirstRun(v =>
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations();
|
||||
}).Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -21,48 +20,14 @@ namespace osu.Desktop.Security
|
||||
[Resolved]
|
||||
private INotificationOverlay notifications { get; set; } = null!;
|
||||
|
||||
private bool elevated;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
elevated = checkElevated();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (elevated)
|
||||
if (Environment.IsPrivilegedProcess)
|
||||
notifications.Post(new ElevatedPrivilegesNotification());
|
||||
}
|
||||
|
||||
private bool checkElevated()
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
case RuntimeInfo.Platform.Windows:
|
||||
if (!OperatingSystem.IsWindows()) return false;
|
||||
|
||||
var windowsIdentity = WindowsIdentity.GetCurrent();
|
||||
var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
|
||||
|
||||
return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
|
||||
case RuntimeInfo.Platform.macOS:
|
||||
case RuntimeInfo.Platform.Linux:
|
||||
return Mono.Unix.Native.Syscall.geteuid() == 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
||||
{
|
||||
public override bool IsImportant => true;
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens.Play;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Squirrel.Sources;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||
|
||||
namespace osu.Desktop.Updater
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public partial class SquirrelUpdateManager : UpdateManager
|
||||
{
|
||||
private Squirrel.UpdateManager? updateManager;
|
||||
private INotificationOverlay notificationOverlay = null!;
|
||||
|
||||
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
|
||||
|
||||
private static readonly Logger logger = Logger.GetLogger("updater");
|
||||
|
||||
/// <summary>
|
||||
/// Whether an update has been downloaded but not yet applied.
|
||||
/// </summary>
|
||||
private bool updatePending;
|
||||
|
||||
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
notificationOverlay = notifications;
|
||||
|
||||
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
|
||||
}
|
||||
|
||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||
|
||||
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null)
|
||||
{
|
||||
// should we schedule a retry on completion of this check?
|
||||
bool scheduleRecheck = true;
|
||||
|
||||
const string? github_token = null; // TODO: populate.
|
||||
|
||||
try
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (localUserInfo?.IsPlaying.Value == true)
|
||||
return false;
|
||||
|
||||
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
|
||||
|
||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
|
||||
|
||||
if (info.ReleasesToApply.Count == 0)
|
||||
{
|
||||
if (updatePending)
|
||||
{
|
||||
// the user may have dismissed the completion notice, so show it again.
|
||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
restartToApplyUpdate();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// no updates available. bail and retry later.
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleRecheck = false;
|
||||
|
||||
if (notification == null)
|
||||
{
|
||||
notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = restartToApplyUpdate,
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
}
|
||||
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.StartInstall();
|
||||
|
||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
updatePending = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (useDeltaPatching)
|
||||
{
|
||||
logger.Add(@"delta patching failed; will attempt full download!");
|
||||
|
||||
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
|
||||
// try again without deltas.
|
||||
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
|
||||
scheduleRecheck = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scheduleRecheck)
|
||||
{
|
||||
// check again in 30 minutes.
|
||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool restartToApplyUpdate()
|
||||
{
|
||||
PrepareUpdateAsync()
|
||||
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
updateManager?.Dispose();
|
||||
}
|
||||
|
||||
private class SquirrelLogger : ILogger, IDisposable
|
||||
{
|
||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
||||
|
||||
public void Write(string message, LogLevel logLevel)
|
||||
{
|
||||
if (logLevel < Level)
|
||||
return;
|
||||
|
||||
logger.Add(message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens.Play;
|
||||
using Velopack;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace osu.Desktop.Updater
|
||||
{
|
||||
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
|
||||
{
|
||||
private readonly UpdateManager updateManager;
|
||||
private INotificationOverlay notificationOverlay = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
||||
|
||||
private UpdateInfo? pendingUpdate;
|
||||
|
||||
public VelopackUpdateManager()
|
||||
{
|
||||
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
|
||||
{
|
||||
AllowVersionDowngrade = true,
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
notificationOverlay = notifications;
|
||||
}
|
||||
|
||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||
|
||||
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null)
|
||||
{
|
||||
// should we schedule a retry on completion of this check?
|
||||
bool scheduleRecheck = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (localUserInfo?.IsPlaying.Value == true)
|
||||
return false;
|
||||
|
||||
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
||||
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
|
||||
if (pendingUpdate != null)
|
||||
{
|
||||
// If there is an update pending restart, show the notification to restart again.
|
||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
restartToApplyUpdate();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
|
||||
// Handle no updates available.
|
||||
if (pendingUpdate == null)
|
||||
return false;
|
||||
|
||||
scheduleRecheck = false;
|
||||
|
||||
if (notification == null)
|
||||
{
|
||||
notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = restartToApplyUpdate,
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
}
|
||||
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
|
||||
scheduleRecheck = true;
|
||||
Logger.Log($@"update check failed ({e.Message})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scheduleRecheck)
|
||||
{
|
||||
// check again in 30 minutes.
|
||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool restartToApplyUpdate()
|
||||
{
|
||||
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
|
||||
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
|
||||
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
|
||||
Schedule(() => game.AttemptExit());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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;
|
||||
@@ -163,6 +164,7 @@ namespace osu.Desktop.Windows
|
||||
[DllImport("Shell32.dll")]
|
||||
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private enum EventId
|
||||
{
|
||||
/// <summary>
|
||||
@@ -172,6 +174,7 @@ namespace osu.Desktop.Windows
|
||||
SHCNE_ASSOCCHANGED = 0x08000000
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private enum Flags : uint
|
||||
{
|
||||
SHCNF_IDLIST = 0x0000
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -23,10 +23,9 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("112643")]
|
||||
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||
[TestCase("high-speed-multiplier-precision")]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[new Droplet(), 0.01, true],
|
||||
[new TinyDroplet(), 0, false],
|
||||
[new Banana(), 0, false],
|
||||
[new BananaShower(), 0, false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
|
||||
@@ -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,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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneCatchEditorSaving : EditorSavingTestScene
|
||||
{
|
||||
protected override Ruleset CreateRuleset() => new CatchRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestCatchJuiceStreamTickCorrect()
|
||||
{
|
||||
AddStep("enter timing mode", () => InputManager.Key(Key.F3));
|
||||
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
|
||||
|
||||
Vector2 startPoint = Vector2.Zero;
|
||||
float increment = 0;
|
||||
|
||||
AddUntilStep("wait for playfield", () => this.ChildrenOfType<CatchPlayfield>().FirstOrDefault()?.IsLoaded == true);
|
||||
AddStep("move to centre", () =>
|
||||
{
|
||||
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
|
||||
startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3);
|
||||
increment = playfield.ScreenSpaceDrawQuad.Height / 10;
|
||||
InputManager.MoveMouseTo(startPoint);
|
||||
});
|
||||
AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3));
|
||||
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment)));
|
||||
AddStep("add node", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment)));
|
||||
AddStep("add node", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment)));
|
||||
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
|
||||
|
||||
int largeDropletCount = 0, tinyDropletCount = 0;
|
||||
AddStep("store droplet count", () =>
|
||||
{
|
||||
largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet));
|
||||
tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet));
|
||||
});
|
||||
|
||||
SaveEditor();
|
||||
ReloadEditorToSameBeatmap();
|
||||
|
||||
AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount));
|
||||
AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
AddMouseMoveStep(-100, 100);
|
||||
addVertexCheckStep(3, 1, times[0], positions[0]);
|
||||
addDragEndStep();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
|
||||
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
|
||||
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
|
||||
|
||||
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||
addDragEndStep();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
addDragEndStep();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
|
||||
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
|
||||
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
|
||||
addDragEndStep();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addAddVertexSteps(500, 150);
|
||||
addVertexCheckStep(3, 1, 500, 150);
|
||||
|
||||
addAddVertexSteps(90, 200);
|
||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||
addAddVertexSteps(160, 200);
|
||||
addVertexCheckStep(4, 1, 160, 200);
|
||||
|
||||
addAddVertexSteps(750, 180);
|
||||
addVertexCheckStep(5, 4, 750, 180);
|
||||
addVertexCheckStep(5, 4, 800, 160);
|
||||
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeleteVertex()
|
||||
{
|
||||
double[] times = { 100, 300, 500 };
|
||||
double[] times = { 100, 300, 400 };
|
||||
float[] positions = { 100, 200, 150 };
|
||||
addBlueprintStep(times, positions);
|
||||
|
||||
@@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddStep("delete vertex", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
StartTime = 5000,
|
||||
}
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
Breaks =
|
||||
{
|
||||
new BreakPeriod(2000, 4000),
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"Mappings":[{"StartTime":265568.0,"Objects":[{"StartTime":265568.0,"Position":486.0,"HyperDash":false},{"StartTime":265658.0,"Position":465.1873,"HyperDash":false},{"StartTime":265749.0,"Position":463.208435,"HyperDash":false},{"StartTime":265840.0,"Position":465.146484,"HyperDash":false},{"StartTime":265967.0,"Position":459.5862,"HyperDash":false}]}]}
|
||||
@@ -0,0 +1,238 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: audio.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 226943
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 2
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 1
|
||||
|
||||
[Editor]
|
||||
Bookmarks: 85568,86768,90968,265568
|
||||
DistanceSpacing: 0.9
|
||||
BeatDivisor: 12
|
||||
GridSize: 16
|
||||
TimelineZoom: 1
|
||||
|
||||
[Metadata]
|
||||
Title:Snow
|
||||
TitleUnicode:Snow
|
||||
Artist:Ricky Montgomery
|
||||
ArtistUnicode:Ricky Montgomery
|
||||
Creator:Crowley
|
||||
Version:Bury Me Six Feet in Snow
|
||||
Source:
|
||||
Tags:indie the honeysticks alternative english
|
||||
BeatmapID:2062131
|
||||
BeatmapSetID:971028
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:6
|
||||
CircleSize:4.2
|
||||
OverallDifficulty:8.3
|
||||
ApproachRate:8.3
|
||||
SliderMultiplier:3.59999990463257
|
||||
SliderTickRate:1
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"me.jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Layer 4 (Overlay)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
368,1200,2,2,1,30,1,0
|
||||
368,-66.6666666666667,2,2,1,30,0,0
|
||||
29168,-58.8235294117647,2,2,1,40,0,0
|
||||
30368,-58.8235294117647,2,2,2,40,0,0
|
||||
30568,-58.8235294117647,2,2,1,40,0,0
|
||||
31368,-58.8235294117647,2,2,2,40,0,0
|
||||
31568,-58.8235294117647,2,2,1,40,0,0
|
||||
32768,-58.8235294117647,2,2,2,40,0,0
|
||||
33568,-58.8235294117647,2,2,2,40,0,0
|
||||
33968,-58.8235294117647,2,2,1,40,0,0
|
||||
35168,-58.8235294117647,2,2,2,40,0,0
|
||||
35968,-58.8235294117647,2,2,1,40,0,0
|
||||
36168,-58.8235294117647,2,2,2,40,0,0
|
||||
36368,-58.8235294117647,2,2,1,40,0,0
|
||||
37568,-58.8235294117647,2,2,2,40,0,0
|
||||
37968,-58.8235294117647,2,2,1,40,0,0
|
||||
38368,-58.8235294117647,2,2,2,40,0,0
|
||||
38768,-58.8235294117647,2,2,1,40,0,0
|
||||
39968,-58.8235294117647,2,2,2,40,0,0
|
||||
40168,-58.8235294117647,2,2,1,40,0,0
|
||||
40968,-58.8235294117647,2,2,2,40,0,0
|
||||
41168,-58.8235294117647,2,2,1,40,0,0
|
||||
42368,-58.8235294117647,2,2,2,40,0,0
|
||||
43168,-58.8235294117647,2,2,2,40,0,0
|
||||
43568,-58.8235294117647,2,2,1,40,0,0
|
||||
44768,-58.8235294117647,2,2,2,40,0,0
|
||||
45768,-58.8235294117647,2,2,2,40,0,0
|
||||
45968,-58.8235294117647,2,2,1,50,0,0
|
||||
47168,-58.8235294117647,2,2,2,50,0,0
|
||||
48368,-62.5,2,2,1,50,0,0
|
||||
67568,-58.8235294117647,2,2,1,70,0,1
|
||||
84668,-58.8235294117647,2,2,1,5,0,1
|
||||
84768,-58.8235294117647,2,2,1,70,0,1
|
||||
85068,-58.8235294117647,2,2,1,5,0,1
|
||||
85168,-58.8235294117647,2,2,1,70,0,1
|
||||
85468,-58.8235294117647,2,2,1,5,0,1
|
||||
85568,-58.8235294117647,2,2,1,70,0,1
|
||||
86768,-58.8235294117647,2,2,1,30,0,0
|
||||
91168,-58.8235294117647,2,2,1,50,0,0
|
||||
91568,1200,2,2,1,50,1,0
|
||||
91568,-58.8235294117647,2,2,1,50,0,1
|
||||
91643,-58.8235294117647,2,2,1,50,0,0
|
||||
92768,-58.8235294117647,2,2,2,50,0,0
|
||||
92968,-58.8235294117647,2,2,1,50,0,0
|
||||
95168,-58.8235294117647,2,2,2,50,0,0
|
||||
95368,-58.8235294117647,2,2,1,50,0,0
|
||||
97568,-58.8235294117647,2,2,2,50,0,0
|
||||
97768,-58.8235294117647,2,2,1,50,0,0
|
||||
99968,-58.8235294117647,2,2,2,50,0,0
|
||||
100168,-58.8235294117647,2,2,1,50,0,0
|
||||
100768,-58.8235294117647,2,2,2,50,0,0
|
||||
101168,-58.8235294117647,2,2,1,50,0,0
|
||||
102368,-58.8235294117647,2,2,2,50,0,0
|
||||
102568,-58.8235294117647,2,2,1,50,0,0
|
||||
104768,-58.8235294117647,2,2,2,50,0,0
|
||||
104968,-58.8235294117647,2,2,1,50,0,0
|
||||
107168,-58.8235294117647,2,2,2,50,0,0
|
||||
107368,-58.8235294117647,2,2,1,50,0,0
|
||||
108968,-58.8235294117647,2,2,2,50,0,0
|
||||
109168,-58.8235294117647,2,2,1,50,0,0
|
||||
109568,-58.8235294117647,2,2,2,50,0,0
|
||||
109968,-58.8235294117647,2,2,1,50,0,0
|
||||
110368,-58.8235294117647,2,2,2,50,0,0
|
||||
110768,-100,2,2,1,40,0,0
|
||||
127568,-62.5,2,2,2,50,0,0
|
||||
127968,-62.5,2,2,1,50,0,0
|
||||
128168,-62.5,2,2,2,50,0,0
|
||||
129968,-58.8235294117647,2,2,1,50,0,0
|
||||
131168,-58.8235294117647,2,2,2,50,0,0
|
||||
131368,-58.8235294117647,2,2,1,50,0,0
|
||||
133568,-58.8235294117647,2,2,2,50,0,0
|
||||
133768,-58.8235294117647,2,2,1,50,0,0
|
||||
135968,-58.8235294117647,2,2,2,50,0,0
|
||||
136168,-58.8235294117647,2,2,1,50,0,0
|
||||
138368,-58.8235294117647,2,2,2,50,0,0
|
||||
138568,-58.8235294117647,2,2,1,50,0,0
|
||||
139168,-58.8235294117647,2,2,2,50,0,0
|
||||
139368,-58.8235294117647,2,2,1,50,0,0
|
||||
139568,-58.8235294117647,2,2,1,50,0,0
|
||||
140768,-58.8235294117647,2,2,2,50,0,0
|
||||
140968,-58.8235294117647,2,2,1,50,0,0
|
||||
143168,-58.8235294117647,2,2,2,50,0,0
|
||||
143368,-58.8235294117647,2,2,1,50,0,0
|
||||
145568,-58.8235294117647,2,2,2,50,0,0
|
||||
145768,-58.8235294117647,2,2,1,50,0,0
|
||||
147368,-58.8235294117647,2,2,2,50,0,0
|
||||
147768,-58.8235294117647,2,2,1,50,0,0
|
||||
147968,-58.8235294117647,2,2,1,60,0,0
|
||||
148768,-58.8235294117647,2,2,2,60,0,0
|
||||
149168,-58.8235294117647,2,2,1,70,0,1
|
||||
158268,-58.8235294117647,2,2,2,70,0,1
|
||||
158568,-58.8235294117647,2,2,1,70,0,1
|
||||
166268,-58.8235294117647,2,2,1,5,0,1
|
||||
166368,-58.8235294117647,2,2,1,70,0,1
|
||||
166668,-58.8235294117647,2,2,1,5,0,1
|
||||
166768,-58.8235294117647,2,2,1,70,0,1
|
||||
167068,-58.8235294117647,2,2,1,5,0,1
|
||||
167168,-58.8235294117647,2,2,1,70,0,1
|
||||
168368,-62.5,2,2,1,50,0,0
|
||||
172368,-62.5,2,2,1,50,0,1
|
||||
173168,-62.5,2,2,1,50,0,0
|
||||
185168,-62.5,2,2,1,60,0,0
|
||||
185468,-62.5,2,2,1,5,0,0
|
||||
185568,-62.5,2,2,1,60,0,0
|
||||
185868,-62.5,2,2,1,5,0,0
|
||||
185968,-62.5,2,2,1,60,0,0
|
||||
186268,-62.5,2,2,1,5,0,0
|
||||
186368,-62.5,2,2,1,60,0,0
|
||||
186668,-62.5,2,2,1,5,0,0
|
||||
186768,-52.6315789473684,2,2,1,60,0,0
|
||||
187068,-62.5,2,2,1,5,0,0
|
||||
187168,-62.5,2,2,1,60,0,0
|
||||
187468,-62.5,2,2,1,5,0,0
|
||||
187568,-62.5,2,2,1,20,0,0
|
||||
187768,-62.5,2,2,1,24,0,0
|
||||
187968,-62.5,2,2,1,28,0,0
|
||||
188168,-62.5,2,2,1,32,0,0
|
||||
188368,-62.5,2,2,1,36,0,0
|
||||
188568,-62.5,2,2,1,40,0,0
|
||||
188768,1200,2,2,1,50,1,1
|
||||
188768,-58.8235294117647,2,2,1,50,0,1
|
||||
188843,-58.8235294117647,2,2,1,50,0,0
|
||||
189968,-58.8235294117647,2,2,2,50,0,0
|
||||
190168,-58.8235294117647,2,2,1,50,0,0
|
||||
192368,-58.8235294117647,2,2,2,50,0,0
|
||||
192568,-58.8235294117647,2,2,1,50,0,0
|
||||
194768,-58.8235294117647,2,2,2,50,0,0
|
||||
194968,-58.8235294117647,2,2,1,50,0,0
|
||||
196568,-58.8235294117647,2,2,2,50,0,0
|
||||
196768,-58.8235294117647,2,2,1,50,0,0
|
||||
197168,-58.8235294117647,2,2,2,50,0,0
|
||||
197368,-58.8235294117647,2,2,1,50,0,0
|
||||
197568,-58.8235294117647,2,2,2,50,0,0
|
||||
197968,-58.8235294117647,2,2,1,50,0,0
|
||||
198368,-58.8235294117647,2,2,1,50,0,0
|
||||
199568,-58.8235294117647,2,2,2,50,0,0
|
||||
199768,-58.8235294117647,2,2,1,50,0,0
|
||||
201968,-58.8235294117647,2,2,2,50,0,0
|
||||
202168,-58.8235294117647,2,2,1,50,0,0
|
||||
204368,-58.8235294117647,2,2,2,50,0,0
|
||||
204568,-58.8235294117647,2,2,1,50,0,0
|
||||
206768,-58.8235294117647,2,2,1,60,0,0
|
||||
207168,-58.8235294117647,2,2,2,60,0,0
|
||||
207968,-58.8235294117647,2,2,1,70,0,1
|
||||
216968,-58.8235294117647,2,2,2,70,0,1
|
||||
217168,-58.8235294117647,2,2,1,70,0,1
|
||||
217368,-58.8235294117647,2,2,2,70,0,1
|
||||
217568,-58.8235294117647,2,2,1,70,0,1
|
||||
225068,-58.8235294117647,2,2,1,5,0,1
|
||||
225168,-58.8235294117647,2,2,1,70,0,1
|
||||
225468,-58.8235294117647,2,2,1,5,0,1
|
||||
225568,-58.8235294117647,2,2,1,70,0,1
|
||||
225868,-58.8235294117647,2,2,1,5,0,1
|
||||
225968,-58.8235294117647,2,2,1,70,0,1
|
||||
227168,-58.8235294117647,2,2,1,30,0,0
|
||||
234368,-58.8235294117647,2,2,1,40,0,0
|
||||
236768,-58.8235294117647,2,2,1,70,0,1
|
||||
255968,-58.8235294117647,2,2,1,70,0,1
|
||||
261168,-58.8235294117647,2,2,1,70,0,1
|
||||
263068,-58.8235294117647,2,2,1,70,0,0
|
||||
263168,-58.8235294117647,2,2,1,60,0,1
|
||||
263243,-58.8235294117647,2,2,1,60,0,0
|
||||
264368,-58.8235294117647,2,2,1,60,0,1
|
||||
264443,-58.8235294117647,2,2,1,60,0,0
|
||||
265568,-444.444444444444,2,2,1,50,0,1
|
||||
265643,-444.444444444444,2,2,1,50,0,0
|
||||
266768,-444.444444444444,2,2,1,40,0,0
|
||||
267968,-444.444444444444,2,2,1,30,0,0
|
||||
269168,-444.444444444444,2,2,1,20,0,0
|
||||
270368,-444.444444444444,2,2,1,10,0,0
|
||||
271168,-444.444444444444,2,2,1,9,0,0
|
||||
271568,-444.444444444444,2,2,1,8,0,0
|
||||
271968,-444.444444444444,2,2,1,7,0,0
|
||||
272368,-444.444444444444,2,2,1,6,0,0
|
||||
272768,-444.444444444444,2,2,1,5,0,0
|
||||
275168,-444.444444444444,2,2,1,5,0,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1 : 255,128,128
|
||||
Combo2 : 72,72,255
|
||||
Combo3 : 192,192,192
|
||||
Combo4 : 255,136,79
|
||||
|
||||
[HitObjects]
|
||||
486,179,265568,6,0,P|461:174|454:174,1,26.999997997284,6|0,1:2|0:0,0:0:0:0:
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Skinning;
|
||||
@@ -19,20 +18,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
|
||||
public void TestLegacyHUDComboCounterNotExistent([Values] bool withModifiedSkin)
|
||||
{
|
||||
if (withModifiedSkin)
|
||||
{
|
||||
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinnableContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("exit player", () => Player.Exit());
|
||||
CreateTest();
|
||||
}
|
||||
|
||||
AddAssert("legacy HUD combo counter hidden", () =>
|
||||
{
|
||||
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
|
||||
});
|
||||
AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyDefaultComboCounter>().Any());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestReplayDetach()
|
||||
{
|
||||
DrawableCatchRuleset drawableRuleset = null!;
|
||||
float catcherPosition = 0;
|
||||
|
||||
AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), []));
|
||||
AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score()));
|
||||
AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType<Catcher>().Single().X);
|
||||
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
|
||||
AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(catcherPosition));
|
||||
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
|
||||
|
||||
AddStep("detach replay", () => drawableRuleset.SetReplayScore(null));
|
||||
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
|
||||
AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.Not.EqualTo(catcherPosition));
|
||||
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
|
||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
|
||||
AddAssert("correct hit lighting colour",
|
||||
() => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -259,6 +260,16 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllExplodedObjectsAtUniquePositions()
|
||||
{
|
||||
AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
|
||||
AddStep("catch normal fruit", () => attemptCatch(new Fruit { IndexInBeatmap = 2, LastInCombo = true }));
|
||||
AddAssert("two fruit at distinct x coordinates",
|
||||
() => this.ChildrenOfType<CaughtFruit>().Select(f => f.DrawPosition.X).Distinct(),
|
||||
() => Has.Exactly(2).Items);
|
||||
}
|
||||
|
||||
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
|
||||
|
||||
private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1 : ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity,
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@@ -29,6 +28,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@@ -62,43 +62,43 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
||||
{
|
||||
if (mods.HasFlagFast(LegacyMods.Nightcore))
|
||||
if (mods.HasFlag(LegacyMods.Nightcore))
|
||||
yield return new CatchModNightcore();
|
||||
else if (mods.HasFlagFast(LegacyMods.DoubleTime))
|
||||
else if (mods.HasFlag(LegacyMods.DoubleTime))
|
||||
yield return new CatchModDoubleTime();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Perfect))
|
||||
if (mods.HasFlag(LegacyMods.Perfect))
|
||||
yield return new CatchModPerfect();
|
||||
else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
|
||||
else if (mods.HasFlag(LegacyMods.SuddenDeath))
|
||||
yield return new CatchModSuddenDeath();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Cinema))
|
||||
if (mods.HasFlag(LegacyMods.Cinema))
|
||||
yield return new CatchModCinema();
|
||||
else if (mods.HasFlagFast(LegacyMods.Autoplay))
|
||||
else if (mods.HasFlag(LegacyMods.Autoplay))
|
||||
yield return new CatchModAutoplay();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Easy))
|
||||
if (mods.HasFlag(LegacyMods.Easy))
|
||||
yield return new CatchModEasy();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Flashlight))
|
||||
if (mods.HasFlag(LegacyMods.Flashlight))
|
||||
yield return new CatchModFlashlight();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.HalfTime))
|
||||
if (mods.HasFlag(LegacyMods.HalfTime))
|
||||
yield return new CatchModHalfTime();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.HardRock))
|
||||
if (mods.HasFlag(LegacyMods.HardRock))
|
||||
yield return new CatchModHardRock();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Hidden))
|
||||
if (mods.HasFlag(LegacyMods.Hidden))
|
||||
yield return new CatchModHidden();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.NoFail))
|
||||
if (mods.HasFlag(LegacyMods.NoFail))
|
||||
yield return new CatchModNoFail();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||
if (mods.HasFlag(LegacyMods.Relax))
|
||||
yield return new CatchModRelax();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
if (mods.HasFlag(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
@@ -223,6 +223,12 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
[
|
||||
new DifficultySection(),
|
||||
new ColoursSection(),
|
||||
];
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||
|
||||
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||
@@ -248,5 +254,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
public override bool EditorShowScrollSpeed => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,11 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
public class CatchSkinComponentLookup : GameplaySkinComponentLookup<CatchSkinComponents>
|
||||
public class CatchSkinComponentLookup : SkinComponentLookup<CatchSkinComponents>
|
||||
{
|
||||
public CatchSkinComponentLookup(CatchSkinComponents component)
|
||||
: base(component)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
|
||||
|
||||
protected override string ComponentName => Component.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
value *= Math.Pow(accuracy(), 5.5);
|
||||
|
||||
if (score.Mods.Any(m => m is ModNoFail))
|
||||
value *= 0.90;
|
||||
value *= Math.Max(0.90, 1.0 - 0.02 * numMiss);
|
||||
|
||||
return new CatchPerformanceAttributes
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
@@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[Resolved]
|
||||
private IBeatSnapProvider? beatSnapProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap? EditorBeatmap { get; private set; }
|
||||
|
||||
protected EditablePath(Func<float, double> positionToTime)
|
||||
{
|
||||
PositionToTime = positionToTime;
|
||||
@@ -103,15 +107,23 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
//
|
||||
// The value is clamped here by the bindable min and max values.
|
||||
// In case the required velocity is too large, the path is not preserved.
|
||||
double previousVelocity = svBindable.Value;
|
||||
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
||||
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
||||
// adjust velocity locally, so that once the SV change is applied by applying defaults
|
||||
// (triggered by `EditorBeatmap.Update()` call at end of method),
|
||||
// it results in the outcome desired by the user.
|
||||
double relativeChange = svBindable.Value / previousVelocity;
|
||||
double localVelocity = hitObject.Velocity * relativeChange;
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity);
|
||||
|
||||
if (beatSnapProvider == null) return;
|
||||
|
||||
double endTime = hitObject.StartTime + path.Duration;
|
||||
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity;
|
||||
|
||||
EditorBeatmap?.Update(hitObject);
|
||||
}
|
||||
|
||||
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -19,22 +18,27 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
||||
|
||||
private readonly JuiceStream juiceStream;
|
||||
|
||||
// To handle when the editor is scrolled while dragging.
|
||||
private Vector2 dragStartPosition;
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||
public SelectionEditablePath(JuiceStream juiceStream, Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
this.juiceStream = juiceStream;
|
||||
}
|
||||
|
||||
public void AddVertex(Vector2 relativePosition)
|
||||
{
|
||||
EditorBeatmap?.BeginChange();
|
||||
|
||||
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||
int index = AddVertex(time, relativePosition.X);
|
||||
UpdateHitObjectFromPath(juiceStream);
|
||||
selectOnly(index);
|
||||
|
||||
EditorBeatmap?.EndChange();
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
|
||||
@@ -45,9 +49,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
if (index == -1 || VertexStates[index].IsFixed)
|
||||
return false;
|
||||
|
||||
if (e.Button == MouseButton.Left && e.ShiftPressed)
|
||||
if (e.Button == MouseButton.Right && e.ShiftPressed)
|
||||
{
|
||||
EditorBeatmap?.BeginChange();
|
||||
RemoveVertex(index);
|
||||
UpdateHitObjectFromPath(juiceStream);
|
||||
EditorBeatmap?.EndChange();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -74,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
VertexStates[i].VertexBeforeChange = Vertices[i];
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
EditorBeatmap?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -88,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
changeHandler?.EndChange();
|
||||
EditorBeatmap?.EndChange();
|
||||
}
|
||||
|
||||
private int getMouseTargetVertex(Vector2 screenSpacePosition)
|
||||
@@ -118,11 +126,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
private void deleteSelectedVertices()
|
||||
{
|
||||
EditorBeatmap?.BeginChange();
|
||||
|
||||
for (int i = VertexCount - 1; i >= 0; i--)
|
||||
{
|
||||
if (VertexStates[i].IsSelected)
|
||||
RemoveVertex(i);
|
||||
}
|
||||
|
||||
UpdateHitObjectFromPath(juiceStream);
|
||||
|
||||
EditorBeatmap?.EndChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
@@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
public partial class VertexPiece : Circle
|
||||
{
|
||||
private VertexState state = new VertexState();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour osuColour { get; set; } = null!;
|
||||
|
||||
@@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdateFrom(VertexState state)
|
||||
{
|
||||
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
|
||||
this.state = state;
|
||||
updateMarkerDisplay();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateMarkerDisplay();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateMarkerDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
private void updateMarkerDisplay()
|
||||
{
|
||||
var colour = osuColour.Yellow;
|
||||
|
||||
if (IsHovered || state.IsSelected)
|
||||
colour = colour.Lighten(1);
|
||||
|
||||
Colour = colour;
|
||||
Alpha = state.IsFixed ? 0.5f : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
inputManager = GetContainingInputManager()!;
|
||||
|
||||
BeginPlacement();
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new SelectionEditablePath(positionToTime)
|
||||
editablePath = new SelectionEditablePath(hitObject, positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckBananaShowerGap()
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckCatchAbnormalDifficultySettings(),
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
|
||||
@@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
|
||||
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (snapType.HasFlagFast(SnapType.RelativeGrids))
|
||||
if (snapType.HasFlag(SnapType.RelativeGrids))
|
||||
{
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
|
||||
@@ -76,21 +76,38 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override bool HandleReverse()
|
||||
{
|
||||
var hitObjects = EditorBeatmap.SelectedHitObjects
|
||||
.OfType<CatchHitObject>()
|
||||
.OrderBy(obj => obj.StartTime)
|
||||
.ToList();
|
||||
|
||||
double selectionStartTime = SelectedItems.Min(h => h.StartTime);
|
||||
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
|
||||
|
||||
EditorBeatmap.PerformOnSelection(hitObject =>
|
||||
{
|
||||
hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
|
||||
// the expectation is that even if the objects themselves are reversed temporally,
|
||||
// the position of new combos in the selection should remain the same.
|
||||
// preserve it for later before doing the reversal.
|
||||
var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
|
||||
|
||||
if (hitObject is JuiceStream juiceStream)
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime);
|
||||
|
||||
if (h is JuiceStream juiceStream)
|
||||
{
|
||||
juiceStream.Path.Reverse(out Vector2 positionalOffset);
|
||||
juiceStream.OriginalX += positionalOffset.X;
|
||||
juiceStream.LegacyConvertedY += positionalOffset.Y;
|
||||
EditorBeatmap.Update(juiceStream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// re-order objects by start time again after reversing, and restore new combo flag positioning
|
||||
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
|
||||
|
||||
for (int i = 0; i < hitObjects.Count; ++i)
|
||||
hitObjects[i].NewCombo = newComboOrder[i];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySettings
|
||||
{
|
||||
public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks catch relevant settings");
|
||||
|
||||
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var diff = context.Beatmap.Difficulty;
|
||||
Issue? issue;
|
||||
|
||||
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
|
||||
yield return issue;
|
||||
|
||||
if (OutOfRange("Approach rate", diff.ApproachRate, out issue))
|
||||
yield return issue;
|
||||
|
||||
if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue))
|
||||
yield return issue;
|
||||
|
||||
if (OutOfRange("Circle size", diff.CircleSize, out issue))
|
||||
yield return issue;
|
||||
|
||||
if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue))
|
||||
yield return issue;
|
||||
|
||||
if (OutOfRange("Drain rate", diff.DrainRate, out issue))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
|
||||
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
|
||||
{
|
||||
public const float OBJECT_RADIUS = 64;
|
||||
|
||||
|
||||
@@ -21,11 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
|
||||
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
|
||||
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
|
||||
|
||||
public Vector2 DisplayPosition => DrawPosition;
|
||||
public Vector2 DisplaySize => Size * Scale;
|
||||
|
||||
public float DisplayRotation => Rotation;
|
||||
|
||||
public double DisplayStartTime => HitObject.StartTime;
|
||||
|
||||
/// <summary>
|
||||
@@ -44,19 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the hit object visual state from another <see cref="IHasCatchObjectState"/> object.
|
||||
/// </summary>
|
||||
public virtual void CopyStateFrom(IHasCatchObjectState objectState)
|
||||
{
|
||||
HitObject = objectState.HitObject;
|
||||
Scale = Vector2.Divide(objectState.DisplaySize, Size);
|
||||
Rotation = objectState.DisplayRotation;
|
||||
AccentColour.Value = objectState.AccentColour.Value;
|
||||
HyperDash.Value = objectState.HyperDash.Value;
|
||||
IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
ClearTransforms();
|
||||
@@ -64,5 +49,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
|
||||
base.FreeAfterUse();
|
||||
}
|
||||
|
||||
public void RestoreState(CatchObjectState state)
|
||||
{
|
||||
HitObject = state.HitObject;
|
||||
AccentColour.Value = state.AccentColour;
|
||||
HyperDash.Value = state.HyperDash;
|
||||
IndexInBeatmap.Value = state.IndexInBeatmap;
|
||||
Position = state.DisplayPosition;
|
||||
Scale = Vector2.Divide(state.DisplaySize, Size);
|
||||
Rotation = state.DisplayRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
/// </summary>
|
||||
protected readonly Container ScalingContainer;
|
||||
|
||||
public Vector2 DisplayPosition => DrawPosition;
|
||||
|
||||
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
|
||||
|
||||
public float DisplayRotation => ScalingContainer.Rotation;
|
||||
@@ -95,5 +97,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
|
||||
base.OnFree();
|
||||
}
|
||||
|
||||
public void RestoreState(CatchObjectState state) => throw new NotSupportedException("Cannot restore state into a drawable catch hitobject.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
public interface IHasCatchObjectState
|
||||
{
|
||||
PalpableCatchHitObject HitObject { get; }
|
||||
|
||||
double DisplayStartTime { get; }
|
||||
|
||||
Bindable<Color4> AccentColour { get; }
|
||||
|
||||
Bindable<bool> HyperDash { get; }
|
||||
|
||||
Bindable<int> IndexInBeatmap { get; }
|
||||
|
||||
double DisplayStartTime { get; }
|
||||
Vector2 DisplayPosition { get; }
|
||||
Vector2 DisplaySize { get; }
|
||||
|
||||
float DisplayRotation { get; }
|
||||
|
||||
void RestoreState(CatchObjectState state);
|
||||
}
|
||||
|
||||
public static class HasCatchObjectStateExtensions
|
||||
{
|
||||
public static CatchObjectState SaveState(this IHasCatchObjectState target) => new CatchObjectState(
|
||||
target.HitObject,
|
||||
target.AccentColour.Value,
|
||||
target.HyperDash.Value,
|
||||
target.IndexInBeatmap.Value,
|
||||
target.DisplayPosition,
|
||||
target.DisplaySize,
|
||||
target.DisplayRotation);
|
||||
}
|
||||
|
||||
public readonly record struct CatchObjectState(
|
||||
PalpableCatchHitObject HitObject,
|
||||
Color4 AccentColour,
|
||||
bool HyperDash,
|
||||
int IndexInBeatmap,
|
||||
Vector2 DisplayPosition,
|
||||
Vector2 DisplaySize,
|
||||
float DisplayRotation);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects
|
||||
@@ -29,7 +30,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
@@ -47,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public double TickDistanceMultiplier = 1;
|
||||
|
||||
[JsonIgnore]
|
||||
private double velocityFactor;
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
|
||||
public double TickDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||
@@ -69,14 +63,21 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
|
||||
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||
Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
|
||||
|
||||
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
|
||||
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
|
||||
double scoringDistance = Velocity * timingPoint.BeatLength;
|
||||
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
this.PopulateNodeSamples();
|
||||
|
||||
var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
|
||||
|
||||
int nodeIndex = 0;
|
||||
|
||||
@@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
{
|
||||
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
|
||||
|
||||
private readonly float halfCatcherWidth;
|
||||
|
||||
public CatchAutoGenerator(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||
}
|
||||
|
||||
protected override void GenerateFrames()
|
||||
@@ -47,10 +50,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
|
||||
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
|
||||
|
||||
// todo: get correct catcher size, based on difficulty CS.
|
||||
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
|
||||
|
||||
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
|
||||
if (lastPosition - halfCatcherWidth < h.EffectiveX && lastPosition + halfCatcherWidth > h.EffectiveX)
|
||||
{
|
||||
// we are already in the correct range.
|
||||
lastTime = h.StartTime;
|
||||
|
||||
@@ -32,6 +32,10 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
if (result.Type == HitResult.SmallTickMiss)
|
||||
return false;
|
||||
|
||||
// on stable, banana showers don't exist as concrete objects themselves, so they can't cause a fail.
|
||||
if (result.HitObject is BananaShower)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
@@ -28,76 +28,94 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
if (lookup is SkinComponentsContainerLookup containerLookup)
|
||||
switch (lookup)
|
||||
{
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
var components = base.GetDrawableComponent(lookup) as Container;
|
||||
|
||||
if (providesComboCounter && components != null)
|
||||
{
|
||||
// catch may provide its own combo counter; hide the default.
|
||||
// todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
|
||||
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>())
|
||||
legacyComboCounter.HiddenByRulesetImplementation = false;
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
}
|
||||
|
||||
if (lookup is CatchSkinComponentLookup catchSkinComponent)
|
||||
{
|
||||
switch (catchSkinComponent.Component)
|
||||
{
|
||||
case CatchSkinComponents.Fruit:
|
||||
if (hasPear)
|
||||
return new LegacyFruitPiece();
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||
if (!IsProvidingLegacyResources)
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.Banana:
|
||||
if (GetTexture("fruit-bananas") != null)
|
||||
return new LegacyBananaPiece();
|
||||
// Our own ruleset components default.
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
|
||||
return null;
|
||||
if (keyCounter != null)
|
||||
{
|
||||
// set the anchor to top right so that it won't squash to the return button to the top
|
||||
keyCounter.Anchor = Anchor.CentreRight;
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LegacyKeyCounterDisplay(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case CatchSkinComponents.Droplet:
|
||||
if (GetTexture("fruit-drop") != null)
|
||||
return new LegacyDropletPiece();
|
||||
return null;
|
||||
|
||||
return null;
|
||||
case CatchSkinComponentLookup catchSkinComponent:
|
||||
switch (catchSkinComponent.Component)
|
||||
{
|
||||
case CatchSkinComponents.Fruit:
|
||||
if (hasPear)
|
||||
return new LegacyFruitPiece();
|
||||
|
||||
case CatchSkinComponents.Catcher:
|
||||
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
return null;
|
||||
|
||||
if (version < 2.3m)
|
||||
{
|
||||
if (hasOldStyleCatcherSprite())
|
||||
return new LegacyCatcherOld();
|
||||
}
|
||||
case CatchSkinComponents.Banana:
|
||||
if (GetTexture("fruit-bananas") != null)
|
||||
return new LegacyBananaPiece();
|
||||
|
||||
if (hasNewStyleCatcherSprite())
|
||||
return new LegacyCatcherNew();
|
||||
return null;
|
||||
|
||||
return null;
|
||||
case CatchSkinComponents.Droplet:
|
||||
if (GetTexture("fruit-drop") != null)
|
||||
return new LegacyDropletPiece();
|
||||
|
||||
case CatchSkinComponents.CatchComboCounter:
|
||||
if (providesComboCounter)
|
||||
return new LegacyCatchComboCounter();
|
||||
return null;
|
||||
|
||||
return null;
|
||||
case CatchSkinComponents.Catcher:
|
||||
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
|
||||
case CatchSkinComponents.HitExplosion:
|
||||
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
|
||||
return new LegacyHitExplosion();
|
||||
if (version < 2.3m)
|
||||
{
|
||||
if (hasOldStyleCatcherSprite())
|
||||
return new LegacyCatcherOld();
|
||||
}
|
||||
|
||||
return null;
|
||||
if (hasNewStyleCatcherSprite())
|
||||
return new LegacyCatcherNew();
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
}
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.CatchComboCounter:
|
||||
if (providesComboCounter)
|
||||
return new LegacyCatchComboCounter();
|
||||
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.HitExplosion:
|
||||
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
|
||||
return new LegacyHitExplosion();
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
@@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
protected void SetTexture(Texture? texture, Texture? overlayTexture)
|
||||
{
|
||||
colouredSprite.Texture = texture;
|
||||
overlaySprite.Texture = overlayTexture;
|
||||
hyperSprite.Texture = texture;
|
||||
// Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture.
|
||||
|
||||
if (colouredSprite.Texture != texture)
|
||||
{
|
||||
colouredSprite.Size = Vector2.Zero;
|
||||
colouredSprite.Texture = texture;
|
||||
}
|
||||
|
||||
if (overlaySprite.Texture != overlayTexture)
|
||||
{
|
||||
overlaySprite.Size = Vector2.Zero;
|
||||
overlaySprite.Texture = overlayTexture;
|
||||
}
|
||||
|
||||
if (hyperSprite.Texture != texture)
|
||||
{
|
||||
hyperSprite.Size = Vector2.Zero;
|
||||
hyperSprite.Texture = texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -362,7 +363,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (caughtObject == null) return;
|
||||
|
||||
caughtObject.CopyStateFrom(drawableObject);
|
||||
caughtObject.RestoreState(drawableObject.SaveState());
|
||||
caughtObject.Anchor = Anchor.TopCentre;
|
||||
caughtObject.Position = position;
|
||||
caughtObject.Scale *= caught_fruit_scale_adjust;
|
||||
@@ -411,41 +412,50 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
}
|
||||
|
||||
private CaughtObject getDroppedObject(CaughtObject caughtObject)
|
||||
private CaughtObject getDroppedObject(CatchObjectState state)
|
||||
{
|
||||
var droppedObject = getCaughtObject(caughtObject.HitObject);
|
||||
var droppedObject = getCaughtObject(state.HitObject);
|
||||
Debug.Assert(droppedObject != null);
|
||||
|
||||
droppedObject.CopyStateFrom(caughtObject);
|
||||
droppedObject.RestoreState(state);
|
||||
droppedObject.Anchor = Anchor.TopLeft;
|
||||
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget);
|
||||
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(state.DisplayPosition, droppedObjectTarget);
|
||||
|
||||
return droppedObject;
|
||||
}
|
||||
|
||||
private void clearPlate(DroppedObjectAnimation animation)
|
||||
{
|
||||
var caughtObjects = caughtObjectContainer.Children.ToArray();
|
||||
int caughtCount = caughtObjectContainer.Children.Count;
|
||||
CatchObjectState[] states = ArrayPool<CatchObjectState>.Shared.Rent(caughtCount);
|
||||
|
||||
caughtObjectContainer.Clear(false);
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < caughtCount; i++)
|
||||
states[i] = caughtObjectContainer.Children[i].SaveState();
|
||||
|
||||
// Use the already returned PoolableDrawables for new objects
|
||||
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
|
||||
caughtObjectContainer.Clear(false);
|
||||
|
||||
droppedObjectTarget.AddRange(droppedObjects);
|
||||
|
||||
foreach (var droppedObject in droppedObjects)
|
||||
applyDropAnimation(droppedObject, animation);
|
||||
for (int i = 0; i < caughtCount; i++)
|
||||
{
|
||||
CaughtObject obj = getDroppedObject(states[i]);
|
||||
droppedObjectTarget.Add(obj);
|
||||
applyDropAnimation(obj, animation);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<CatchObjectState>.Shared.Return(states);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
|
||||
{
|
||||
CatchObjectState state = caughtObject.SaveState();
|
||||
caughtObjectContainer.Remove(caughtObject, false);
|
||||
|
||||
var droppedObject = getDroppedObject(caughtObject);
|
||||
|
||||
var droppedObject = getDroppedObject(state);
|
||||
droppedObjectTarget.Add(droppedObject);
|
||||
|
||||
applyDropAnimation(droppedObject, animation);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
|
||||
var replayState = (GetContainingInputManager()!.CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
|
||||
|
||||
SetCatcherPosition(
|
||||
replayState?.CatcherX ??
|
||||
@@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (Catcher.Dashing || Catcher.HyperDashing)
|
||||
{
|
||||
double generationInterval = Catcher.HyperDashing ? 25 : 50;
|
||||
const double trail_generation_interval = 16;
|
||||
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
|
||||
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
@@ -52,5 +54,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
|
||||
|
||||
public override DrawableHitObject<CatchHitObject>? CreateDrawableRepresentation(CatchHitObject h) => null;
|
||||
|
||||
protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Edit.Checks;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckKeyCountTest
|
||||
{
|
||||
private CheckKeyCount check = null!;
|
||||
|
||||
private IBeatmap beatmap = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckKeyCount();
|
||||
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeycountFour()
|
||||
{
|
||||
beatmap.Difficulty.CircleSize = 4;
|
||||
|
||||
var context = getContext();
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeycountSmallerThanFour()
|
||||
{
|
||||
beatmap.Difficulty.CircleSize = 1;
|
||||
|
||||
var context = getContext();
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow);
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext()
|
||||
{
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
@@ -30,5 +35,43 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
|
||||
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReloadOnBPMChange()
|
||||
{
|
||||
HitObjectComposer oldComposer = null!;
|
||||
|
||||
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
|
||||
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
|
||||
AddStep("switch to timing tab", () => InputManager.Key(Key.F3));
|
||||
AddUntilStep("wait for loaded", () => this.ChildrenOfType<TimingAdjustButton>().ElementAtOrDefault(1), () => Is.Not.Null);
|
||||
AddStep("change timing point BPM", () =>
|
||||
{
|
||||
var bpmControl = this.ChildrenOfType<TimingAdjustButton>().ElementAt(1);
|
||||
InputManager.MoveMouseTo(bpmControl);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("switch back to composer", () => InputManager.Key(Key.F1));
|
||||
AddUntilStep("composer reloaded", () =>
|
||||
{
|
||||
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
|
||||
return composer != null && composer != oldComposer;
|
||||
});
|
||||
|
||||
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
|
||||
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
|
||||
AddStep("undo", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Z);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("composer reloaded", () =>
|
||||
{
|
||||
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
|
||||
return composer != null && composer != oldComposer;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
@@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public partial class TestHitObjectComposer : HitObjectComposer
|
||||
{
|
||||
public override Playfield Playfield { get; }
|
||||
public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException();
|
||||
public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>();
|
||||
public override bool CursorInPlacementArea => false;
|
||||
|
||||
@@ -100,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneManiaEditorSaving : EditorSavingTestScene
|
||||
{
|
||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestKeyCountChange()
|
||||
{
|
||||
LabelledSliderBar<float> keyCount = null!;
|
||||
|
||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null);
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddStep("change key count to 8", () =>
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
AddStep("change key count to 8 again", () =>
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,8 +186,106 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||
|
||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
|
||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
|
||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
|
||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragHoldNoteHead()
|
||||
{
|
||||
setScrollStep(ScrollingDirection.Down);
|
||||
|
||||
HoldNote holdNote = null;
|
||||
AddStep("setup beatmap", () =>
|
||||
{
|
||||
composer.EditorBeatmap.Clear();
|
||||
composer.EditorBeatmap.Add(holdNote = new HoldNote
|
||||
{
|
||||
Column = 1,
|
||||
StartTime = 250,
|
||||
EndTime = 750,
|
||||
});
|
||||
});
|
||||
|
||||
DrawableHoldNote drawableHoldNote = null;
|
||||
EditHoldNoteEndPiece headPiece = null;
|
||||
|
||||
AddStep("select blueprint", () =>
|
||||
{
|
||||
drawableHoldNote = this.ChildrenOfType<DrawableHoldNote>().Single();
|
||||
InputManager.MoveMouseTo(drawableHoldNote);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("grab hold note head", () =>
|
||||
{
|
||||
headPiece = this.ChildrenOfType<EditHoldNoteEndPiece>().First();
|
||||
InputManager.MoveMouseTo(headPiece);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("drag head downwards", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(headPiece, new Vector2(0, 100));
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("start time moved back", () => holdNote!.StartTime, () => Is.LessThan(250));
|
||||
AddAssert("end time unchanged", () => holdNote.EndTime, () => Is.EqualTo(750));
|
||||
|
||||
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||
|
||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
|
||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragHoldNoteTail()
|
||||
{
|
||||
setScrollStep(ScrollingDirection.Down);
|
||||
|
||||
HoldNote holdNote = null;
|
||||
AddStep("setup beatmap", () =>
|
||||
{
|
||||
composer.EditorBeatmap.Clear();
|
||||
composer.EditorBeatmap.Add(holdNote = new HoldNote
|
||||
{
|
||||
Column = 1,
|
||||
StartTime = 250,
|
||||
EndTime = 750,
|
||||
});
|
||||
});
|
||||
|
||||
DrawableHoldNote drawableHoldNote = null;
|
||||
EditHoldNoteEndPiece tailPiece = null;
|
||||
|
||||
AddStep("select blueprint", () =>
|
||||
{
|
||||
drawableHoldNote = this.ChildrenOfType<DrawableHoldNote>().Single();
|
||||
InputManager.MoveMouseTo(drawableHoldNote);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("grab hold note tail", () =>
|
||||
{
|
||||
tailPiece = this.ChildrenOfType<EditHoldNoteEndPiece>().Last();
|
||||
InputManager.MoveMouseTo(tailPiece);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("drag tail upwards", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(tailPiece, new Vector2(0, -100));
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("start time unchanged", () => holdNote!.StartTime, () => Is.EqualTo(250));
|
||||
AddAssert("end time moved forward", () => holdNote.EndTime, () => Is.GreaterThan(750));
|
||||
|
||||
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||
|
||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
|
||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
|
||||
}
|
||||
|
||||
private void setScrollStep(ScrollingDirection direction)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneManiaSelectionHandler : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestHorizontalFlipOverSelection()
|
||||
{
|
||||
ManiaHitObject first = null!, second = null!, third = null!;
|
||||
|
||||
AddStep("create objects", () =>
|
||||
{
|
||||
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
|
||||
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
|
||||
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
|
||||
});
|
||||
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("flip horizontally over selection", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxButton>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("first object stayed in place", () => first.Column, () => Is.EqualTo(2));
|
||||
AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(3));
|
||||
AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHorizontalFlipOverPlayfield()
|
||||
{
|
||||
ManiaHitObject first = null!, second = null!, third = null!;
|
||||
|
||||
AddStep("create objects", () =>
|
||||
{
|
||||
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
|
||||
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
|
||||
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
|
||||
});
|
||||
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("flip horizontally", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.H);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
AddAssert("first object flipped", () => first.Column, () => Is.EqualTo(1));
|
||||
AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(2));
|
||||
AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVerticalFlip()
|
||||
{
|
||||
ManiaHitObject first = null!, second = null!, third = null!;
|
||||
|
||||
AddStep("create objects", () =>
|
||||
{
|
||||
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
|
||||
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
|
||||
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
|
||||
});
|
||||
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("flip vertically", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.J);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
AddAssert("first object flipped", () => first.StartTime, () => Is.EqualTo(2250));
|
||||
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
||||
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
[TestCase(ManiaAction.Key5)]
|
||||
[TestCase(ManiaAction.Key9)]
|
||||
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(9));
|
||||
@@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
|
||||
[TestCase(ManiaAction.Key3)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
[TestCase(ManiaAction.Key3, ManiaAction.Key8)]
|
||||
[TestCase(ManiaAction.Key3, ManiaAction.Key6)]
|
||||
[TestCase(ManiaAction.Key10)]
|
||||
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(5));
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
@@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
Mod = new ManiaModInvert(),
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBreaksPreservedOnOriginalBeatmap()
|
||||
{
|
||||
var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo);
|
||||
beatmap.Breaks.Clear();
|
||||
beatmap.Breaks.Add(new BreakPeriod(0, 1000));
|
||||
|
||||
var workingBeatmap = new FlatWorkingBeatmap(beatmap);
|
||||
|
||||
var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() });
|
||||
Assert.That(playableWithInvert.Breaks.Count, Is.Zero);
|
||||
|
||||
var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo);
|
||||
Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero);
|
||||
Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
// 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.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneManiaModNoRelease : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private const double time_before_head = 250;
|
||||
private const double time_head = 1500;
|
||||
private const double time_during_hold_1 = 2500;
|
||||
private const double time_tail = 4000;
|
||||
private const double time_after_tail = 5250;
|
||||
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// o o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestNoInput()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_before_head),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestCorrectInput()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestLateRelease()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressTooEarlyAndReleaseAfterTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_after_tail, ManiaAction.Key1),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressTooEarlyAndReleaseAtTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_before_head + 10),
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_before_head + 10),
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAtStartAndBreak()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + 10),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xox o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + 1),
|
||||
new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertComboAtJudgement(0, 1);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
assertComboAtJudgement(1, 0);
|
||||
assertComboAtJudgement(3, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + 10),
|
||||
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo x o o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + 10),
|
||||
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressDuringNoteAndReleaseAfterTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressDuringNoteAndReleaseAtTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]--------------
|
||||
/// xo
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAndReleaseAfterTailWithCloseByHead()
|
||||
{
|
||||
const int duration = 30;
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
// hold note is very short, to make the head still in range
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = duration,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head + duration + 60, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + duration + 70),
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-O-------------
|
||||
/// xo o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead()
|
||||
{
|
||||
Note note;
|
||||
|
||||
const int duration = 50;
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
// hold note is very short, to make the head still in range
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = duration,
|
||||
Column = 0,
|
||||
},
|
||||
{
|
||||
// Next note within tail lenience
|
||||
note = new Note
|
||||
{
|
||||
StartTime = time_head + duration + 10
|
||||
}
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head + duration, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + duration + 10),
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Good);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Miss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]--O--
|
||||
/// xo o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAndReleaseJustBeforeTailWithNearbyNote()
|
||||
{
|
||||
Note note;
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
},
|
||||
{
|
||||
// Next note within tail lenience
|
||||
note = new Note
|
||||
{
|
||||
StartTime = time_tail + 50
|
||||
}
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAndReleaseJustAfterTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_tail + 20, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail + 30),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]--O--
|
||||
/// xo o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
|
||||
{
|
||||
// Next note within tail lenience
|
||||
Note note = new Note { StartTime = time_tail + 50 };
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
},
|
||||
note
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail + 20),
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Great);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAndReleaseAtTail()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_tail, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail + 10),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissReleaseAndHitSecondRelease()
|
||||
{
|
||||
var windows = new ManiaHitWindows();
|
||||
windows.SetDifficulty(10);
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 500,
|
||||
Column = 0,
|
||||
},
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
|
||||
Duration = 500,
|
||||
Column = 0,
|
||||
},
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderTickRate = 4,
|
||||
OverallDifficulty = 10,
|
||||
},
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
|
||||
}, beatmap);
|
||||
|
||||
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => !j.Type.IsHit()));
|
||||
|
||||
AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroLength()
|
||||
{
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 0,
|
||||
Column = 0,
|
||||
},
|
||||
},
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1),
|
||||
}, beatmap);
|
||||
|
||||
AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
|
||||
=> AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result));
|
||||
|
||||
private void assertTailJudgement(HitResult result)
|
||||
=> AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result));
|
||||
|
||||
private void assertNoteJudgement(HitResult result)
|
||||
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
|
||||
|
||||
private void assertComboAtJudgement(int judgementIndex, int combo)
|
||||
=> AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject>? beatmap = null)
|
||||
{
|
||||
if (beatmap == null)
|
||||
{
|
||||
beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
}
|
||||
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
SelectedMods.Value = new List<Mod>
|
||||
{
|
||||
new ManiaModNoRelease()
|
||||
};
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
|
||||
{
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
||||
protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo();
|
||||
|
||||
[Cached]
|
||||
private readonly StageDefinition stage = new StageDefinition(4);
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
|
||||
protected ManiaSkinnableTestScene()
|
||||
{
|
||||
scrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
ScrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
|
||||
Add(new Box
|
||||
{
|
||||
@@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Test]
|
||||
public void TestScrollingDown()
|
||||
{
|
||||
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollingUp()
|
||||
{
|
||||
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
}
|
||||
|
||||
private class TestScrollingInfo : IScrollingInfo
|
||||
protected class TestScrollingInfo : IScrollingInfo
|
||||
{
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Argon;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
public partial class TestSceneComboCounter : ManiaSkinnableTestScene
|
||||
{
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
setup(Anchor.Centre);
|
||||
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
|
||||
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnchorOrigin()
|
||||
{
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
setup(Anchor.TopCentre, 20);
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
check(Anchor.BottomCentre, -20);
|
||||
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
setup(Anchor.BottomCentre, -20);
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
check(Anchor.TopCentre, 20);
|
||||
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
setup(Anchor.Centre, 20);
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
check(Anchor.Centre, 20);
|
||||
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
setup(Anchor.Centre, -20);
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
check(Anchor.Centre, -20);
|
||||
}
|
||||
|
||||
private void setup(Anchor anchor, float y = 0)
|
||||
{
|
||||
AddStep($"setup {anchor} {y}", () => SetContents(s =>
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
if (s is ArgonSkin)
|
||||
container.Add(new ArgonManiaComboCounter());
|
||||
else if (s is LegacySkin)
|
||||
container.Add(new LegacyManiaComboCounter());
|
||||
else
|
||||
container.Add(new LegacyManiaComboCounter());
|
||||
|
||||
container.Child.Anchor = anchor;
|
||||
container.Child.Origin = Anchor.Centre;
|
||||
container.Child.Y = y;
|
||||
|
||||
return container;
|
||||
}));
|
||||
}
|
||||
|
||||
private void check(Anchor anchor, float y)
|
||||
{
|
||||
AddAssert($"check {anchor} {y}", () =>
|
||||
{
|
||||
foreach (var combo in this.ChildrenOfType<ISerialisableDrawable>())
|
||||
{
|
||||
var drawableCombo = (Drawable)combo;
|
||||
if (drawableCombo.Anchor != anchor || drawableCombo.Y != y)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,14 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
AddStep("Show " + result.GetDescription(), () =>
|
||||
{
|
||||
SetContents(_ =>
|
||||
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
|
||||
{
|
||||
Type = result
|
||||
}, null)
|
||||
{
|
||||
var drawableManiaJudgement = new DrawableManiaJudgement
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
};
|
||||
|
||||
drawableManiaJudgement.Apply(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
|
||||
{
|
||||
Type = result
|
||||
}, null);
|
||||
|
||||
return drawableManiaJudgement;
|
||||
});
|
||||
|
||||
// for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value
|
||||
// (see `LegacyManiaJudgementPiece.load()`).
|
||||
|
||||
@@ -3,15 +3,22 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
public partial class TestScenePlayfield : ManiaSkinnableTestScene
|
||||
{
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
|
||||
|
||||
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
|
||||
|
||||
[Test]
|
||||
@@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
Child = new ManiaPlayfield(stageDefinitions)
|
||||
});
|
||||
});
|
||||
|
||||
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
|
||||
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||
}
|
||||
|
||||
[TestCase(2)]
|
||||
@@ -54,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
|
||||
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmapForSkinProvider()
|
||||
|
||||
@@ -14,12 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
SetContents(_ =>
|
||||
{
|
||||
ManiaAction normalAction = ManiaAction.Key1;
|
||||
ManiaAction specialAction = ManiaAction.Special1;
|
||||
ManiaAction action = ManiaAction.Key1;
|
||||
|
||||
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
|
||||
{
|
||||
Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction)
|
||||
Child = new Stage(0, new StageDefinition(4), ref action)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddStep("Hold key", () =>
|
||||
{
|
||||
clock.CurrentTime = 0;
|
||||
note.OnPressed(new KeyBindingPressEvent<ManiaAction>(GetContainingInputManager().CurrentState, ManiaAction.Key1));
|
||||
note.OnPressed(new KeyBindingPressEvent<ManiaAction>(GetContainingInputManager()!.CurrentState, ManiaAction.Key1));
|
||||
});
|
||||
AddStep("progress time", () => clock.CurrentTime = 500);
|
||||
AddAssert("head is visible", () => note.Head.Alpha == 1);
|
||||
|
||||
@@ -474,8 +474,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => !j.Type.IsHit()));
|
||||
|
||||
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type.IsHit()));
|
||||
AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneManiaPlayerLegacySkin : LegacySkinPlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
// play with a converted beatmap to allow dual stages mod to work.
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(new RulesetInfo());
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[Test]
|
||||
public void TestSingleStage()
|
||||
{
|
||||
AddStep("Load single stage", LoadPlayer);
|
||||
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDualStage()
|
||||
{
|
||||
AddStep("Load dual stage", () => LoadPlayer(new Mod[] { new ManiaModDualStages() }));
|
||||
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||