Merge branch 'master' into magnetised-judgements
@ -21,7 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2023.1117.0",
|
"version": "2024.517.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
]
|
||||||
|
1
.gitignore
vendored
@ -341,3 +341,4 @@ inspectcode
|
|||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
|
||||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
.idea/.idea.osu.Desktop/.idea/misc.xml
|
||||||
|
.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup Label="C#">
|
<PropertyGroup Label="C#">
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>12.0</LangVersion>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
BIN
assets/lazer.png
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 438 KiB |
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.419.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.528.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Android.Input;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Localisation;
|
|
||||||
using osu.Game.Overlays.Settings;
|
|
||||||
|
|
||||||
namespace osu.Android
|
|
||||||
{
|
|
||||||
public partial class AndroidJoystickSettings : SettingsSubsection
|
|
||||||
{
|
|
||||||
protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
|
|
||||||
|
|
||||||
private readonly AndroidJoystickHandler joystickHandler;
|
|
||||||
|
|
||||||
private readonly Bindable<bool> enabled = new BindableBool(true);
|
|
||||||
|
|
||||||
private SettingsSlider<float> deadzoneSlider = null!;
|
|
||||||
|
|
||||||
private Bindable<float> handlerDeadzone = null!;
|
|
||||||
|
|
||||||
private Bindable<float> localDeadzone = null!;
|
|
||||||
|
|
||||||
public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
|
|
||||||
{
|
|
||||||
this.joystickHandler = joystickHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
// use local bindable to avoid changing enabled state of game host's bindable.
|
|
||||||
handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
|
|
||||||
localDeadzone = handlerDeadzone.GetUnboundCopy();
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new SettingsCheckbox
|
|
||||||
{
|
|
||||||
LabelText = CommonStrings.Enabled,
|
|
||||||
Current = enabled
|
|
||||||
},
|
|
||||||
deadzoneSlider = new SettingsSlider<float>
|
|
||||||
{
|
|
||||||
LabelText = JoystickSettingsStrings.DeadzoneThreshold,
|
|
||||||
KeyboardStep = 0.01f,
|
|
||||||
DisplayAsPercentage = true,
|
|
||||||
Current = localDeadzone,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
enabled.BindTo(joystickHandler.Enabled);
|
|
||||||
enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
|
|
||||||
|
|
||||||
handlerDeadzone.BindValueChanged(val =>
|
|
||||||
{
|
|
||||||
bool disabled = localDeadzone.Disabled;
|
|
||||||
|
|
||||||
localDeadzone.Disabled = false;
|
|
||||||
localDeadzone.Value = val.NewValue;
|
|
||||||
localDeadzone.Disabled = disabled;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using Android.OS;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Android.Input;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Localisation;
|
|
||||||
using osu.Game.Overlays.Settings;
|
|
||||||
using osu.Game.Overlays.Settings.Sections.Input;
|
|
||||||
|
|
||||||
namespace osu.Android
|
|
||||||
{
|
|
||||||
public partial class AndroidMouseSettings : SettingsSubsection
|
|
||||||
{
|
|
||||||
private readonly AndroidMouseHandler mouseHandler;
|
|
||||||
|
|
||||||
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
|
|
||||||
|
|
||||||
private Bindable<double> handlerSensitivity = null!;
|
|
||||||
|
|
||||||
private Bindable<double> localSensitivity = null!;
|
|
||||||
|
|
||||||
private Bindable<bool> relativeMode = null!;
|
|
||||||
|
|
||||||
public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
|
|
||||||
{
|
|
||||||
this.mouseHandler = mouseHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuConfigManager osuConfig)
|
|
||||||
{
|
|
||||||
// use local bindable to avoid changing enabled state of game host's bindable.
|
|
||||||
handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
|
|
||||||
localSensitivity = handlerSensitivity.GetUnboundCopy();
|
|
||||||
|
|
||||||
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
|
|
||||||
|
|
||||||
// High precision/pointer capture is only available on Android 8.0 and up
|
|
||||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
new SettingsCheckbox
|
|
||||||
{
|
|
||||||
LabelText = MouseSettingsStrings.HighPrecisionMouse,
|
|
||||||
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
|
|
||||||
Current = relativeMode,
|
|
||||||
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
|
|
||||||
},
|
|
||||||
new MouseSettings.SensitivitySetting
|
|
||||||
{
|
|
||||||
LabelText = MouseSettingsStrings.CursorSensitivity,
|
|
||||||
Current = localSensitivity,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
new SettingsCheckbox
|
|
||||||
{
|
|
||||||
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
|
|
||||||
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
|
|
||||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
|
|
||||||
},
|
|
||||||
new SettingsCheckbox
|
|
||||||
{
|
|
||||||
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
|
|
||||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
|
|
||||||
|
|
||||||
handlerSensitivity.BindValueChanged(val =>
|
|
||||||
{
|
|
||||||
bool disabled = localSensitivity.Disabled;
|
|
||||||
|
|
||||||
localSensitivity.Disabled = false;
|
|
||||||
localSensitivity.Value = val.NewValue;
|
|
||||||
localSensitivity.Disabled = disabled;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,13 +5,9 @@ using System;
|
|||||||
using Android.App;
|
using Android.App;
|
||||||
using Microsoft.Maui.Devices;
|
using Microsoft.Maui.Devices;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Android.Input;
|
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Input.Handlers;
|
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
using osu.Game.Overlays.Settings;
|
|
||||||
using osu.Game.Overlays.Settings.Sections.Input;
|
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
|
||||||
@ -88,24 +84,6 @@ namespace osu.Android
|
|||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
||||||
|
|
||||||
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
|
||||||
{
|
|
||||||
switch (handler)
|
|
||||||
{
|
|
||||||
case AndroidMouseHandler mh:
|
|
||||||
return new AndroidMouseSettings(mh);
|
|
||||||
|
|
||||||
case AndroidJoystickHandler jh:
|
|
||||||
return new AndroidJoystickSettings(jh);
|
|
||||||
|
|
||||||
case AndroidTouchHandler th:
|
|
||||||
return new TouchSettings(th);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return base.CreateSettingsSubsectionFor(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AndroidBatteryInfo : BatteryInfo
|
private class AndroidBatteryInfo : BatteryInfo
|
||||||
{
|
{
|
||||||
public override double? ChargeLevel => Battery.ChargeLevel;
|
public override double? ChargeLevel => Battery.ChargeLevel;
|
||||||
|
@ -1,24 +1,8 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="108dp" android:viewportHeight="434"
|
||||||
android:width="108dp"
|
android:viewportWidth="434" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:height="108dp"
|
<path android:fillColor="#000000" android:pathData="M299.36,178.05C303.08,178.05 305.62,180.81 305.62,184.62V219.92C305.62,223.74 303.08,226.5 299.36,226.5C295.55,226.5 293.11,223.74 293.11,219.92V184.62C293.11,180.81 295.55,178.05 299.36,178.05ZM299.36,248.97C294.81,248.97 291.2,245.37 291.2,240.81C291.2,236.35 294.81,232.75 299.36,232.75C303.92,232.75 307.53,236.35 307.53,240.81C307.53,245.37 303.92,248.97 299.36,248.97Z"/>
|
||||||
android:viewportWidth="108"
|
<path android:fillColor="#000000" android:pathData="M276.52,195.12C280.34,195.12 282.77,197.87 282.77,201.58V225.01C282.77,242.29 272.12,248.97 259.19,248.97C246.15,248.97 235.49,242.29 235.49,225.01V201.58C235.49,197.87 237.93,195.12 241.75,195.12C245.46,195.12 248,197.87 248,201.58V224.16C248,233.6 251.98,237.31 259.19,237.31C266.29,237.31 270.27,233.6 270.27,224.16V201.58C270.27,197.87 272.81,195.12 276.52,195.12Z"/>
|
||||||
android:viewportHeight="108">
|
<path android:fillColor="#000000" android:pathData="M200.02,209.43C200.02,212.93 203.63,214.2 210.52,215.79C220.06,218.12 229.18,220.56 229.18,232.33C229.18,243.78 220.7,248.97 208.51,248.97C198.43,248.97 191.12,245.47 187.73,241.44C185.08,238.26 185.4,235.51 187.94,233.07C191.12,229.99 193.88,231.27 195.68,232.86C198.54,235.51 202.04,238.26 208.82,238.26C213.91,238.26 217.09,236.57 217.09,233.18C217.09,229.78 213.7,228.62 204.8,226.18C196,223.74 188.15,221.41 188.15,210.91C188.15,199.15 197.69,194.27 208.29,194.27C214.34,194.27 221.23,195.86 225.47,200.42C227.27,202.22 228.65,204.76 225.47,208.26C222.29,211.55 219.96,210.6 217.73,208.9C215.71,207.41 212.22,204.87 206.92,204.87C203.31,204.87 200.02,206.04 200.02,209.43Z"/>
|
||||||
<path
|
<path android:fillColor="#000000" android:pathData="M153.74,248.97C138.46,248.97 127.5,237.27 127.5,221.53C127.5,205.68 138.46,194.09 153.74,194.09C169.03,194.09 179.99,205.68 179.99,221.53C179.99,237.27 169.03,248.97 153.74,248.97ZM153.74,237.27C162.59,237.27 168.12,230.46 168.12,221.53C168.12,212.6 162.59,205.68 153.74,205.68C144.89,205.68 139.36,212.6 139.36,221.53C139.36,230.46 144.89,237.27 153.74,237.27Z"/>
|
||||||
android:pathData="M73.92,44.43C74.82,44.43 75.43,45.1 75.43,46.02V54.54C75.43,55.46 74.82,56.13 73.92,56.13C73,56.13 72.41,55.46 72.41,54.54V46.02C72.41,45.1 73,44.43 73.92,44.43ZM73.92,61.55C72.82,61.55 71.95,60.68 71.95,59.58C71.95,58.51 72.82,57.64 73.92,57.64C75.02,57.64 75.89,58.51 75.89,59.58C75.89,60.68 75.02,61.55 73.92,61.55Z"
|
<path android:fillColor="#000000" android:pathData="M349,217.5C349,290.13 290.13,349 217.5,349C144.88,349 86,290.13 86,217.5C86,144.88 144.88,86 217.5,86C290.13,86 349,144.88 349,217.5ZM99.15,217.5C99.15,282.86 152.14,335.85 217.5,335.85C282.86,335.85 335.85,282.86 335.85,217.5C335.85,152.14 282.86,99.15 217.5,99.15C152.14,99.15 99.15,152.14 99.15,217.5Z"/>
|
||||||
android:fillColor="#000000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M68.41,48.55C69.33,48.55 69.92,49.22 69.92,50.11V55.77C69.92,59.94 67.35,61.55 64.22,61.55C61.08,61.55 58.5,59.94 58.5,55.77V50.11C58.5,49.22 59.09,48.55 60.01,48.55C60.91,48.55 61.52,49.22 61.52,50.11V55.56C61.52,57.84 62.48,58.74 64.22,58.74C65.94,58.74 66.9,57.84 66.9,55.56V50.11C66.9,49.22 67.51,48.55 68.41,48.55Z"
|
|
||||||
android:fillColor="#000000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M49.94,52.01C49.94,52.85 50.81,53.16 52.47,53.54C54.78,54.1 56.98,54.69 56.98,57.53C56.98,60.3 54.93,61.55 51.99,61.55C49.56,61.55 47.79,60.71 46.97,59.73C46.33,58.97 46.41,58.3 47.02,57.71C47.79,56.97 48.46,57.28 48.89,57.66C49.58,58.3 50.43,58.97 52.07,58.97C53.29,58.97 54.06,58.56 54.06,57.74C54.06,56.92 53.24,56.64 51.09,56.05C48.97,55.46 47.08,54.9 47.08,52.36C47.08,49.52 49.38,48.35 51.94,48.35C53.4,48.35 55.06,48.73 56.08,49.83C56.52,50.27 56.85,50.88 56.08,51.72C55.32,52.52 54.75,52.29 54.22,51.88C53.73,51.52 52.88,50.91 51.6,50.91C50.73,50.91 49.94,51.19 49.94,52.01Z"
|
|
||||||
android:fillColor="#000000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M38.79,61.55C34.9,61.55 32.11,58.74 32.11,54.95C32.11,51.14 34.9,48.35 38.79,48.35C42.68,48.35 45.47,51.14 45.47,54.95C45.47,58.74 42.68,61.55 38.79,61.55ZM38.79,58.74C41.04,58.74 42.45,57.1 42.45,54.95C42.45,52.8 41.04,51.14 38.79,51.14C36.54,51.14 35.13,52.8 35.13,54.95C35.13,57.1 36.54,58.74 38.79,58.74Z"
|
|
||||||
android:fillColor="#000000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M86,54C86,71.67 71.67,86 54,86C36.33,86 22,71.67 22,54C22,36.33 36.33,22 54,22C71.67,22 86,36.33 86,54ZM25.2,54C25.2,69.91 38.09,82.8 54,82.8C69.91,82.8 82.8,69.91 82.8,54C82.8,38.09 69.91,25.2 54,25.2C38.09,25.2 25.2,38.09 25.2,54Z"
|
|
||||||
android:fillColor="#000000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M36.78,54.99C36.78,56.09 37.65,56.96 38.75,56.96C39.85,56.96 40.72,56.09 40.72,54.99C40.72,53.91 39.85,53.04 38.75,53.04C37.65,53.04 36.78,53.91 36.78,54.99Z"
|
|
||||||
android:fillColor="#000000"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 49 KiB |
@ -164,8 +164,8 @@ namespace osu.Desktop
|
|||||||
// user activity
|
// user activity
|
||||||
if (activity.Value != null)
|
if (activity.Value != null)
|
||||||
{
|
{
|
||||||
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
|
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||||
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||||
|
|
||||||
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
|
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
|
||||||
{
|
{
|
||||||
@ -271,8 +271,19 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
||||||
|
|
||||||
private static string truncate(string str)
|
private static string clampLength(string str)
|
||||||
{
|
{
|
||||||
|
// Empty strings are fine to discord even though single-character strings are not. Make it make sense.
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return str;
|
||||||
|
|
||||||
|
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
||||||
|
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
||||||
|
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||||
|
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
|
||||||
|
if (str.Length < 2)
|
||||||
|
return str.PadRight(2, '\u200B');
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
||||||
return str;
|
return str;
|
||||||
|
|
||||||
|
@ -489,6 +489,7 @@ namespace osu.Desktop
|
|||||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
|
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
internal enum NvStatus
|
internal enum NvStatus
|
||||||
{
|
{
|
||||||
OK = 0, // Success. Request is completed.
|
OK = 0, // Success. Request is completed.
|
||||||
@ -611,6 +612,7 @@ namespace osu.Desktop
|
|||||||
FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
|
FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
internal enum NvSystemType
|
internal enum NvSystemType
|
||||||
{
|
{
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
@ -618,6 +620,7 @@ namespace osu.Desktop
|
|||||||
DESKTOP = 2
|
DESKTOP = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
internal enum NvGpuType
|
internal enum NvGpuType
|
||||||
{
|
{
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
@ -625,6 +628,7 @@ namespace osu.Desktop
|
|||||||
DGPU = 2, // Discrete
|
DGPU = 2, // Discrete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
internal enum NvSettingID : uint
|
internal enum NvSettingID : uint
|
||||||
{
|
{
|
||||||
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
|
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
|
||||||
@ -717,6 +721,7 @@ namespace osu.Desktop
|
|||||||
INVALID_SETTING_ID = 0xFFFFFFFF
|
INVALID_SETTING_ID = 0xFFFFFFFF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
internal enum NvShimSetting : uint
|
internal enum NvShimSetting : uint
|
||||||
{
|
{
|
||||||
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
|
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
|
||||||
@ -731,6 +736,7 @@ namespace osu.Desktop
|
|||||||
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
|
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
internal enum NvThreadControlSetting : uint
|
internal enum NvThreadControlSetting : uint
|
||||||
{
|
{
|
||||||
OGL_THREAD_CONTROL_ENABLE = 0x00000001,
|
OGL_THREAD_CONTROL_ENABLE = 0x00000001,
|
||||||
|
@ -22,7 +22,6 @@ using osu.Game.IPC;
|
|||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Performance;
|
using osu.Game.Performance;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using SDL;
|
|
||||||
|
|
||||||
namespace osu.Desktop
|
namespace osu.Desktop
|
||||||
{
|
{
|
||||||
@ -161,7 +160,7 @@ namespace osu.Desktop
|
|||||||
host.Window.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
@ -169,24 +168,5 @@ namespace osu.Desktop
|
|||||||
osuSchemeLinkIPCChannel?.Dispose();
|
osuSchemeLinkIPCChannel?.Dispose();
|
||||||
archiveImportIPCChannel?.Dispose();
|
archiveImportIPCChannel?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,13 @@ namespace osu.Desktop
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null }))
|
var hostOptions = new HostOptions
|
||||||
|
{
|
||||||
|
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
|
||||||
|
FriendlyGameName = OsuGameBase.GAME_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions))
|
||||||
{
|
{
|
||||||
if (!host.IsPrimaryInstance)
|
if (!host.IsPrimaryInstance)
|
||||||
{
|
{
|
||||||
|
25
osu.Desktop/SDL2BatteryInfo.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
|
namespace osu.Desktop
|
||||||
|
{
|
||||||
|
internal class SDL2BatteryInfo : BatteryInfo
|
||||||
|
{
|
||||||
|
public override double? ChargeLevel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
|
||||||
|
|
||||||
|
if (percentage == -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return percentage / 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||||
|
}
|
||||||
|
}
|
27
osu.Desktop/SDL3BatteryInfo.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using SDL;
|
||||||
|
|
||||||
|
namespace osu.Desktop
|
||||||
|
{
|
||||||
|
internal unsafe class SDL3BatteryInfo : BatteryInfo
|
||||||
|
{
|
||||||
|
public override double? ChargeLevel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int percentage;
|
||||||
|
SDL3.SDL_GetPowerInfo(null, &percentage);
|
||||||
|
|
||||||
|
if (percentage == -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return percentage / 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
@ -163,6 +164,7 @@ namespace osu.Desktop.Windows
|
|||||||
[DllImport("Shell32.dll")]
|
[DllImport("Shell32.dll")]
|
||||||
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
private enum EventId
|
private enum EventId
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -172,6 +174,7 @@ namespace osu.Desktop.Windows
|
|||||||
SHCNE_ASSOCCHANGED = 0x08000000
|
SHCNE_ASSOCCHANGED = 0x08000000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
private enum Flags : uint
|
private enum Flags : uint
|
||||||
{
|
{
|
||||||
SHCNF_IDLIST = 0x0000
|
SHCNF_IDLIST = 0x0000
|
||||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchAreaNotInitiallyVisible()
|
||||||
|
{
|
||||||
|
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPressReceptors()
|
||||||
|
{
|
||||||
|
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
|
||||||
|
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action sent",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Contain(getReceptor(index).Action.Value));
|
||||||
|
|
||||||
|
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||||
|
|
||||||
|
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
|
public new ManiaPlayfield Playfield => drawableRuleset.Playfield;
|
||||||
|
|
||||||
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
|
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
|
||||||
|
|
||||||
|
@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
|
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
|
||||||
// (see `Stage.columnBackgrounds`).
|
// (see `Stage.columnBackgrounds`).
|
||||||
BackgroundContainer,
|
BackgroundContainer,
|
||||||
TopLevelContainer,
|
TopLevelContainer
|
||||||
new ColumnTouchInputArea(this)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||||
@ -181,38 +180,5 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||||
|
|
||||||
public partial class ColumnTouchInputArea : Drawable
|
|
||||||
{
|
|
||||||
private readonly Column column;
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
|
||||||
private ManiaInputManager maniaInputManager { get; set; }
|
|
||||||
|
|
||||||
private KeyBindingContainer<ManiaAction> keyBindingContainer;
|
|
||||||
|
|
||||||
public ColumnTouchInputArea(Column column)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
this.column = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnTouchDown(TouchDownEvent e)
|
|
||||||
{
|
|
||||||
keyBindingContainer?.TriggerPressed(column.Action.Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTouchUp(TouchUpEvent e)
|
|
||||||
{
|
|
||||||
keyBindingContainer?.TriggerReleased(column.Action.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,12 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Skinning;
|
using osu.Game.Rulesets.Mania.Skinning;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
@ -62,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
onSkinChanged();
|
onSkinChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
updateMobileSizing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSkinChanged()
|
private void onSkinChanged()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||||
@ -92,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
columns[i].Width = width.Value;
|
columns[i].Width = width.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMobileSizing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -106,31 +95,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
Content[column] = columns[column].Child = content;
|
Content[column] = columns[column].Child = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMobileSizing()
|
|
||||||
{
|
|
||||||
if (!IsLoaded || !RuntimeInfo.IsMobile)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
|
|
||||||
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
|
|
||||||
|
|
||||||
// Will be null in tests.
|
|
||||||
if (containingCell == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
|
|
||||||
|
|
||||||
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
|
|
||||||
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
|
|
||||||
// 1.92 is a "reference" mobile screen aspect ratio for phones.
|
|
||||||
// We should scale it back for cases like tablets which aren't so extreme.
|
|
||||||
mobileAdjust *= aspectRatio / 1.92f;
|
|
||||||
|
|
||||||
// Best effort until we have better mobile support.
|
|
||||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
|
||||||
columns[i].Width *= mobileAdjust;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -31,6 +31,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MAX_TIME_RANGE = 11485;
|
public const double MAX_TIME_RANGE = 11485;
|
||||||
|
|
||||||
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
public new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
||||||
|
|
||||||
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
|
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
|
||||||
|
|
||||||
@ -103,6 +104,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
|
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
|
||||||
|
|
||||||
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||||
|
|
||||||
|
KeyBindingInputManager.Add(new ManiaTouchInputArea());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
||||||
|
216
osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An overlay that captures and displays osu!mania mouse and touch input.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ManiaTouchInputArea : VisibilityContainer
|
||||||
|
{
|
||||||
|
// visibility state affects our child. we always want to handle input.
|
||||||
|
public override bool PropagatePositionalInputSubTree => true;
|
||||||
|
public override bool PropagateNonPositionalInputSubTree => true;
|
||||||
|
|
||||||
|
[SettingSource("Spacing", "The spacing between receptors.")]
|
||||||
|
public BindableFloat Spacing { get; } = new BindableFloat(10)
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
[SettingSource("Opacity", "The receptor opacity.")]
|
||||||
|
public BindableFloat Opacity { get; } = new BindableFloat(1)
|
||||||
|
{
|
||||||
|
Precision = 0.1f,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
|
||||||
|
|
||||||
|
private GridContainer gridContainer = null!;
|
||||||
|
|
||||||
|
public ManiaTouchInputArea()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomCentre;
|
||||||
|
Origin = Anchor.BottomCentre;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Height = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
List<Drawable> receptorGridContent = new List<Drawable>();
|
||||||
|
List<Dimension> receptorGridDimensions = new List<Dimension>();
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
foreach (var stage in drawableRuleset.Playfield.Stages)
|
||||||
|
{
|
||||||
|
foreach (var column in stage.Columns)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
|
||||||
|
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
|
||||||
|
receptorGridDimensions.Add(new Dimension());
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalChild = gridContainer = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Content = new[] { receptorGridContent.ToArray() },
|
||||||
|
ColumnDimensions = receptorGridDimensions.ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
// Hide whenever the keyboard is used.
|
||||||
|
Hide();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
gridContainer.FadeIn(500, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
gridContainer.FadeOut(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ColumnInputReceptor : CompositeDrawable
|
||||||
|
{
|
||||||
|
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||||
|
|
||||||
|
private readonly Box highlightOverlay;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ManiaInputManager? inputManager { get; set; }
|
||||||
|
|
||||||
|
private bool isPressed;
|
||||||
|
|
||||||
|
public ColumnInputReceptor()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 10,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0.15f,
|
||||||
|
},
|
||||||
|
highlightOverlay = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
|
{
|
||||||
|
updateButton(true);
|
||||||
|
return false; // handled by parent container to show overlay.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnTouchUp(TouchUpEvent e)
|
||||||
|
{
|
||||||
|
updateButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
updateButton(true);
|
||||||
|
return false; // handled by parent container to show overlay.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
|
{
|
||||||
|
updateButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButton(bool press)
|
||||||
|
{
|
||||||
|
if (press == isPressed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
isPressed = press;
|
||||||
|
|
||||||
|
if (press)
|
||||||
|
{
|
||||||
|
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value);
|
||||||
|
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value);
|
||||||
|
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class Gutter : Drawable
|
||||||
|
{
|
||||||
|
public readonly IBindable<float> Spacing = new Bindable<float>();
|
||||||
|
|
||||||
|
public Gutter()
|
||||||
|
{
|
||||||
|
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -52,6 +53,65 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
|
||||||
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
||||||
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||||
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDistanceSnapAdjustDoesNotHideTheGridIfStartingEnabled()
|
||||||
|
{
|
||||||
|
double distanceSnap = double.PositiveInfinity;
|
||||||
|
|
||||||
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
|
|
||||||
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value);
|
||||||
|
|
||||||
|
AddStep("increase distance", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.ScrollVerticalBy(1);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("distance snap increased", () => this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
|
||||||
|
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDistanceSnapAdjustShowsGridMomentarilyIfStartingDisabled()
|
||||||
|
{
|
||||||
|
double distanceSnap = double.PositiveInfinity;
|
||||||
|
|
||||||
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value);
|
||||||
|
|
||||||
|
AddStep("start increasing distance", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
|
||||||
|
AddStep("finish increasing distance", () =>
|
||||||
|
{
|
||||||
|
InputManager.ScrollVerticalBy(1);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("distance snap increased", () => this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
|
||||||
|
AddUntilStep("distance snap hidden in the end", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -30,23 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestAddOverlappingControlPoints()
|
|
||||||
{
|
|
||||||
createVisualiser(true);
|
|
||||||
|
|
||||||
addControlPointStep(new Vector2(200));
|
|
||||||
addControlPointStep(new Vector2(300));
|
|
||||||
addControlPointStep(new Vector2(300));
|
|
||||||
addControlPointStep(new Vector2(500, 300));
|
|
||||||
|
|
||||||
AddAssert("last connection displayed", () =>
|
|
||||||
{
|
|
||||||
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
|
|
||||||
return lastConnection.DrawWidth > 50;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPerfectCurveTooManyPoints()
|
public void TestPerfectCurveTooManyPoints()
|
||||||
{
|
{
|
||||||
@ -194,24 +177,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
addAssertPointPositionChanged(points, i);
|
addAssertPointPositionChanged(points, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestStackingUpdatesConnectionPosition()
|
|
||||||
{
|
|
||||||
createVisualiser(true);
|
|
||||||
|
|
||||||
Vector2 connectionPosition;
|
|
||||||
addControlPointStep(connectionPosition = new Vector2(300));
|
|
||||||
addControlPointStep(new Vector2(600));
|
|
||||||
|
|
||||||
// Apply a big number in stacking so the person running the test can clearly see if it fails
|
|
||||||
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
|
|
||||||
|
|
||||||
AddAssert($"Connection at {connectionPosition} changed",
|
|
||||||
() => visualiser.Connections[0].Position,
|
|
||||||
() => !Is.EqualTo(connectionPosition)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAssertPointPositionChanged(Vector2[] points, int index)
|
private void addAssertPointPositionChanged(Vector2[] points, int index)
|
||||||
{
|
{
|
||||||
AddAssert($"Point at {points.ElementAt(index)} changed",
|
AddAssert($"Point at {points.ElementAt(index)} changed",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
@ -13,6 +14,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Rendering;
|
using osu.Framework.Graphics.Rendering;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Testing.Input;
|
using osu.Framework.Testing.Input;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
@ -47,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
createTest(() =>
|
createTest(() =>
|
||||||
{
|
{
|
||||||
var skinContainer = new LegacySkinContainer(renderer, false);
|
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false);
|
||||||
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
|
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
|
||||||
|
|
||||||
skinContainer.Child = legacyCursorTrail;
|
skinContainer.Child = legacyCursorTrail;
|
||||||
@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
createTest(() =>
|
createTest(() =>
|
||||||
{
|
{
|
||||||
var skinContainer = new LegacySkinContainer(renderer, true);
|
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true);
|
||||||
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
|
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
|
||||||
|
|
||||||
skinContainer.Child = legacyCursorTrail;
|
skinContainer.Child = legacyCursorTrail;
|
||||||
@ -70,6 +72,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyDisjointCursorTrailViaNoCursor()
|
||||||
|
{
|
||||||
|
createTest(() =>
|
||||||
|
{
|
||||||
|
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false, provideCursor: false);
|
||||||
|
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
|
||||||
|
|
||||||
|
skinContainer.Child = legacyCursorTrail;
|
||||||
|
|
||||||
|
return skinContainer;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
@ -86,12 +104,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private partial class LegacySkinContainer : Container, ISkinSource
|
private partial class LegacySkinContainer : Container, ISkinSource
|
||||||
{
|
{
|
||||||
private readonly IRenderer renderer;
|
private readonly IRenderer renderer;
|
||||||
private readonly bool disjoint;
|
private readonly bool provideMiddle;
|
||||||
|
private readonly bool provideCursor;
|
||||||
|
|
||||||
public LegacySkinContainer(IRenderer renderer, bool disjoint)
|
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
|
||||||
{
|
{
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.disjoint = disjoint;
|
this.provideMiddle = provideMiddle;
|
||||||
|
this.provideCursor = provideCursor;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
@ -102,15 +122,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
switch (componentName)
|
switch (componentName)
|
||||||
{
|
{
|
||||||
case "cursortrail":
|
case "cursor":
|
||||||
var tex = new Texture(renderer.WhitePixel);
|
return provideCursor ? new Texture(renderer.WhitePixel) : null;
|
||||||
|
|
||||||
if (disjoint)
|
case "cursortrail":
|
||||||
tex.ScaleAdjust = 1 / 25f;
|
return new Texture(renderer.WhitePixel);
|
||||||
return tex;
|
|
||||||
|
|
||||||
case "cursormiddle":
|
case "cursormiddle":
|
||||||
return disjoint ? null : renderer.WhitePixel;
|
return provideMiddle ? null : renderer.WhitePixel;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.Beatmaps.ControlPoints;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -578,6 +579,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
assertKeyCounter(1, 1);
|
assertKeyCounter(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchJudgedCircle()
|
||||||
|
{
|
||||||
|
addHitCircleAt(TouchSource.Touch1);
|
||||||
|
addHitCircleAt(TouchSource.Touch2);
|
||||||
|
|
||||||
|
beginTouch(TouchSource.Touch1);
|
||||||
|
endTouch(TouchSource.Touch1);
|
||||||
|
|
||||||
|
// Hold the second touch (this becomes the primary touch).
|
||||||
|
beginTouch(TouchSource.Touch2);
|
||||||
|
|
||||||
|
// Touch again on the first circle.
|
||||||
|
// Because it's been judged, the cursor should not move here.
|
||||||
|
beginTouch(TouchSource.Touch1);
|
||||||
|
checkPosition(TouchSource.Touch2);
|
||||||
|
}
|
||||||
|
|
||||||
private void addHitCircleAt(TouchSource source)
|
private void addHitCircleAt(TouchSource source)
|
||||||
{
|
{
|
||||||
AddStep($"Add circle at {source}", () =>
|
AddStep($"Add circle at {source}", () =>
|
||||||
@ -590,6 +609,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
Clock = new FramedClock(new ManualClock()),
|
Clock = new FramedClock(new ManualClock()),
|
||||||
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
|
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
|
||||||
|
CheckHittable = (_, _, _) => ClickAction.Hit
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
|
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
|
||||||
rhythmStart++;
|
rhythmStart++;
|
||||||
|
|
||||||
|
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
||||||
|
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
|
||||||
|
|
||||||
for (int i = rhythmStart; i > 0; i--)
|
for (int i = rhythmStart; i > 0; i--)
|
||||||
{
|
{
|
||||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
||||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
|
|
||||||
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1);
|
|
||||||
|
|
||||||
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
||||||
|
|
||||||
@ -66,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
||||||
effectiveRatio *= 0.125;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
effectiveRatio *= 0.25;
|
effectiveRatio *= 0.25;
|
||||||
|
|
||||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||||
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
startRatio = effectiveRatio;
|
startRatio = effectiveRatio;
|
||||||
islandSize = 1;
|
islandSize = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastObj = prevObj;
|
||||||
|
prevObj = currObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
|
@ -4,10 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Lines;
|
using osu.Framework.Graphics.Lines;
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -15,36 +12,21 @@ using osuTK;
|
|||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s.
|
/// A visualisation of the lines between <see cref="PathControlPointPiece{T}"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam>
|
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnection{T}"/> visualises.</typeparam>
|
||||||
public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath
|
public partial class PathControlPointConnection<T> : SmoothPath where T : OsuHitObject, IHasPath
|
||||||
{
|
{
|
||||||
public readonly PathControlPoint ControlPoint;
|
|
||||||
|
|
||||||
private readonly Path path;
|
|
||||||
private readonly T hitObject;
|
private readonly T hitObject;
|
||||||
public int ControlPointIndex { get; set; }
|
|
||||||
|
|
||||||
private IBindable<Vector2> hitObjectPosition;
|
private IBindable<Vector2> hitObjectPosition;
|
||||||
private IBindable<int> pathVersion;
|
private IBindable<int> pathVersion;
|
||||||
private IBindable<int> stackHeight;
|
private IBindable<int> stackHeight;
|
||||||
|
|
||||||
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
|
public PathControlPointConnection(T hitObject)
|
||||||
{
|
{
|
||||||
this.hitObject = hitObject;
|
this.hitObject = hitObject;
|
||||||
ControlPointIndex = controlPointIndex;
|
PathRadius = 1;
|
||||||
|
|
||||||
Origin = Anchor.Centre;
|
|
||||||
AutoSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
|
|
||||||
|
|
||||||
InternalChild = path = new SmoothPath
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
PathRadius = 1
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -68,18 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void updateConnectingPath()
|
private void updateConnectingPath()
|
||||||
{
|
{
|
||||||
Position = hitObject.StackedPosition + ControlPoint.Position;
|
Position = hitObject.StackedPosition;
|
||||||
|
|
||||||
path.ClearVertices();
|
ClearVertices();
|
||||||
|
|
||||||
int nextIndex = ControlPointIndex + 1;
|
foreach (var controlPoint in hitObject.Path.ControlPoints)
|
||||||
if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count)
|
AddVertex(controlPoint.Position);
|
||||||
return;
|
|
||||||
|
|
||||||
path.AddVertex(Vector2.Zero);
|
OriginPosition = PositionInBoundingBox(Vector2.Zero);
|
||||||
path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
|
|
||||||
|
|
||||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,9 +8,9 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public readonly PathControlPoint ControlPoint;
|
public readonly PathControlPoint ControlPoint;
|
||||||
|
|
||||||
private readonly T hitObject;
|
private readonly T hitObject;
|
||||||
private readonly Container marker;
|
private readonly Circle circle;
|
||||||
private readonly Drawable markerRing;
|
private readonly Drawable markerRing;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -60,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
marker = new Container
|
circle = new Circle
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
new Circle
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(20),
|
Size = new Vector2(20),
|
||||||
},
|
},
|
||||||
markerRing = new CircularContainer
|
markerRing = new CircularProgress
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(28),
|
Size = new Vector2(28),
|
||||||
Masking = true,
|
|
||||||
BorderThickness = 2,
|
|
||||||
BorderColour = Color4.White,
|
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
Child = new Box
|
InnerRadius = 0.1f,
|
||||||
{
|
Progress = 1
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Alpha = 0,
|
|
||||||
AlwaysPresent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -115,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The connecting path is excluded from positional input
|
// The connecting path is excluded from positional input
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
@ -209,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (IsHovered || IsSelected.Value)
|
if (IsHovered || IsSelected.Value)
|
||||||
colour = colour.Lighten(1);
|
colour = colour.Lighten(1);
|
||||||
|
|
||||||
marker.Colour = colour;
|
Colour = colour;
|
||||||
marker.Scale = new Vector2(hitObject.Scale);
|
Scale = new Vector2(hitObject.Scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 getColourFromNodeType()
|
private Color4 getColourFromNodeType()
|
||||||
|
@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
|
||||||
|
|
||||||
internal readonly Container<PathControlPointPiece<T>> Pieces;
|
internal readonly Container<PathControlPointPiece<T>> Pieces;
|
||||||
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
|
|
||||||
|
|
||||||
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
||||||
private readonly T hitObject;
|
private readonly T hitObject;
|
||||||
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both },
|
new PathControlPointConnection<T>(hitObject),
|
||||||
Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
|
Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
controlPoints.BindTo(hitObject.Path.ControlPoints);
|
controlPoints.BindTo(hitObject.Path.ControlPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generally all the control points are within the visible area all the time.
|
||||||
|
public override bool UpdateSubTreeMasking() => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles correction of invalid path types.
|
/// Handles correction of invalid path types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -185,17 +187,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
Debug.Assert(e.NewItems != null);
|
Debug.Assert(e.NewItems != null);
|
||||||
|
|
||||||
// If inserting in the path (not appending),
|
|
||||||
// update indices of existing connections after insert location
|
|
||||||
if (e.NewStartingIndex < Pieces.Count)
|
|
||||||
{
|
|
||||||
foreach (var connection in Connections)
|
|
||||||
{
|
|
||||||
if (connection.ControlPointIndex >= e.NewStartingIndex)
|
|
||||||
connection.ControlPointIndex += e.NewItems.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < e.NewItems.Count; i++)
|
for (int i = 0; i < e.NewItems.Count; i++)
|
||||||
{
|
{
|
||||||
var point = (PathControlPoint)e.NewItems[i];
|
var point = (PathControlPoint)e.NewItems[i];
|
||||||
@ -209,8 +200,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
d.DragInProgress = DragInProgress;
|
d.DragInProgress = DragInProgress;
|
||||||
d.DragEnded = DragEnded;
|
d.DragEnded = DragEnded;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -222,19 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
{
|
{
|
||||||
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
||||||
piece.RemoveAndDisposeImmediately();
|
piece.RemoveAndDisposeImmediately();
|
||||||
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
|
|
||||||
connection.RemoveAndDisposeImmediately();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If removing before the end of the path,
|
|
||||||
// update indices of connections after remove location
|
|
||||||
if (e.OldStartingIndex < Pieces.Count)
|
|
||||||
{
|
|
||||||
foreach (var connection in Connections)
|
|
||||||
{
|
|
||||||
if (connection.ControlPointIndex >= e.OldStartingIndex)
|
|
||||||
connection.ControlPointIndex -= e.OldItems.Count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -459,10 +435,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
||||||
{
|
{
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||||
updatePathType(p, type);
|
updatePathType(p, type);
|
||||||
|
|
||||||
EnsureValidPathTypes();
|
EnsureValidPathTypes();
|
||||||
|
|
||||||
|
changeHandler?.EndChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (countOfState == totalCount)
|
if (countOfState == totalCount)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
public SliderBodyPiece()
|
public SliderBodyPiece()
|
||||||
{
|
{
|
||||||
InternalChild = body = new ManualSliderBody
|
AutoSizeAxes = Axes.Both;
|
||||||
{
|
|
||||||
AccentColour = Color4.Transparent
|
|
||||||
};
|
|
||||||
|
|
||||||
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
|
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
|
||||||
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
|
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
|
||||||
AlwaysPresent = true;
|
AlwaysPresent = true;
|
||||||
|
|
||||||
|
InternalChild = body = new ManualSliderBody
|
||||||
|
{
|
||||||
|
AccentColour = Color4.Transparent
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
body.SetVertices(vertices);
|
body.SetVertices(vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
Size = body.Size;
|
|
||||||
OriginPosition = body.PathOffset;
|
OriginPosition = body.PathOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,7 +403,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
||||||
{
|
{
|
||||||
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
|
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
|
||||||
|
{
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
addControlPoint(rightClickPosition);
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
}),
|
||||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,7 +101,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
RightToolbox.AddRange(new EditorToolboxGroup[]
|
RightToolbox.AddRange(new EditorToolboxGroup[]
|
||||||
{
|
{
|
||||||
new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, },
|
new TransformToolboxGroup
|
||||||
|
{
|
||||||
|
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||||
|
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||||
|
},
|
||||||
FreehandlSliderToolboxGroup
|
FreehandlSliderToolboxGroup
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||||
{
|
{
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
|
||||||
/// can be maintained throughout the operation.
|
|
||||||
/// </summary>
|
|
||||||
private List<PathType?>? referencePathTypes;
|
|
||||||
|
|
||||||
protected override void OnSelectionChanged()
|
protected override void OnSelectionChanged()
|
||||||
{
|
{
|
||||||
base.OnSelectionChanged();
|
base.OnSelectionChanged();
|
||||||
|
|
||||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||||
|
|
||||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
SelectionBox.CanFlipX = quad.Width > 0;
|
||||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
SelectionBox.CanFlipY = quad.Height > 0;
|
||||||
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
|
|
||||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnOperationEnded()
|
|
||||||
{
|
|
||||||
base.OnOperationEnded();
|
|
||||||
referencePathTypes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
||||||
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return didFlip;
|
return didFlip;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleScale(Vector2 scale, Anchor reference)
|
|
||||||
{
|
|
||||||
adjustScaleFromAnchor(ref scale, reference);
|
|
||||||
|
|
||||||
var hitObjects = selectedMovableObjects;
|
|
||||||
|
|
||||||
// for the time being, allow resizing of slider paths only if the slider is
|
|
||||||
// the only hit object selected. with a group selection, it's likely the user
|
|
||||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
|
||||||
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
|
||||||
scaleSlider(slider, scale);
|
|
||||||
else
|
|
||||||
scaleHitObjects(hitObjects, reference, scale);
|
|
||||||
|
|
||||||
moveSelectionInBounds();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
|
||||||
{
|
|
||||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
|
||||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
|
||||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
|
||||||
|
|
||||||
// reverse the scale direction if dragging from top or left.
|
|
||||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
|
||||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||||
|
|
||||||
private void scaleSlider(Slider slider, Vector2 scale)
|
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
|
||||||
{
|
|
||||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
|
||||||
|
|
||||||
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
|
||||||
|
|
||||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
|
||||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
|
||||||
|
|
||||||
Vector2 pathRelativeDeltaScale = new Vector2(
|
|
||||||
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
|
|
||||||
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
|
|
||||||
|
|
||||||
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
|
|
||||||
|
|
||||||
foreach (var point in slider.Path.ControlPoints)
|
|
||||||
{
|
|
||||||
oldControlPoints.Enqueue(point.Position);
|
|
||||||
point.Position *= pathRelativeDeltaScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
|
||||||
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
|
||||||
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
|
|
||||||
|
|
||||||
// Snap the slider's length to the current beat divisor
|
|
||||||
// to calculate the final resulting duration / bounding box before the final checks.
|
|
||||||
slider.SnapTo(snapProvider);
|
|
||||||
|
|
||||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
|
||||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
|
||||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
|
||||||
|
|
||||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var point in slider.Path.ControlPoints)
|
|
||||||
point.Position = oldControlPoints.Dequeue();
|
|
||||||
|
|
||||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
|
||||||
slider.SnapTo(snapProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
|
||||||
{
|
|
||||||
scale = getClampedScale(hitObjects, reference, scale);
|
|
||||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
|
||||||
|
|
||||||
foreach (var h in hitObjects)
|
|
||||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
|
||||||
{
|
|
||||||
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
|
|
||||||
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
|
|
||||||
|
|
||||||
return (xInBounds, yInBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
{
|
{
|
||||||
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
h.Position += delta;
|
h.Position += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObjects">The hitobjects to be scaled</param>
|
|
||||||
/// <param name="reference">The anchor from which the scale operation is performed</param>
|
|
||||||
/// <param name="scale">The scale to be clamped</param>
|
|
||||||
/// <returns>The clamped scale vector</returns>
|
|
||||||
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
|
||||||
{
|
|
||||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
|
||||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
|
||||||
|
|
||||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
|
||||||
|
|
||||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
|
||||||
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
|
||||||
|
|
||||||
//max Size -> playfield bounds
|
|
||||||
if (scaledQuad.TopLeft.X < 0)
|
|
||||||
scale.X += scaledQuad.TopLeft.X;
|
|
||||||
if (scaledQuad.TopLeft.Y < 0)
|
|
||||||
scale.Y += scaledQuad.TopLeft.Y;
|
|
||||||
|
|
||||||
if (scaledQuad.BottomRight.X > DrawWidth)
|
|
||||||
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
|
|
||||||
if (scaledQuad.BottomRight.Y > DrawHeight)
|
|
||||||
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
|
|
||||||
|
|
||||||
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
|
|
||||||
Vector2 scaledSize = selectionQuad.Size + scale;
|
|
||||||
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
|
|
||||||
|
|
||||||
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
|
|
||||||
|
|
||||||
return scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||||
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
CanRotateAroundSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
||||||
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
|
CanRotateAroundPlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private OsuHitObject[]? objectsInRotation;
|
private OsuHitObject[]? objectsInRotation;
|
||||||
|
241
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether scaling anchored by the center of the playfield can currently be performed.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<bool> CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a single slider is currently selected, which results in a different scaling behaviour.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||||
|
|
||||||
|
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
|
{
|
||||||
|
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||||
|
|
||||||
|
CanScaleX.Value = quad.Width > 0;
|
||||||
|
CanScaleY.Value = quad.Height > 0;
|
||||||
|
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
|
||||||
|
CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||||
|
IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||||
|
private Vector2? defaultOrigin;
|
||||||
|
|
||||||
|
public override void Begin()
|
||||||
|
{
|
||||||
|
if (objectsInScale != null)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||||
|
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
|
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||||
|
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||||
|
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||||
|
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||||
|
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||||
|
{
|
||||||
|
if (objectsInScale == null)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
|
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
|
|
||||||
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
|
|
||||||
|
// for the time being, allow resizing of slider paths only if the slider is
|
||||||
|
// the only hit object selected. with a group selection, it's likely the user
|
||||||
|
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||||
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
|
{
|
||||||
|
var originalInfo = objectsInScale[slider];
|
||||||
|
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||||
|
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
|
||||||
|
|
||||||
|
foreach (var (ho, originalState) in objectsInScale)
|
||||||
|
{
|
||||||
|
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSelectionInBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Commit()
|
||||||
|
{
|
||||||
|
if (objectsInScale == null)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
|
||||||
|
objectsInScale = null;
|
||||||
|
OriginalSurroundingQuad = null;
|
||||||
|
defaultOrigin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||||
|
.Where(h => h is not Spinner);
|
||||||
|
|
||||||
|
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
||||||
|
{
|
||||||
|
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||||
|
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||||
|
{
|
||||||
|
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
||||||
|
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the slider's length to the current beat divisor
|
||||||
|
// to calculate the final resulting duration / bounding box before the final checks.
|
||||||
|
slider.SnapTo(snapProvider);
|
||||||
|
|
||||||
|
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||||
|
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||||
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||||
|
|
||||||
|
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||||
|
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
||||||
|
|
||||||
|
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||||
|
slider.SnapTo(snapProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||||
|
{
|
||||||
|
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
|
||||||
|
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
|
||||||
|
|
||||||
|
return (xInBounds, yInBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||||
|
/// <param name="scale">The scale to be clamped</param>
|
||||||
|
/// <returns>The clamped scale vector</returns>
|
||||||
|
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
|
||||||
|
{
|
||||||
|
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||||
|
if (objectsInScale == null)
|
||||||
|
return scale;
|
||||||
|
|
||||||
|
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
|
|
||||||
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
|
origin = slider.Position;
|
||||||
|
|
||||||
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
|
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||||
|
|
||||||
|
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||||
|
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||||
|
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||||
|
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||||
|
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
|
||||||
|
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
|
||||||
|
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
|
||||||
|
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
|
||||||
|
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
||||||
|
|
||||||
|
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveSelectionInBounds()
|
||||||
|
{
|
||||||
|
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
|
||||||
|
|
||||||
|
Vector2 delta = Vector2.Zero;
|
||||||
|
|
||||||
|
if (quad.TopLeft.X < 0)
|
||||||
|
delta.X -= quad.TopLeft.X;
|
||||||
|
if (quad.TopLeft.Y < 0)
|
||||||
|
delta.Y -= quad.TopLeft.Y;
|
||||||
|
|
||||||
|
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
|
||||||
|
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
|
||||||
|
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||||
|
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
|
||||||
|
|
||||||
|
foreach (var (h, _) in objectsInScale!)
|
||||||
|
h.Position += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct OriginalHitObjectState
|
||||||
|
{
|
||||||
|
public Vector2 Position { get; }
|
||||||
|
public Vector2[]? PathControlPointPositions { get; }
|
||||||
|
public PathType?[]? PathControlPointTypes { get; }
|
||||||
|
|
||||||
|
public OriginalHitObjectState(OsuHitObject hitObject)
|
||||||
|
{
|
||||||
|
Position = hitObject.Position;
|
||||||
|
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
|
||||||
|
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,11 +78,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
angleInput.TakeFocus();
|
||||||
|
angleInput.SelectAll();
|
||||||
|
});
|
||||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||||
rotationOrigin.Items.First().Select();
|
rotationOrigin.Items.First().Select();
|
||||||
|
|
||||||
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
|
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||||
{
|
{
|
||||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
212
osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PreciseScalePopover : OsuPopover
|
||||||
|
{
|
||||||
|
private readonly OsuSelectionScaleHandler scaleHandler;
|
||||||
|
|
||||||
|
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
|
||||||
|
|
||||||
|
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||||
|
private BindableNumber<float> scaleInputBindable = null!;
|
||||||
|
private EditorRadioButtonCollection scaleOrigin = null!;
|
||||||
|
|
||||||
|
private RadioButton playfieldCentreButton = null!;
|
||||||
|
private RadioButton selectionCentreButton = null!;
|
||||||
|
|
||||||
|
private OsuCheckbox xCheckBox = null!;
|
||||||
|
private OsuCheckbox yCheckBox = null!;
|
||||||
|
|
||||||
|
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
|
||||||
|
{
|
||||||
|
this.scaleHandler = scaleHandler;
|
||||||
|
|
||||||
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 220,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
scaleInput = new SliderWithTextBoxInput<float>("Scale:")
|
||||||
|
{
|
||||||
|
Current = scaleInputBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
MinValue = 0.5f,
|
||||||
|
MaxValue = 2,
|
||||||
|
Precision = 0.001f,
|
||||||
|
Value = 1,
|
||||||
|
Default = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
scaleOrigin = new EditorRadioButtonCollection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Items = new[]
|
||||||
|
{
|
||||||
|
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||||
|
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
|
selectionCentreButton = new RadioButton("Selection centre",
|
||||||
|
() => setOrigin(ScaleOrigin.SelectionCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(4),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
xCheckBox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "X-axis",
|
||||||
|
Current = { Value = true },
|
||||||
|
},
|
||||||
|
yCheckBox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "Y-axis",
|
||||||
|
Current = { Value = true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
||||||
|
};
|
||||||
|
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
selectionCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to its centre." : string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
scaleInput.TakeFocus();
|
||||||
|
scaleInput.SelectAll();
|
||||||
|
});
|
||||||
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
|
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
||||||
|
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
||||||
|
|
||||||
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
|
|
||||||
|
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||||
|
|
||||||
|
scaleInfo.BindValueChanged(scale =>
|
||||||
|
{
|
||||||
|
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
|
||||||
|
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAxisCheckBoxesEnabled()
|
||||||
|
{
|
||||||
|
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
|
||||||
|
{
|
||||||
|
toggleAxisAvailable(xCheckBox.Current, true);
|
||||||
|
toggleAxisAvailable(yCheckBox.Current, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toggleAxisAvailable(xCheckBox.Current, scaleHandler.CanScaleX.Value);
|
||||||
|
toggleAxisAvailable(yCheckBox.Current, scaleHandler.CanScaleY.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleAxisAvailable(Bindable<bool> axisBindable, bool available)
|
||||||
|
{
|
||||||
|
// enable the bindable to allow setting the value
|
||||||
|
axisBindable.Disabled = false;
|
||||||
|
// restore the presumed default value given the axis's new availability state
|
||||||
|
axisBindable.Value = available;
|
||||||
|
axisBindable.Disabled = !available;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMaxScale()
|
||||||
|
{
|
||||||
|
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float max_scale = 10;
|
||||||
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
|
||||||
|
|
||||||
|
if (!scaleInfo.Value.XAxis)
|
||||||
|
scale.X = max_scale;
|
||||||
|
if (!scaleInfo.Value.YAxis)
|
||||||
|
scale.Y = max_scale;
|
||||||
|
|
||||||
|
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setOrigin(ScaleOrigin origin)
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||||
|
updateMaxScale();
|
||||||
|
updateAxisCheckBoxesEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
|
||||||
|
|
||||||
|
private void setAxis(bool x, bool y)
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
||||||
|
updateMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
scaleHandler.Begin();
|
||||||
|
updateMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
if (IsLoaded) scaleHandler.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ScaleOrigin
|
||||||
|
{
|
||||||
|
PlayfieldCentre,
|
||||||
|
SelectionCentre
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis);
|
||||||
|
}
|
@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
private readonly Bindable<bool> canRotate = new BindableBool();
|
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
|
||||||
private EditorToolButton rotateButton = null!;
|
private EditorToolButton rotateButton = null!;
|
||||||
|
private EditorToolButton scaleButton = null!;
|
||||||
private Bindable<bool> canRotatePlayfieldOrigin = null!;
|
|
||||||
private Bindable<bool> canRotateSelectionOrigin = null!;
|
|
||||||
|
|
||||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||||
|
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
||||||
|
|
||||||
public TransformToolboxGroup()
|
public TransformToolboxGroup()
|
||||||
: base("transform")
|
: base("transform")
|
||||||
@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
rotateButton = new EditorToolButton("Rotate",
|
rotateButton = new EditorToolButton("Rotate",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||||
() => new PreciseRotationPopover(RotationHandler)),
|
() => new PreciseRotationPopover(RotationHandler)),
|
||||||
// TODO: scale
|
scaleButton = new EditorToolButton("Scale",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
|
() => new PreciseScalePopover(ScaleHandler))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -54,21 +56,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
// aggregate two values into canRotate
|
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||||
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
|
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
|
||||||
|
|
||||||
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
|
canScale.AddSource(ScaleHandler.CanScaleX);
|
||||||
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
canScale.AddSource(ScaleHandler.CanScaleY);
|
||||||
|
canScale.AddSource(ScaleHandler.CanScaleFromPlayfieldOrigin);
|
||||||
void updateCanRotateAggregate()
|
|
||||||
{
|
|
||||||
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||||
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
|
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||||
|
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
@ -82,6 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
rotateButton.TriggerClick();
|
rotateButton.TriggerClick();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case GlobalAction.EditorToggleScaleControl:
|
||||||
|
{
|
||||||
|
scaleButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "Put your faith in the approach circles...";
|
public override LocalisableString Description => "Put your faith in the approach circles...";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
||||||
|
|
||||||
|
@ -10,9 +10,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -63,22 +61,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
base.ApplyTransformsAt(time, false);
|
base.ApplyTransformsAt(time, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2? lastPosition;
|
|
||||||
|
|
||||||
public void UpdateProgress(double completionProgress)
|
public void UpdateProgress(double completionProgress)
|
||||||
{
|
{
|
||||||
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
Slider slider = drawableSlider.HitObject;
|
||||||
|
Position = slider.CurvePositionAt(completionProgress);
|
||||||
|
|
||||||
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
||||||
|
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
|
||||||
bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true;
|
|
||||||
|
|
||||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||||
|
// Needed for when near completion, or in case of a very short slider.
|
||||||
if (diff.LengthFast < 0.01f)
|
if (diff.LengthFast < 0.01f)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0);
|
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
|
||||||
lastPosition = Position;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
//
|
//
|
||||||
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
|
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
|
||||||
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
|
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
|
||||||
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition));
|
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.CanBeHit() && c.ReceivePositionalInputAt(screenSpacePosition));
|
||||||
|
|
||||||
public OsuInputManager(RulesetInfo ruleset)
|
public OsuInputManager(RulesetInfo ruleset)
|
||||||
: base(ruleset, 0, SimultaneousBindingMode.Unique)
|
: base(ruleset, 0, SimultaneousBindingMode.Unique)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ManualSliderBody : SliderBody
|
public partial class ManualSliderBody : SliderBody
|
||||||
{
|
{
|
||||||
public new void SetVertices(IReadOnlyList<Vector2> vertices)
|
public ManualSliderBody()
|
||||||
{
|
{
|
||||||
base.SetVertices(vertices);
|
AutoSizeAxes = Axes.Both;
|
||||||
Size = Path.Size;
|
}
|
||||||
}
|
|
||||||
|
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private readonly ISkin skin;
|
private readonly ISkin skin;
|
||||||
private const double disjoint_trail_time_separation = 1000 / 60.0;
|
private const double disjoint_trail_time_separation = 1000 / 60.0;
|
||||||
|
|
||||||
private bool disjointTrail;
|
public bool DisjointTrail { get; private set; }
|
||||||
private double lastTrailTime;
|
private double lastTrailTime;
|
||||||
|
|
||||||
private IBindable<float> cursorSize = null!;
|
private IBindable<float> cursorSize = null!;
|
||||||
@ -31,14 +31,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||||
{
|
{
|
||||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||||
|
|
||||||
Texture = skin.GetTexture("cursortrail");
|
Texture = skin.GetTexture("cursortrail");
|
||||||
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
|
||||||
|
|
||||||
if (disjointTrail)
|
// Cursor and cursor trail components are sourced from potentially different skin sources.
|
||||||
|
// Stable always chooses cursor trail disjoint behaviour based on the cursor texture lookup source, so we need to fetch where that occurred.
|
||||||
|
// See https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/SkinManager.cs#L269
|
||||||
|
var cursorProvider = skinSource.FindProvider(s => s.GetTexture("cursor") != null);
|
||||||
|
DisjointTrail = cursorProvider?.GetTexture("cursormiddle") == null;
|
||||||
|
|
||||||
|
if (DisjointTrail)
|
||||||
{
|
{
|
||||||
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
|
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
|
||||||
|
|
||||||
@ -57,19 +62,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
protected override double FadeDuration => DisjointTrail ? 150 : 500;
|
||||||
protected override float FadeExponent => 1;
|
protected override float FadeExponent => 1;
|
||||||
|
|
||||||
protected override bool InterpolateMovements => !disjointTrail;
|
protected override bool InterpolateMovements => !DisjointTrail;
|
||||||
|
|
||||||
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
|
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
|
||||||
protected override bool AvoidDrawingNearCursor => !disjointTrail;
|
protected override bool AvoidDrawingNearCursor => !DisjointTrail;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (!disjointTrail || !currentPosition.HasValue)
|
if (!DisjointTrail || !currentPosition.HasValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
|
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
|
||||||
@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
{
|
{
|
||||||
if (!disjointTrail)
|
if (!DisjointTrail)
|
||||||
return base.OnMouseMove(e);
|
return base.OnMouseMove(e);
|
||||||
|
|
||||||
currentPosition = e.ScreenSpaceMousePosition;
|
currentPosition = e.ScreenSpaceMousePosition;
|
||||||
|
@ -8,7 +8,6 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Primitives;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
// For osu! gameplay, everything is always on screen.
|
// For osu! gameplay, everything is always on screen.
|
||||||
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
|
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
|
||||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
|
public override bool UpdateSubTreeMasking() => false;
|
||||||
|
|
||||||
public SmokeContainer Smoke { get; }
|
public SmokeContainer Smoke { get; }
|
||||||
public FollowPointRenderer FollowPoints { get; }
|
public FollowPointRenderer FollowPoints { get; }
|
||||||
|
@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double ComputeTimeRange()
|
|
||||||
{
|
|
||||||
// Adjust when we're using constant algorithm to not be sluggish.
|
|
||||||
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
|
|
||||||
return base.ComputeTimeRange() / multiplier;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
|
{
|
||||||
|
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
|
||||||
|
{
|
||||||
|
public override string Name => "Constant Speed";
|
||||||
|
public override string Acronym => "CS";
|
||||||
|
public override double ScoreMultiplier => 0.9;
|
||||||
|
public override LocalisableString Description => "No more tricky speed changes!";
|
||||||
|
public override IconUsage? Icon => FontAwesome.Solid.Equals;
|
||||||
|
public override ModType Type => ModType.Conversion;
|
||||||
|
|
||||||
|
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||||
|
{
|
||||||
|
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||||
|
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
Scale = new Vector2(0.7f),
|
Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
|
||||||
Colour = new Colour4(255, 228, 0, 255),
|
Colour = new Colour4(255, 228, 0, 255),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
if (!result.IsHit || !isKiaiActive)
|
if (!result.IsHit || !isKiaiActive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
sprite.ScaleTo(0.85f).Then()
|
sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
|
||||||
.ScaleTo(0.7f, 80, Easing.OutQuad);
|
.ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class TaikoLegacyHitTarget : CompositeDrawable
|
public partial class TaikoLegacyHitTarget : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592)
|
||||||
|
/// but for whatever reason this doesn't match visually.
|
||||||
|
/// </summary>
|
||||||
|
public const float SCALE = 0.8f;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin)
|
private void load(ISkinSource skin)
|
||||||
{
|
{
|
||||||
@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("approachcircle"),
|
Texture = skin.GetTexture("approachcircle"),
|
||||||
Scale = new Vector2(0.83f),
|
Scale = new Vector2(SCALE + 0.03f),
|
||||||
Alpha = 0.47f, // eyeballed to match stable
|
Alpha = 0.47f, // eyeballed to match stable
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("taikobigcircle"),
|
Texture = skin.GetTexture("taikobigcircle"),
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(SCALE),
|
||||||
Alpha = 0.22f, // eyeballed to match stable
|
Alpha = 0.22f, // eyeballed to match stable
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
new TaikoModClassic(),
|
new TaikoModClassic(),
|
||||||
new TaikoModSwap(),
|
new TaikoModSwap(),
|
||||||
new TaikoModSingleTap(),
|
new TaikoModSingleTap(),
|
||||||
|
new TaikoModConstantSpeed(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Automation:
|
case ModType.Automation:
|
||||||
|
@ -82,7 +82,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
TimeRange.Value = ComputeTimeRange();
|
TimeRange.Value = ComputeTimeRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange();
|
protected virtual double ComputeTimeRange()
|
||||||
|
{
|
||||||
|
// Adjust when we're using constant algorithm to not be sluggish.
|
||||||
|
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
|
||||||
|
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,6 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Primitives;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -345,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
public void Add(Drawable proxy) => AddInternal(proxy);
|
public void Add(Drawable proxy) => AddInternal(proxy);
|
||||||
|
|
||||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
|
public override bool UpdateSubTreeMasking()
|
||||||
{
|
{
|
||||||
// DrawableHitObject disables masking.
|
// DrawableHitObject disables masking.
|
||||||
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.
|
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.
|
||||||
|
@ -1188,5 +1188,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
|
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBeatmapDifficultyIsClamped()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var decoded = decoder.Decode(stream).Difficulty;
|
||||||
|
Assert.That(decoded.DrainRate, Is.EqualTo(10));
|
||||||
|
Assert.That(decoded.CircleSize, Is.EqualTo(10));
|
||||||
|
Assert.That(decoded.OverallDifficulty, Is.EqualTo(10));
|
||||||
|
Assert.That(decoded.ApproachRate, Is.EqualTo(10));
|
||||||
|
Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6));
|
||||||
|
Assert.That(decoded.SliderTickRate, Is.EqualTo(8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestManiaBeatmapDifficultyCircleSizeClamp()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var decoded = decoder.Decode(stream).Difficulty;
|
||||||
|
Assert.That(decoded.CircleSize, Is.EqualTo(14));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu;
|
|||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Taiko;
|
using osu.Game.Rulesets.Taiko;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -37,6 +38,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
|
|
||||||
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
|
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUnsupportedStoryboardEvents()
|
||||||
|
{
|
||||||
|
const string name = "Resources/storyboard_only_video.osu";
|
||||||
|
|
||||||
|
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
|
||||||
|
Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\""));
|
||||||
|
|
||||||
|
var memoryStream = encodeToLegacy(decoded);
|
||||||
|
|
||||||
|
var storyboard = new LegacyStoryboardDecoder().Decode(new LineBufferedReader(memoryStream));
|
||||||
|
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
|
||||||
|
Assert.That(video.Elements.Count, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
[TestCaseSource(nameof(allBeatmaps))]
|
[TestCaseSource(nameof(allBeatmaps))]
|
||||||
public void TestEncodeDecodeStability(string name)
|
public void TestEncodeDecodeStability(string name)
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
using osu.Game.IO.Legacy;
|
using osu.Game.IO.Legacy;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
@ -31,6 +32,8 @@ using osu.Game.Rulesets.Taiko;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Beatmaps.Formats
|
namespace osu.Game.Tests.Beatmaps.Formats
|
||||||
{
|
{
|
||||||
@ -124,17 +127,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
|
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
|
||||||
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
|
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
|
||||||
{
|
{
|
||||||
const double first_frame_time = 48;
|
const double first_frame_time = 31;
|
||||||
const double second_frame_time = 65;
|
const double second_frame_time = 48;
|
||||||
|
const double third_frame_time = 65;
|
||||||
|
|
||||||
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
|
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
|
||||||
|
|
||||||
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||||
{
|
{
|
||||||
var score = decoder.Parse(resourceStream);
|
var score = decoder.Parse(resourceStream);
|
||||||
|
int offset = offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
|
||||||
|
|
||||||
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
|
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + offset));
|
||||||
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
|
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + offset));
|
||||||
|
Assert.That(score.Replay.Frames[2].Time, Is.EqualTo(third_frame_time + offset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +181,94 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
|
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNegativeFrameSkipped()
|
||||||
|
{
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
|
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = scoreInfo,
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(0, new Vector2()),
|
||||||
|
new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE),
|
||||||
|
new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2),
|
||||||
|
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(1000));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void FirstTwoFramesSwappedIfInWrongOrder()
|
||||||
|
{
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
|
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = scoreInfo,
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(100, new Vector2()),
|
||||||
|
new OsuReplayFrame(50, OsuPlayfield.BASE_SIZE / 2),
|
||||||
|
new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(100));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void FirstTwoFramesPulledTowardThirdIfTheyAreAfterIt()
|
||||||
|
{
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
|
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = scoreInfo,
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(0, new Vector2()),
|
||||||
|
new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2),
|
||||||
|
new OsuReplayFrame(-1500, OsuPlayfield.BASE_SIZE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(-1500));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(-1500));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(-1500));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCultureInvariance()
|
public void TestCultureInvariance()
|
||||||
{
|
{
|
||||||
@ -224,6 +318,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
|
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
|
||||||
};
|
};
|
||||||
scoreInfo.OnlineID = 123123;
|
scoreInfo.OnlineID = 123123;
|
||||||
|
scoreInfo.User = new APIUser
|
||||||
|
{
|
||||||
|
Username = "spaceman_atlas",
|
||||||
|
Id = 3035836,
|
||||||
|
CountryCode = CountryCode.PL
|
||||||
|
};
|
||||||
scoreInfo.ClientVersion = "2023.1221.0";
|
scoreInfo.ClientVersion = "2023.1221.0";
|
||||||
|
|
||||||
var beatmap = new TestBeatmap(ruleset);
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
@ -248,6 +348,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
|
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
|
||||||
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
|
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
|
||||||
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
|
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +453,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
[HitResult.Great] = 200,
|
[HitResult.Great] = 200,
|
||||||
[HitResult.LargeTickHit] = 1,
|
[HitResult.LargeTickHit] = 1,
|
||||||
};
|
};
|
||||||
|
scoreInfo.Rank = ScoreRank.A;
|
||||||
|
|
||||||
var beatmap = new TestBeatmap(ruleset);
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
var score = new Score
|
var score = new Score
|
||||||
@ -412,6 +514,80 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTotalScoreWithoutModsReadIfPresent()
|
||||||
|
{
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
scoreInfo.Mods = new Mod[]
|
||||||
|
{
|
||||||
|
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
|
||||||
|
};
|
||||||
|
scoreInfo.OnlineID = 123123;
|
||||||
|
scoreInfo.ClientVersion = "2023.1221.0";
|
||||||
|
scoreInfo.TotalScoreWithoutMods = 1_000_000;
|
||||||
|
scoreInfo.TotalScore = 1_020_000;
|
||||||
|
|
||||||
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = scoreInfo,
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing()
|
||||||
|
{
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
scoreInfo.Mods = new Mod[]
|
||||||
|
{
|
||||||
|
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
|
||||||
|
};
|
||||||
|
scoreInfo.OnlineID = 123123;
|
||||||
|
scoreInfo.ClientVersion = "2023.1221.0";
|
||||||
|
scoreInfo.TotalScoreWithoutMods = 0;
|
||||||
|
scoreInfo.TotalScore = 1_020_000;
|
||||||
|
|
||||||
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = scoreInfo,
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||||
{
|
{
|
||||||
var encodeStream = new MemoryStream();
|
var encodeStream = new MemoryStream();
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
[TestCase(1, 3)]
|
[TestCase(1, 3)]
|
||||||
[TestCase(1, 0)]
|
[TestCase(1, 0)]
|
||||||
[TestCase(0, 3)]
|
[TestCase(0, 3)]
|
||||||
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
|
public void TestCatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
|
||||||
{
|
{
|
||||||
var ruleset = new CatchRuleset().RulesetInfo;
|
var ruleset = new CatchRuleset().RulesetInfo;
|
||||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
@ -41,7 +41,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ScoreWithMissIsNotPerfect()
|
public void TestFailPreserved()
|
||||||
|
{
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo();
|
||||||
|
var beatmap = new TestBeatmap(ruleset);
|
||||||
|
|
||||||
|
scoreInfo.Rank = ScoreRank.F;
|
||||||
|
|
||||||
|
var score = new Score { ScoreInfo = scoreInfo };
|
||||||
|
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.F));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreWithMissIsNotPerfect()
|
||||||
{
|
{
|
||||||
var ruleset = new OsuRuleset().RulesetInfo;
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
128
osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using ManagedBass;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK.Audio;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editing.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CheckHitsoundsFormatTest
|
||||||
|
{
|
||||||
|
private CheckHitsoundsFormat check = null!;
|
||||||
|
|
||||||
|
private IBeatmap beatmap = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckHitsoundsFormat();
|
||||||
|
beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
BeatmapSet = new BeatmapSetInfo
|
||||||
|
{
|
||||||
|
Files = { CheckTestHelpers.CreateMockFile("wav") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0 = No output device. This still allows decoding.
|
||||||
|
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
|
||||||
|
throw new AudioException("Could not initialize Bass.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMp3Audio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
|
||||||
|
{
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateIncorrectFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOggAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
|
||||||
|
{
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWavAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
|
||||||
|
{
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWebmAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
|
||||||
|
{
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotAnAudioFile()
|
||||||
|
{
|
||||||
|
beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
BeatmapSet = new BeatmapSetInfo
|
||||||
|
{
|
||||||
|
Files = { CheckTestHelpers.CreateMockFile("png") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Textures/test-image.png"))
|
||||||
|
{
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCorruptAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
|
||||||
|
{
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BeatmapVerifierContext getContext(Stream? resourceStream)
|
||||||
|
{
|
||||||
|
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
|
||||||
|
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
|
||||||
|
|
||||||
|
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using ManagedBass;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK.Audio;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editing.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class CheckSongFormatTest
|
||||||
|
{
|
||||||
|
private CheckSongFormat check = null!;
|
||||||
|
|
||||||
|
private IBeatmap beatmap = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckSongFormat();
|
||||||
|
beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
BeatmapSet = new BeatmapSetInfo
|
||||||
|
{
|
||||||
|
Files = { CheckTestHelpers.CreateMockFile("mp3") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0 = No output device. This still allows decoding.
|
||||||
|
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
|
||||||
|
throw new AudioException("Could not initialize Bass.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMp3Audio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
|
||||||
|
{
|
||||||
|
beatmap.Metadata.AudioFile = "abc123.mp3";
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOggAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
|
||||||
|
{
|
||||||
|
beatmap.Metadata.AudioFile = "abc123.mp3";
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWavAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
|
||||||
|
{
|
||||||
|
beatmap.Metadata.AudioFile = "abc123.mp3";
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateIncorrectFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWebmAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
|
||||||
|
{
|
||||||
|
beatmap.Metadata.AudioFile = "abc123.mp3";
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCorruptAudio()
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
|
||||||
|
{
|
||||||
|
beatmap.Metadata.AudioFile = "abc123.mp3";
|
||||||
|
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BeatmapVerifierContext getContext(Stream? resourceStream)
|
||||||
|
{
|
||||||
|
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
|
||||||
|
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
|
||||||
|
|
||||||
|
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -95,18 +95,6 @@ namespace osu.Game.Tests.Editing.Checks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestCorruptAudioFile()
|
|
||||||
{
|
|
||||||
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
|
|
||||||
{
|
|
||||||
var issues = check.Run(getContext(resourceStream)).ToList();
|
|
||||||
|
|
||||||
Assert.That(issues, Has.Count.EqualTo(1));
|
|
||||||
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BeatmapVerifierContext getContext(Stream? resourceStream)
|
private BeatmapVerifierContext getContext(Stream? resourceStream)
|
||||||
{
|
{
|
||||||
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
|
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
|
||||||
|
235
osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneTimedDifficultyCalculation
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestAttributesGeneratedForAllNonSkippedObjects()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<TestHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new TestHitObject { StartTime = 1 },
|
||||||
|
new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = 2,
|
||||||
|
Nested = 1
|
||||||
|
},
|
||||||
|
new TestHitObject { StartTime = 3 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
|
||||||
|
|
||||||
|
Assert.That(attribs.Count, Is.EqualTo(4));
|
||||||
|
assertEquals(attribs[0], beatmap.HitObjects[0]);
|
||||||
|
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
|
||||||
|
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object.
|
||||||
|
assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAttributesNotGeneratedForSkippedObjects()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<TestHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
// The first object is usually skipped in all implementations
|
||||||
|
new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = 1,
|
||||||
|
Skip = true
|
||||||
|
},
|
||||||
|
// An intermediate skipped object.
|
||||||
|
new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = 2,
|
||||||
|
Skip = true
|
||||||
|
},
|
||||||
|
new TestHitObject { StartTime = 3 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
|
||||||
|
|
||||||
|
Assert.That(attribs.Count, Is.EqualTo(1));
|
||||||
|
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNestedObjectOnlyAddsParentOnce()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<TestHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = 1,
|
||||||
|
Skip = true,
|
||||||
|
Nested = 2
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
|
||||||
|
|
||||||
|
Assert.That(attribs.Count, Is.EqualTo(2));
|
||||||
|
assertEquals(attribs[0], beatmap.HitObjects[0]);
|
||||||
|
assertEquals(attribs[1], beatmap.HitObjects[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSkippedLastObjectAddedInLastIteration()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<TestHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new TestHitObject { StartTime = 1 },
|
||||||
|
new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = 2,
|
||||||
|
Skip = true
|
||||||
|
},
|
||||||
|
new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = 3,
|
||||||
|
Skip = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
|
||||||
|
|
||||||
|
Assert.That(attribs.Count, Is.EqualTo(1));
|
||||||
|
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected)
|
||||||
|
{
|
||||||
|
Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestHitObject : HitObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to skip generating a difficulty representation for this object.
|
||||||
|
/// </summary>
|
||||||
|
public bool Skip { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to generate nested difficulty representations for this object, and if so, how many.
|
||||||
|
/// </summary>
|
||||||
|
public int Nested { get; set; }
|
||||||
|
|
||||||
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Nested; i++)
|
||||||
|
AddNested(new TestHitObject { StartTime = StartTime + 0.1 * i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRuleset : Ruleset
|
||||||
|
{
|
||||||
|
public override IEnumerable<Mod> GetModsFor(ModType type) => Enumerable.Empty<Mod>();
|
||||||
|
|
||||||
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PassThroughBeatmapConverter(beatmap);
|
||||||
|
|
||||||
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TestDifficultyCalculator(beatmap);
|
||||||
|
|
||||||
|
public override string Description => string.Empty;
|
||||||
|
public override string ShortName => string.Empty;
|
||||||
|
|
||||||
|
private class PassThroughBeatmapConverter : IBeatmapConverter
|
||||||
|
{
|
||||||
|
public event Action<HitObject, IEnumerable<HitObject>>? ObjectConverted
|
||||||
|
{
|
||||||
|
add { }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IBeatmap Beatmap { get; }
|
||||||
|
|
||||||
|
public PassThroughBeatmapConverter(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
Beatmap = beatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanConvert() => true;
|
||||||
|
|
||||||
|
public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDifficultyCalculator : DifficultyCalculator
|
||||||
|
{
|
||||||
|
public TestDifficultyCalculator(IWorkingBeatmap beatmap)
|
||||||
|
: base(new TestRuleset().RulesetInfo, beatmap)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
=> new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() };
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
|
foreach (var obj in beatmap.HitObjects.OfType<TestHitObject>())
|
||||||
|
{
|
||||||
|
if (!obj.Skip)
|
||||||
|
objects.Add(new DifficultyHitObject(obj, obj, clockRate, objects, objects.Count));
|
||||||
|
|
||||||
|
foreach (var nested in obj.NestedHitObjects)
|
||||||
|
objects.Add(new DifficultyHitObject(nested, nested, clockRate, objects, objects.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) };
|
||||||
|
|
||||||
|
private class PassThroughSkill : Skill
|
||||||
|
{
|
||||||
|
public PassThroughSkill(Mod[] mods)
|
||||||
|
: base(mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double DifficultyValue() => 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDifficultyAttributes : DifficultyAttributes
|
||||||
|
{
|
||||||
|
public HitObject[] Objects = Array.Empty<HitObject>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
osu.Game.Tests/Resources/Archives/japanese-filename.osz
Normal file
BIN
osu.Game.Tests/Resources/Samples/test-sample.ogg
Normal file
BIN
osu.Game.Tests/Resources/Samples/test-sample.webm
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[General]
|
||||||
|
Mode: 3
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
CircleSize:14
|
10
osu.Game.Tests/Resources/out-of-range-difficulties.osu
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[General]
|
||||||
|
Mode: 0
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:25
|
||||||
|
CircleSize:25
|
||||||
|
OverallDifficulty:25
|
||||||
|
ApproachRate:30
|
||||||
|
SliderMultiplier:30
|
||||||
|
SliderTickRate:30
|
@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Scoring;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Scores.IO
|
namespace osu.Game.Tests.Scores.IO
|
||||||
{
|
{
|
||||||
@ -284,6 +286,272 @@ namespace osu.Game.Tests.Scores.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserLookedUpByUsernameForOnlineScoreIfUserIDMissing()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = LoadOsuIntoHost(host, true);
|
||||||
|
|
||||||
|
var api = (DummyAPIAccess)osu.API;
|
||||||
|
api.HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case GetUserRequest userRequest:
|
||||||
|
if (userRequest.Lookup != "Test user")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
userRequest.TriggerSuccess(new APIUser
|
||||||
|
{
|
||||||
|
Username = "Test user",
|
||||||
|
CountryCode = CountryCode.JP,
|
||||||
|
Id = 1234
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||||
|
|
||||||
|
var toImport = new ScoreInfo
|
||||||
|
{
|
||||||
|
Rank = ScoreRank.B,
|
||||||
|
TotalScore = 987654,
|
||||||
|
Accuracy = 0.8,
|
||||||
|
MaxCombo = 500,
|
||||||
|
Combo = 250,
|
||||||
|
User = new APIUser { Username = "Test user" },
|
||||||
|
Date = DateTimeOffset.Now,
|
||||||
|
OnlineID = 12345,
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BeatmapInfo = beatmap.Beatmaps.First()
|
||||||
|
};
|
||||||
|
|
||||||
|
var imported = LoadScoreIntoOsu(osu, toImport);
|
||||||
|
|
||||||
|
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||||
|
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||||
|
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
|
||||||
|
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
|
||||||
|
Assert.AreEqual(toImport.User.Username, imported.User.Username);
|
||||||
|
Assert.AreEqual(toImport.Date, imported.Date);
|
||||||
|
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
|
||||||
|
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
|
||||||
|
Assert.AreEqual(1234, imported.RealmUser.OnlineID);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserLookedUpByUsernameForLegacyOnlineScore()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = LoadOsuIntoHost(host, true);
|
||||||
|
|
||||||
|
var api = (DummyAPIAccess)osu.API;
|
||||||
|
api.HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case GetUserRequest userRequest:
|
||||||
|
if (userRequest.Lookup != "Test user")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
userRequest.TriggerSuccess(new APIUser
|
||||||
|
{
|
||||||
|
Username = "Test user",
|
||||||
|
CountryCode = CountryCode.JP,
|
||||||
|
Id = 1234
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||||
|
|
||||||
|
var toImport = new ScoreInfo
|
||||||
|
{
|
||||||
|
Rank = ScoreRank.B,
|
||||||
|
TotalScore = 987654,
|
||||||
|
Accuracy = 0.8,
|
||||||
|
MaxCombo = 500,
|
||||||
|
Combo = 250,
|
||||||
|
User = new APIUser { Username = "Test user" },
|
||||||
|
Date = DateTimeOffset.Now,
|
||||||
|
LegacyOnlineID = 12345,
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BeatmapInfo = beatmap.Beatmaps.First()
|
||||||
|
};
|
||||||
|
|
||||||
|
var imported = LoadScoreIntoOsu(osu, toImport);
|
||||||
|
|
||||||
|
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||||
|
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||||
|
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
|
||||||
|
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
|
||||||
|
Assert.AreEqual(toImport.User.Username, imported.User.Username);
|
||||||
|
Assert.AreEqual(toImport.Date, imported.Date);
|
||||||
|
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
|
||||||
|
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
|
||||||
|
Assert.AreEqual(1234, imported.RealmUser.OnlineID);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserNotLookedUpForOfflineScoreIfUserIDMissing()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = LoadOsuIntoHost(host, true);
|
||||||
|
|
||||||
|
var api = (DummyAPIAccess)osu.API;
|
||||||
|
api.HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case GetUserRequest userRequest:
|
||||||
|
if (userRequest.Lookup != "Test user")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
userRequest.TriggerSuccess(new APIUser
|
||||||
|
{
|
||||||
|
Username = "Test user",
|
||||||
|
CountryCode = CountryCode.JP,
|
||||||
|
Id = 1234
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||||
|
|
||||||
|
var toImport = new ScoreInfo
|
||||||
|
{
|
||||||
|
Rank = ScoreRank.B,
|
||||||
|
TotalScore = 987654,
|
||||||
|
Accuracy = 0.8,
|
||||||
|
MaxCombo = 500,
|
||||||
|
Combo = 250,
|
||||||
|
User = new APIUser { Username = "Test user" },
|
||||||
|
Date = DateTimeOffset.Now,
|
||||||
|
OnlineID = -1,
|
||||||
|
LegacyOnlineID = -1,
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BeatmapInfo = beatmap.Beatmaps.First()
|
||||||
|
};
|
||||||
|
|
||||||
|
var imported = LoadScoreIntoOsu(osu, toImport);
|
||||||
|
|
||||||
|
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||||
|
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||||
|
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
|
||||||
|
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
|
||||||
|
Assert.AreEqual(toImport.User.Username, imported.User.Username);
|
||||||
|
Assert.AreEqual(toImport.Date, imported.Date);
|
||||||
|
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
|
||||||
|
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
|
||||||
|
Assert.That(imported.RealmUser.OnlineID, Is.LessThanOrEqualTo(1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserLookedUpByOnlineIDIfPresent([Values] bool isOnlineScore)
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = LoadOsuIntoHost(host, true);
|
||||||
|
|
||||||
|
var api = (DummyAPIAccess)osu.API;
|
||||||
|
api.HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case GetUserRequest userRequest:
|
||||||
|
if (userRequest.Lookup != "5555")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
userRequest.TriggerSuccess(new APIUser
|
||||||
|
{
|
||||||
|
Username = "Some other guy",
|
||||||
|
CountryCode = CountryCode.DE,
|
||||||
|
Id = 5555
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||||
|
|
||||||
|
var toImport = new ScoreInfo
|
||||||
|
{
|
||||||
|
Rank = ScoreRank.B,
|
||||||
|
TotalScore = 987654,
|
||||||
|
Accuracy = 0.8,
|
||||||
|
MaxCombo = 500,
|
||||||
|
Combo = 250,
|
||||||
|
User = new APIUser { Id = 5555 },
|
||||||
|
Date = DateTimeOffset.Now,
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BeatmapInfo = beatmap.Beatmaps.First()
|
||||||
|
};
|
||||||
|
if (isOnlineScore)
|
||||||
|
toImport.OnlineID = 12345;
|
||||||
|
|
||||||
|
var imported = LoadScoreIntoOsu(osu, toImport);
|
||||||
|
|
||||||
|
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||||
|
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||||
|
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
|
||||||
|
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
|
||||||
|
Assert.AreEqual(toImport.Date, imported.Date);
|
||||||
|
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
|
||||||
|
Assert.AreEqual("Some other guy", imported.RealmUser.Username);
|
||||||
|
Assert.AreEqual(5555, imported.RealmUser.OnlineID);
|
||||||
|
Assert.AreEqual(CountryCode.DE, imported.RealmUser.CountryCode);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
|
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
|
||||||
{
|
{
|
||||||
// clone to avoid attaching the input score to realm.
|
// clone to avoid attaching the input score to realm.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
using MemoryStream = System.IO.MemoryStream;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Skins
|
namespace osu.Game.Tests.Skins
|
||||||
{
|
{
|
||||||
@ -21,6 +23,52 @@ namespace osu.Game.Tests.Skins
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapManager beatmaps { get; set; } = null!;
|
private BeatmapManager beatmaps { get; set; } = null!;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRetrieveAndLegacyExportJapaneseFilename()
|
||||||
|
{
|
||||||
|
IWorkingBeatmap beatmap = null!;
|
||||||
|
MemoryStream outStream = null!;
|
||||||
|
|
||||||
|
// Ensure importer encoding is correct
|
||||||
|
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
|
||||||
|
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
|
||||||
|
|
||||||
|
// Ensure exporter encoding is correct (round trip)
|
||||||
|
AddStep("export", () =>
|
||||||
|
{
|
||||||
|
outStream = new MemoryStream();
|
||||||
|
|
||||||
|
new LegacyBeatmapExporter(LocalStorage)
|
||||||
|
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||||
|
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRetrieveAndNonLegacyExportJapaneseFilename()
|
||||||
|
{
|
||||||
|
IWorkingBeatmap beatmap = null!;
|
||||||
|
MemoryStream outStream = null!;
|
||||||
|
|
||||||
|
// Ensure importer encoding is correct
|
||||||
|
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
|
||||||
|
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
|
||||||
|
|
||||||
|
// Ensure exporter encoding is correct (round trip)
|
||||||
|
AddStep("export", () =>
|
||||||
|
{
|
||||||
|
outStream = new MemoryStream();
|
||||||
|
|
||||||
|
new BeatmapExporter(LocalStorage)
|
||||||
|
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||||
|
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRetrieveOggAudio()
|
public void TestRetrieveOggAudio()
|
||||||
{
|
{
|
||||||
@ -45,6 +93,12 @@ namespace osu.Game.Tests.Skins
|
|||||||
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
|
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IWorkingBeatmap importBeatmapFromStream(Stream stream)
|
||||||
|
{
|
||||||
|
var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
|
||||||
|
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
|
||||||
|
}
|
||||||
|
|
||||||
private IWorkingBeatmap importBeatmapFromArchives(string filename)
|
private IWorkingBeatmap importBeatmapFromArchives(string filename)
|
||||||
{
|
{
|
||||||
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
|||||||
var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value);
|
var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value);
|
||||||
beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online.
|
beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online.
|
||||||
|
|
||||||
Child = thumbnail = new BeatmapCardThumbnail(beatmapSet)
|
Child = thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -10,9 +10,11 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Cached(typeof(SelectionRotationHandler))]
|
[Cached(typeof(SelectionRotationHandler))]
|
||||||
private TestSelectionRotationHandler rotationHandler;
|
private TestSelectionRotationHandler rotationHandler;
|
||||||
|
|
||||||
|
[Cached(typeof(SelectionScaleHandler))]
|
||||||
|
private TestSelectionScaleHandler scaleHandler;
|
||||||
|
|
||||||
public TestSceneComposeSelectBox()
|
public TestSceneComposeSelectBox()
|
||||||
{
|
{
|
||||||
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
||||||
|
scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
|
||||||
CanScaleX = true,
|
|
||||||
CanScaleY = true,
|
|
||||||
CanScaleDiagonally = true,
|
|
||||||
CanFlipX = true,
|
CanFlipX = true,
|
||||||
CanFlipY = true,
|
CanFlipY = true,
|
||||||
|
|
||||||
OnScale = handleScale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseButton(MouseButton.Left);
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
private bool handleScale(Vector2 amount, Anchor reference)
|
|
||||||
{
|
|
||||||
if ((reference & Anchor.y1) == 0)
|
|
||||||
{
|
|
||||||
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
|
|
||||||
if (directionY < 0)
|
|
||||||
selectionArea.Y += amount.Y;
|
|
||||||
selectionArea.Height += directionY * amount.Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((reference & Anchor.x1) == 0)
|
|
||||||
{
|
|
||||||
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
|
|
||||||
if (directionX < 0)
|
|
||||||
selectionArea.X += amount.X;
|
|
||||||
selectionArea.Width += directionX * amount.X;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
||||||
{
|
{
|
||||||
private readonly Func<Container> getTargetContainer;
|
private readonly Func<Container> getTargetContainer;
|
||||||
@ -89,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
this.getTargetContainer = getTargetContainer;
|
this.getTargetContainer = getTargetContainer;
|
||||||
|
|
||||||
CanRotateSelectionOrigin.Value = true;
|
CanRotateAroundSelectionOrigin.Value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class TestSelectionScaleHandler : SelectionScaleHandler
|
||||||
|
{
|
||||||
|
private readonly Func<Container> getTargetContainer;
|
||||||
|
|
||||||
|
public TestSelectionScaleHandler(Func<Container> getTargetContainer)
|
||||||
|
{
|
||||||
|
this.getTargetContainer = getTargetContainer;
|
||||||
|
|
||||||
|
CanScaleX.Value = true;
|
||||||
|
CanScaleY.Value = true;
|
||||||
|
CanScaleDiagonally.Value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private Container targetContainer;
|
||||||
|
|
||||||
|
public override void Begin()
|
||||||
|
{
|
||||||
|
if (targetContainer != null)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||||
|
|
||||||
|
targetContainer = getTargetContainer();
|
||||||
|
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||||
|
{
|
||||||
|
if (targetContainer == null)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
|
Vector2 actualOrigin = origin ?? Vector2.Zero;
|
||||||
|
|
||||||
|
targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft);
|
||||||
|
targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Commit()
|
||||||
|
{
|
||||||
|
if (targetContainer == null)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
|
targetContainer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRotationHandleShownOnHover()
|
public void TestRotationHandleShownOnHover()
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -83,6 +84,49 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeleteDifficultyWithPendingChanges()
|
||||||
|
{
|
||||||
|
Guid deletedDifficultyID = Guid.Empty;
|
||||||
|
int countBeforeDeletion = 0;
|
||||||
|
string beatmapSetHashBefore = string.Empty;
|
||||||
|
|
||||||
|
AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true);
|
||||||
|
|
||||||
|
AddStep("store selected difficulty", () =>
|
||||||
|
{
|
||||||
|
deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID;
|
||||||
|
countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count;
|
||||||
|
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("make change to difficulty", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.BeginChange();
|
||||||
|
EditorBeatmap.BeatmapInfo.DifficultyName = "changin' things";
|
||||||
|
EditorBeatmap.EndChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
||||||
|
|
||||||
|
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
||||||
|
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||||
|
AddAssert("dialog is deletion confirmation dialog", () => DialogOverlay.CurrentDialog, Is.InstanceOf<DeleteDifficultyConfirmationDialog>);
|
||||||
|
AddStep("confirm", () => InputManager.Key(Key.Number1));
|
||||||
|
|
||||||
|
AddUntilStep("no next dialog", () => DialogOverlay.CurrentDialog == null);
|
||||||
|
AddUntilStep("switched to different difficulty",
|
||||||
|
() => this.ChildrenOfType<EditorBeatmap>().SingleOrDefault() != null && EditorBeatmap.BeatmapInfo.ID != deletedDifficultyID);
|
||||||
|
|
||||||
|
AddAssert("difficulty is unattached from set",
|
||||||
|
() => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID));
|
||||||
|
AddAssert("beatmap set difficulty count decreased by one",
|
||||||
|
() => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1));
|
||||||
|
AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore));
|
||||||
|
AddAssert("difficulty is deleted from realm",
|
||||||
|
() => Realm.Run(r => r.Find<BeatmapInfo>(deletedDifficultyID)), () => Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType<DrawableOsuMenuItem>()
|
private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType<DrawableOsuMenuItem>()
|
||||||
.Single(item => item.ChildrenOfType<SpriteText>().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)));
|
.Single(item => item.ChildrenOfType<SpriteText>().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)));
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
textBox.Current.Value = bank;
|
textBox.Current.Value = bank;
|
||||||
// force a commit via keyboard.
|
// force a commit via keyboard.
|
||||||
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
|
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
|
||||||
InputManager.ChangeFocus(textBox);
|
((IFocusManager)InputManager).ChangeFocus(textBox);
|
||||||
InputManager.Key(Key.Enter);
|
InputManager.Key(Key.Enter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Timing;
|
using osu.Game.Beatmaps.Timing;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
|
||||||
|
|
||||||
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
|
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
|
||||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||||
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
|
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
|
||||||
|
|
||||||
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
|
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
|
||||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
|
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,22 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
public partial class TestSceneMetadataSection : OsuTestScene
|
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
||||||
@ -26,6 +31,81 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private TestMetadataSection metadataSection;
|
private TestMetadataSection metadataSection;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateViaTextBoxOnFocusLoss()
|
||||||
|
{
|
||||||
|
AddStep("set metadata", () =>
|
||||||
|
{
|
||||||
|
editorBeatmap.Metadata.Artist = "Example Artist";
|
||||||
|
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
createSection();
|
||||||
|
|
||||||
|
TextBox textbox;
|
||||||
|
|
||||||
|
AddStep("focus first textbox", () =>
|
||||||
|
{
|
||||||
|
textbox = metadataSection.ChildrenOfType<TextBox>().First();
|
||||||
|
InputManager.MoveMouseTo(textbox);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("simulate changing textbox", () =>
|
||||||
|
{
|
||||||
|
// Can't simulate text input but this should work.
|
||||||
|
InputManager.Keys(PlatformAction.SelectAll);
|
||||||
|
InputManager.Keys(PlatformAction.Copy);
|
||||||
|
InputManager.Keys(PlatformAction.Paste);
|
||||||
|
InputManager.Keys(PlatformAction.Paste);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertArtistMetadata("Example Artist");
|
||||||
|
|
||||||
|
// It's important values are committed immediately on focus loss so the editor exit sequence detects them.
|
||||||
|
AddAssert("value immediately changed on focus loss", () =>
|
||||||
|
{
|
||||||
|
((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
|
||||||
|
return editorBeatmap.Metadata.Artist;
|
||||||
|
}, () => Is.EqualTo("Example ArtistExample Artist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateViaTextBoxOnCommit()
|
||||||
|
{
|
||||||
|
AddStep("set metadata", () =>
|
||||||
|
{
|
||||||
|
editorBeatmap.Metadata.Artist = "Example Artist";
|
||||||
|
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
createSection();
|
||||||
|
|
||||||
|
TextBox textbox;
|
||||||
|
|
||||||
|
AddStep("focus first textbox", () =>
|
||||||
|
{
|
||||||
|
textbox = metadataSection.ChildrenOfType<TextBox>().First();
|
||||||
|
InputManager.MoveMouseTo(textbox);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("simulate changing textbox", () =>
|
||||||
|
{
|
||||||
|
// Can't simulate text input but this should work.
|
||||||
|
InputManager.Keys(PlatformAction.SelectAll);
|
||||||
|
InputManager.Keys(PlatformAction.Copy);
|
||||||
|
InputManager.Keys(PlatformAction.Paste);
|
||||||
|
InputManager.Keys(PlatformAction.Paste);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertArtistMetadata("Example Artist");
|
||||||
|
|
||||||
|
AddStep("commit", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
|
assertArtistMetadata("Example ArtistExample Artist");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMinimalMetadata()
|
public void TestMinimalMetadata()
|
||||||
{
|
{
|
||||||
@ -40,7 +120,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
createSection();
|
createSection();
|
||||||
|
|
||||||
assertArtist("Example Artist");
|
assertArtistTextBox("Example Artist");
|
||||||
assertRomanisedArtist("Example Artist", false);
|
assertRomanisedArtist("Example Artist", false);
|
||||||
|
|
||||||
assertTitle("Example Title");
|
assertTitle("Example Title");
|
||||||
@ -61,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
createSection();
|
createSection();
|
||||||
|
|
||||||
assertArtist("*なみりん");
|
assertArtistTextBox("*なみりん");
|
||||||
assertRomanisedArtist(string.Empty, true);
|
assertRomanisedArtist(string.Empty, true);
|
||||||
|
|
||||||
assertTitle("コイシテイク・プラネット");
|
assertTitle("コイシテイク・プラネット");
|
||||||
@ -82,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
createSection();
|
createSection();
|
||||||
|
|
||||||
assertArtist("*なみりん");
|
assertArtistTextBox("*なみりん");
|
||||||
assertRomanisedArtist("*namirin", true);
|
assertRomanisedArtist("*namirin", true);
|
||||||
|
|
||||||
assertTitle("コイシテイク・プラネット");
|
assertTitle("コイシテイク・プラネット");
|
||||||
@ -104,11 +184,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
createSection();
|
createSection();
|
||||||
|
|
||||||
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
|
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
|
||||||
assertArtist("*namirin");
|
assertArtistTextBox("*namirin");
|
||||||
assertRomanisedArtist("*namirin", false);
|
assertRomanisedArtist("*namirin", false);
|
||||||
|
|
||||||
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
|
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
|
||||||
assertArtist("*なみりん");
|
assertArtistTextBox("*なみりん");
|
||||||
assertRomanisedArtist("*namirin", true);
|
assertRomanisedArtist("*namirin", true);
|
||||||
|
|
||||||
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
|
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
|
||||||
@ -123,21 +203,24 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private void createSection()
|
private void createSection()
|
||||||
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
|
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
|
||||||
|
|
||||||
private void assertArtist(string expected)
|
private void assertArtistMetadata(string expected)
|
||||||
=> AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected);
|
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
|
||||||
|
|
||||||
|
private void assertArtistTextBox(string expected)
|
||||||
|
=> AddAssert($"artist textbox is {expected}", () => metadataSection.ArtistTextBox.Current.Value, () => Is.EqualTo(expected));
|
||||||
|
|
||||||
private void assertRomanisedArtist(string expected, bool editable)
|
private void assertRomanisedArtist(string expected, bool editable)
|
||||||
{
|
{
|
||||||
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected);
|
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected));
|
||||||
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
|
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertTitle(string expected)
|
private void assertTitle(string expected)
|
||||||
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected);
|
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value, () => Is.EqualTo(expected));
|
||||||
|
|
||||||
private void assertRomanisedTitle(string expected, bool editable)
|
private void assertRomanisedTitle(string expected, bool editable)
|
||||||
{
|
{
|
||||||
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected);
|
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected));
|
||||||
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
|
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("disallow all lookups", () =>
|
AddStep("disallow all lookups", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = false;
|
storyboard.UseSkinSprites = false;
|
||||||
storyboard.AlwaysProvideTexture = false;
|
storyboard.ProvideResources = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("allow storyboard lookup", () =>
|
AddStep("allow storyboard lookup", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = false;
|
storyboard.UseSkinSprites = false;
|
||||||
storyboard.AlwaysProvideTexture = true;
|
storyboard.ProvideResources = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -67,13 +68,48 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
assertStoryboardSourced();
|
assertStoryboardSourced();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(false)]
|
||||||
|
[TestCase(true)]
|
||||||
|
public void TestVideo(bool scaleTransformProvided)
|
||||||
|
{
|
||||||
|
AddStep("allow storyboard lookup", () =>
|
||||||
|
{
|
||||||
|
storyboard.ProvideResources = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create video", () => SetContents(_ =>
|
||||||
|
{
|
||||||
|
var layer = storyboard.GetLayer("Video");
|
||||||
|
|
||||||
|
var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current);
|
||||||
|
|
||||||
|
if (scaleTransformProvided)
|
||||||
|
{
|
||||||
|
sprite.Commands.AddScale(Easing.None, Time.Current, Time.Current + 1000, 1, 2);
|
||||||
|
sprite.Commands.AddScale(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.Elements.Clear();
|
||||||
|
layer.Add(sprite);
|
||||||
|
|
||||||
|
return new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
storyboard.CreateDrawable()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSkinLookupPreferredOverStoryboard()
|
public void TestSkinLookupPreferredOverStoryboard()
|
||||||
{
|
{
|
||||||
AddStep("allow all lookups", () =>
|
AddStep("allow all lookups", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = true;
|
storyboard.UseSkinSprites = true;
|
||||||
storyboard.AlwaysProvideTexture = true;
|
storyboard.ProvideResources = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -91,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("allow skin lookup", () =>
|
AddStep("allow skin lookup", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = true;
|
storyboard.UseSkinSprites = true;
|
||||||
storyboard.AlwaysProvideTexture = false;
|
storyboard.ProvideResources = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -109,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("allow all lookups", () =>
|
AddStep("allow all lookups", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = true;
|
storyboard.UseSkinSprites = true;
|
||||||
storyboard.AlwaysProvideTexture = true;
|
storyboard.ProvideResources = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -127,7 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("allow all lookups", () =>
|
AddStep("allow all lookups", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = true;
|
storyboard.UseSkinSprites = true;
|
||||||
storyboard.AlwaysProvideTexture = true;
|
storyboard.ProvideResources = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -142,7 +178,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("allow all lookups", () =>
|
AddStep("allow all lookups", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = true;
|
storyboard.UseSkinSprites = true;
|
||||||
storyboard.AlwaysProvideTexture = true;
|
storyboard.ProvideResources = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -156,7 +192,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("allow all lookups", () =>
|
AddStep("allow all lookups", () =>
|
||||||
{
|
{
|
||||||
storyboard.UseSkinSprites = true;
|
storyboard.UseSkinSprites = true;
|
||||||
storyboard.AlwaysProvideTexture = true;
|
storyboard.ProvideResources = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
|
||||||
@ -170,17 +206,25 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
|
AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
|
||||||
}
|
}
|
||||||
|
|
||||||
private DrawableStoryboard createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
|
private Drawable createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
|
||||||
{
|
{
|
||||||
var layer = storyboard.GetLayer("Background");
|
var layer = storyboard.GetLayer("Background");
|
||||||
|
|
||||||
var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
|
var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
|
||||||
sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1);
|
var loop = sprite.AddLoopingGroup(Time.Current, 100);
|
||||||
|
loop.AddAlpha(Easing.None, 0, 10000, 1, 1);
|
||||||
|
|
||||||
layer.Elements.Clear();
|
layer.Elements.Clear();
|
||||||
layer.Add(sprite);
|
layer.Add(sprite);
|
||||||
|
|
||||||
return storyboard.CreateDrawable().With(s => s.RelativeSizeAxes = Axes.Both);
|
return new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
storyboard.CreateDrawable()
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertStoryboardSourced()
|
private void assertStoryboardSourced()
|
||||||
@ -202,42 +246,52 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
return new TestDrawableStoryboard(this, mods);
|
return new TestDrawableStoryboard(this, mods);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AlwaysProvideTexture { get; set; }
|
public bool ProvideResources { get; set; }
|
||||||
|
|
||||||
public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty;
|
public override string GetStoragePathFromStoryboardPath(string path) => ProvideResources ? path : string.Empty;
|
||||||
|
|
||||||
private partial class TestDrawableStoryboard : DrawableStoryboard
|
private partial class TestDrawableStoryboard : DrawableStoryboard
|
||||||
{
|
{
|
||||||
private readonly bool alwaysProvideTexture;
|
private readonly bool provideResources;
|
||||||
|
|
||||||
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
|
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
|
||||||
: base(storyboard, mods)
|
: base(storyboard, mods)
|
||||||
{
|
{
|
||||||
alwaysProvideTexture = storyboard.AlwaysProvideTexture;
|
provideResources = storyboard.ProvideResources;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IResourceStore<byte[]> CreateResourceLookupStore() => alwaysProvideTexture
|
protected override IResourceStore<byte[]> CreateResourceLookupStore() => provideResources
|
||||||
? new AlwaysReturnsTextureStore()
|
? new ResourcesTextureStore()
|
||||||
: new ResourceStore<byte[]>();
|
: new ResourceStore<byte[]>();
|
||||||
|
|
||||||
internal class AlwaysReturnsTextureStore : IResourceStore<byte[]>
|
internal class ResourcesTextureStore : IResourceStore<byte[]>
|
||||||
{
|
{
|
||||||
private const string test_image = "Resources/Textures/test-image.png";
|
|
||||||
|
|
||||||
private readonly DllResourceStore store;
|
private readonly DllResourceStore store;
|
||||||
|
|
||||||
public AlwaysReturnsTextureStore()
|
public ResourcesTextureStore()
|
||||||
{
|
{
|
||||||
store = TestResources.GetStore();
|
store = TestResources.GetStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => store.Dispose();
|
public void Dispose() => store.Dispose();
|
||||||
|
|
||||||
public byte[] Get(string name) => store.Get(test_image);
|
public byte[] Get(string name) => store.Get(map(name));
|
||||||
|
|
||||||
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken);
|
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(map(name), cancellationToken);
|
||||||
|
|
||||||
public Stream GetStream(string name) => store.GetStream(test_image);
|
public Stream GetStream(string name) => store.GetStream(map(name));
|
||||||
|
|
||||||
|
private string map(string name)
|
||||||
|
{
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case lookup_name:
|
||||||
|
return "Resources/Textures/test-image.png";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return $"Resources/{name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
|
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||||
|
|
||||||
sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
|
sprite.Commands.AddAlpha(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
|
||||||
|
|
||||||
storyboard.GetLayer("Background").Add(sprite);
|
storyboard.GetLayer("Background").Add(sprite);
|
||||||
|
|
||||||
@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||||
|
|
||||||
// these should be ignored as we have an alpha visibility blocker proceeding this command.
|
// these should be ignored as we have an alpha visibility blocker proceeding this command.
|
||||||
sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1);
|
sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
|
||||||
var loopGroup = sprite.AddLoop(loop_start_time, 50);
|
var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50);
|
||||||
loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1);
|
loopGroup.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
|
||||||
|
|
||||||
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
|
var target = addEventToLoop ? loopGroup : sprite.Commands;
|
||||||
double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0;
|
double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0;
|
||||||
target.Alpha.Add(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1);
|
target.AddAlpha(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1);
|
||||||
|
|
||||||
// these should be ignored due to being in the future.
|
// these should be ignored due to being in the future.
|
||||||
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
|
sprite.Commands.AddAlpha(Easing.None, 18000, 20000, 0, 1);
|
||||||
loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1);
|
loopGroup.AddAlpha(Easing.None, 38000, 40000, 0, 1);
|
||||||
|
|
||||||
storyboard.GetLayer("Background").Add(sprite);
|
storyboard.GetLayer("Background").Add(sprite);
|
||||||
|
|
||||||
|
264
osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Storyboards.Drawables;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
public partial class TestSceneStoryboardCommands : OsuTestScene
|
||||||
|
{
|
||||||
|
[Cached(typeof(Storyboard))]
|
||||||
|
private TestStoryboard storyboard { get; set; } = new TestStoryboard
|
||||||
|
{
|
||||||
|
UseSkinSprites = false,
|
||||||
|
AlwaysProvideTexture = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly ManualClock manualClock = new ManualClock { Rate = 1, IsRunning = true };
|
||||||
|
private int clockDirection;
|
||||||
|
|
||||||
|
private const string lookup_name = "hitcircleoverlay";
|
||||||
|
private const double clock_limit = 2500;
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
|
private Container content = null!;
|
||||||
|
private SpriteText timelineText = null!;
|
||||||
|
private Box timelineMarker = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
base.Content.Children = new Drawable[]
|
||||||
|
{
|
||||||
|
content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
timelineText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Margin = new MarginPadding { Bottom = 60 },
|
||||||
|
},
|
||||||
|
timelineMarker = new Box
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomCentre,
|
||||||
|
RelativePositionAxes = Axes.X,
|
||||||
|
Size = new Vector2(2, 50),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("start clock", () => clockDirection = 1);
|
||||||
|
AddStep("pause clock", () => clockDirection = 0);
|
||||||
|
AddStep("set clock = 0", () => manualClock.CurrentTime = 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNormalCommandPlayback()
|
||||||
|
{
|
||||||
|
AddStep("create storyboard", () => Child = createStoryboard(s =>
|
||||||
|
{
|
||||||
|
s.Commands.AddY(Easing.OutBounce, 500, 900, 100, 240);
|
||||||
|
s.Commands.AddY(Easing.OutQuint, 1100, 1500, 240, 100);
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert(0, 100);
|
||||||
|
assert(500, 100);
|
||||||
|
assert(1000, 240);
|
||||||
|
assert(1500, 100);
|
||||||
|
assert(clock_limit, 100);
|
||||||
|
assert(1500, 100);
|
||||||
|
assert(1000, 240);
|
||||||
|
assert(500, 100);
|
||||||
|
assert(0, 100);
|
||||||
|
|
||||||
|
void assert(double time, double y)
|
||||||
|
{
|
||||||
|
AddStep($"set clock = {time}", () => manualClock.CurrentTime = time);
|
||||||
|
AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().Y == y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLoopingCommandsPlayback()
|
||||||
|
{
|
||||||
|
AddStep("create storyboard", () => Child = createStoryboard(s =>
|
||||||
|
{
|
||||||
|
var loop = s.AddLoopingGroup(250, 1);
|
||||||
|
loop.AddY(Easing.OutBounce, 0, 400, 100, 240);
|
||||||
|
loop.AddY(Easing.OutQuint, 600, 1000, 240, 100);
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert(0, 100);
|
||||||
|
assert(250, 100);
|
||||||
|
assert(850, 240);
|
||||||
|
assert(1250, 100);
|
||||||
|
assert(1850, 240);
|
||||||
|
assert(2250, 100);
|
||||||
|
assert(clock_limit, 100);
|
||||||
|
assert(2250, 100);
|
||||||
|
assert(1850, 240);
|
||||||
|
assert(1250, 100);
|
||||||
|
assert(850, 240);
|
||||||
|
assert(250, 100);
|
||||||
|
assert(0, 100);
|
||||||
|
|
||||||
|
void assert(double time, double y)
|
||||||
|
{
|
||||||
|
AddStep($"set clock = {time}", () => manualClock.CurrentTime = time);
|
||||||
|
AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().Y == y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLoopManyTimes()
|
||||||
|
{
|
||||||
|
AddStep("create storyboard", () => Child = createStoryboard(s =>
|
||||||
|
{
|
||||||
|
var loop = s.AddLoopingGroup(500, 10000);
|
||||||
|
loop.AddY(Easing.OutBounce, 0, 60, 100, 240);
|
||||||
|
loop.AddY(Easing.OutQuint, 80, 120, 240, 100);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestParameterTemporaryEffect()
|
||||||
|
{
|
||||||
|
AddStep("create storyboard", () => Child = createStoryboard(s =>
|
||||||
|
{
|
||||||
|
s.Commands.AddFlipV(Easing.None, 1000, 1500, true, false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddAssert("sprite not flipped at t = 0", () => !this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
|
||||||
|
|
||||||
|
AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250);
|
||||||
|
AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
|
||||||
|
|
||||||
|
AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000);
|
||||||
|
AddAssert("sprite not flipped at t = 2000", () => !this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
|
||||||
|
|
||||||
|
AddStep("resume clock", () => clockDirection = 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestParameterPermanentEffect()
|
||||||
|
{
|
||||||
|
AddStep("create storyboard", () => Child = createStoryboard(s =>
|
||||||
|
{
|
||||||
|
s.Commands.AddFlipV(Easing.None, 1000, 1000, true, true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddAssert("sprite flipped at t = 0", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
|
||||||
|
|
||||||
|
AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250);
|
||||||
|
AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
|
||||||
|
|
||||||
|
AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000);
|
||||||
|
AddAssert("sprite flipped at t = 2000", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
|
||||||
|
|
||||||
|
AddStep("resume clock", () => clockDirection = 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (manualClock.CurrentTime > clock_limit || manualClock.CurrentTime < 0)
|
||||||
|
clockDirection = -clockDirection;
|
||||||
|
|
||||||
|
manualClock.CurrentTime += Time.Elapsed * clockDirection;
|
||||||
|
timelineText.Text = $"Time: {manualClock.CurrentTime:0}ms";
|
||||||
|
timelineMarker.X = (float)(manualClock.CurrentTime / clock_limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrawableStoryboard createStoryboard(Action<StoryboardSprite>? addCommands = null)
|
||||||
|
{
|
||||||
|
var layer = storyboard.GetLayer("Background");
|
||||||
|
|
||||||
|
var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240));
|
||||||
|
sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f);
|
||||||
|
sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1);
|
||||||
|
addCommands?.Invoke(sprite);
|
||||||
|
|
||||||
|
layer.Elements.Clear();
|
||||||
|
layer.Add(sprite);
|
||||||
|
|
||||||
|
return storyboard.CreateDrawable().With(c => c.Clock = new FramedClock(manualClock));
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestStoryboard : Storyboard
|
||||||
|
{
|
||||||
|
public override DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null)
|
||||||
|
{
|
||||||
|
return new TestDrawableStoryboard(this, mods);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AlwaysProvideTexture { get; set; }
|
||||||
|
|
||||||
|
public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty;
|
||||||
|
|
||||||
|
private partial class TestDrawableStoryboard : DrawableStoryboard
|
||||||
|
{
|
||||||
|
private readonly bool alwaysProvideTexture;
|
||||||
|
|
||||||
|
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
|
||||||
|
: base(storyboard, mods)
|
||||||
|
{
|
||||||
|
alwaysProvideTexture = storyboard.AlwaysProvideTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IResourceStore<byte[]> CreateResourceLookupStore() => alwaysProvideTexture
|
||||||
|
? new AlwaysReturnsTextureStore()
|
||||||
|
: new ResourceStore<byte[]>();
|
||||||
|
|
||||||
|
internal class AlwaysReturnsTextureStore : IResourceStore<byte[]>
|
||||||
|
{
|
||||||
|
private const string test_image = "Resources/Textures/test-image.png";
|
||||||
|
|
||||||
|
private readonly DllResourceStore store;
|
||||||
|
|
||||||
|
public AlwaysReturnsTextureStore()
|
||||||
|
{
|
||||||
|
store = TestResources.GetStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => store.Dispose();
|
||||||
|
|
||||||
|
public byte[] Get(string name) => store.Get(test_image);
|
||||||
|
|
||||||
|
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken);
|
||||||
|
|
||||||
|
public Stream GetStream(string name) => store.GetStream(test_image);
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
var storyboard = new Storyboard();
|
var storyboard = new Storyboard();
|
||||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||||
sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1);
|
sprite.Commands.AddAlpha(Easing.None, startTime, 0, 0, 1);
|
||||||
storyboard.GetLayer("Background").Add(sprite);
|
storyboard.GetLayer("Background").Add(sprite);
|
||||||
return storyboard;
|
return storyboard;
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
var storyboard = new Storyboard();
|
var storyboard = new Storyboard();
|
||||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||||
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
|
sprite.Commands.AddAlpha(Easing.None, 0, duration, 1, 0);
|
||||||
storyboard.GetLayer("Background").Add(sprite);
|
storyboard.GetLayer("Background").Add(sprite);
|
||||||
return storyboard;
|
return storyboard;
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
}),
|
}),
|
||||||
createLoungeRoom(new Room
|
createLoungeRoom(new Room
|
||||||
{
|
{
|
||||||
Name = { Value = "Multiplayer room" },
|
Name = { Value = "Private room" },
|
||||||
Status = { Value = new RoomStatusOpen() },
|
Status = { Value = new RoomStatusOpenPrivate() },
|
||||||
|
HasPassword = { Value = true },
|
||||||
EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
|
EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
|
||||||
Type = { Value = MatchType.HeadToHead },
|
Type = { Value = MatchType.HeadToHead },
|
||||||
Playlist =
|
Playlist =
|
||||||
|
@ -16,6 +16,7 @@ using osu.Framework.Platform;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osu.Game.Beatmaps.Drawables.Cards;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
@ -317,13 +318,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
p.RequestResults = _ => resultsRequested = true;
|
p.RequestResults = _ => resultsRequested = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
|
||||||
AddStep("move mouse to first item title", () =>
|
AddStep("move mouse to first item title", () =>
|
||||||
{
|
{
|
||||||
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
|
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
|
||||||
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
|
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
|
||||||
InputManager.MoveMouseTo(location);
|
InputManager.MoveMouseTo(location);
|
||||||
});
|
});
|
||||||
AddUntilStep("wait for text load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any());
|
|
||||||
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
|
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
|
||||||
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
|
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
|
||||||
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);
|
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);
|
||||||
|
@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public void TestIntroStoryboardElement() => testLeadIn(b =>
|
public void TestIntroStoryboardElement() => testLeadIn(b =>
|
||||||
{
|
{
|
||||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||||
sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1);
|
sprite.Commands.AddAlpha(Easing.None, -2000, 0, 0, 1);
|
||||||
b.Storyboard.GetLayer("Background").Add(sprite);
|
b.Storyboard.GetLayer("Background").Add(sprite);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Extensions;
|
|||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Mania;
|
|||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.GameplayTest;
|
using osu.Game.Screens.Edit.GameplayTest;
|
||||||
|
using osu.Game.Screens.Edit.Setup;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
@ -27,6 +29,59 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
{
|
{
|
||||||
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
||||||
{
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
|
||||||
|
{
|
||||||
|
BeatmapSetInfo beatmapSet = null!;
|
||||||
|
|
||||||
|
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||||
|
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||||
|
|
||||||
|
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||||
|
AddUntilStep("wait for song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||||
|
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||||
|
&& songSelect.IsLoaded);
|
||||||
|
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||||
|
|
||||||
|
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||||
|
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
|
||||||
|
AddStep("change to song setup", () => InputManager.Key(Key.F4));
|
||||||
|
|
||||||
|
TextBox textbox = null!;
|
||||||
|
|
||||||
|
AddUntilStep("wait for metadata section", () =>
|
||||||
|
{
|
||||||
|
var t = Game.ChildrenOfType<MetadataSection>().SingleOrDefault().ChildrenOfType<TextBox>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (t == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
textbox = t;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("focus textbox", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(textbox);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("simulate changing textbox", () =>
|
||||||
|
{
|
||||||
|
// Can't simulate text input but this should work.
|
||||||
|
InputManager.Keys(PlatformAction.SelectAll);
|
||||||
|
InputManager.Keys(PlatformAction.Copy);
|
||||||
|
InputManager.Keys(PlatformAction.Paste);
|
||||||
|
InputManager.Keys(PlatformAction.Paste);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
|
||||||
|
|
||||||
|
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -26,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
{
|
{
|
||||||
public partial class TestScenePresentScore : OsuGameTestScene
|
public partial class TestScenePresentScore : OsuGameTestScene
|
||||||
{
|
{
|
||||||
private BeatmapSetInfo beatmap;
|
private BeatmapSetInfo beatmap = null!;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public new void SetUpSteps()
|
public new void SetUpSteps()
|
||||||
@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
Ruleset = new OsuRuleset().RulesetInfo
|
Ruleset = new OsuRuleset().RulesetInfo
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?.Value;
|
})!.Value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +143,40 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
presentAndConfirm(secondImport, type);
|
presentAndConfirm(secondImport, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPresentTwoImportsWithSameOnlineIDButDifferentHashes([Values] ScorePresentType type)
|
||||||
|
{
|
||||||
|
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
|
||||||
|
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
|
||||||
|
|
||||||
|
var firstImport = importScore(1);
|
||||||
|
presentAndConfirm(firstImport, type);
|
||||||
|
|
||||||
|
var secondImport = importScore(1);
|
||||||
|
presentAndConfirm(secondImport, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreRefetchIgnoresEmptyHash()
|
||||||
|
{
|
||||||
|
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
|
||||||
|
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
|
||||||
|
|
||||||
|
importScore(-1, hash: string.Empty);
|
||||||
|
importScore(3, hash: @"deadbeef");
|
||||||
|
|
||||||
|
// oftentimes a `PresentScore()` call will be given a `ScoreInfo` which is converted from an online score,
|
||||||
|
// in which cases the hash will generally not be available.
|
||||||
|
AddStep("present score", () => Game.PresentScore(new ScoreInfo { OnlineID = 3, Hash = string.Empty }));
|
||||||
|
|
||||||
|
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||||
|
AddUntilStep("correct score displayed", () =>
|
||||||
|
{
|
||||||
|
var score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!;
|
||||||
|
return score.OnlineID == 3 && score.Hash == "deadbeef";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void returnToMenu()
|
private void returnToMenu()
|
||||||
{
|
{
|
||||||
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
|
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
|
||||||
@ -158,31 +190,31 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Func<ScoreInfo> importScore(int i, RulesetInfo ruleset = null)
|
private Func<ScoreInfo> importScore(int i, RulesetInfo? ruleset = null, string? hash = null)
|
||||||
{
|
{
|
||||||
ScoreInfo imported = null;
|
ScoreInfo? imported = null;
|
||||||
AddStep($"import score {i}", () =>
|
AddStep($"import score {i}", () =>
|
||||||
{
|
{
|
||||||
imported = Game.ScoreManager.Import(new ScoreInfo
|
imported = Game.ScoreManager.Import(new ScoreInfo
|
||||||
{
|
{
|
||||||
Hash = Guid.NewGuid().ToString(),
|
Hash = hash ?? Guid.NewGuid().ToString(),
|
||||||
OnlineID = i,
|
OnlineID = i,
|
||||||
BeatmapInfo = beatmap.Beatmaps.First(),
|
BeatmapInfo = beatmap.Beatmaps.First(),
|
||||||
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
|
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
|
||||||
User = new GuestUser(),
|
User = new GuestUser(),
|
||||||
}).Value;
|
})!.Value;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert($"import {i} succeeded", () => imported != null);
|
AddAssert($"import {i} succeeded", () => imported != null);
|
||||||
|
|
||||||
return () => imported;
|
return () => imported!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
|
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
|
||||||
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
|
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IScreen lastWaitedScreen;
|
private IScreen lastWaitedScreen = null!;
|
||||||
|
|
||||||
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
|
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -321,6 +322,30 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
|
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSkinSavesOnChange()
|
||||||
|
{
|
||||||
|
advanceToSongSelect();
|
||||||
|
openSkinEditor();
|
||||||
|
|
||||||
|
Guid editedSkinId = Guid.Empty;
|
||||||
|
AddStep("save skin id", () => editedSkinId = Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.Value.ID);
|
||||||
|
AddStep("add skinnable component", () =>
|
||||||
|
{
|
||||||
|
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("change to triangles skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(SkinInfo.TRIANGLES_SKIN.ToString()));
|
||||||
|
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||||
|
// sort of implicitly relies on song select not being skinnable.
|
||||||
|
// TODO: revisit if the above ever changes
|
||||||
|
AddUntilStep("skin changed", () => !skinEditor.ChildrenOfType<SkinBlueprint>().Any());
|
||||||
|
|
||||||
|
AddStep("change back to modified skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(editedSkinId.ToString()));
|
||||||
|
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||||
|
AddUntilStep("changes saved", () => skinEditor.ChildrenOfType<SkinBlueprint>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
private void advanceToSongSelect()
|
private void advanceToSongSelect()
|
||||||
{
|
{
|
||||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||||
|
@ -82,6 +82,14 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
}).ToList());
|
}).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNonBasicHitResultsAreIgnored()
|
||||||
|
{
|
||||||
|
createTest(CreateDistributedHitEvents(0, 50)
|
||||||
|
.Select(h => new HitEvent(h.TimeOffset, 1.0, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMultipleWindowsOfHitResult()
|
public void TestMultipleWindowsOfHitResult()
|
||||||
{
|
{
|
||||||
|
@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
scores.Add(score);
|
scores.Add(score);
|
||||||
}
|
}
|
||||||
|
|
||||||
scoresCallback?.Invoke(scores);
|
scoresCallback.Invoke(scores);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,7 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBindingConflictResolvedByRollback()
|
public void TestBindingConflictResolvedByRollbackViaMouse()
|
||||||
{
|
{
|
||||||
AddStep("reset taiko section to default", () =>
|
AddStep("reset taiko section to default", () =>
|
||||||
{
|
{
|
||||||
@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBindingConflictResolvedByOverwrite()
|
public void TestBindingConflictResolvedByOverwriteViaMouse()
|
||||||
{
|
{
|
||||||
AddStep("reset taiko section to default", () =>
|
AddStep("reset taiko section to default", () =>
|
||||||
{
|
{
|
||||||
@ -333,6 +333,46 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
checkBinding("Left (rim)", "M1");
|
checkBinding("Left (rim)", "M1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBindingConflictResolvedByRollbackViaKeyboard()
|
||||||
|
{
|
||||||
|
AddStep("reset taiko & global sections to default", () =>
|
||||||
|
{
|
||||||
|
panel.ChildrenOfType<VariantBindingsSubsection>().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset))
|
||||||
|
.ChildrenOfType<ResetButton>().Single().TriggerClick();
|
||||||
|
|
||||||
|
panel.ChildrenOfType<ResetButton>().First().TriggerClick();
|
||||||
|
});
|
||||||
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
|
||||||
|
scrollToAndStartBinding("Left (rim)");
|
||||||
|
AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddUntilStep("wait for popover", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Not.Null);
|
||||||
|
AddStep("press Esc", () => InputManager.Key(Key.Escape));
|
||||||
|
checkBinding("Left (centre)", "M1");
|
||||||
|
checkBinding("Left (rim)", "M2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBindingConflictResolvedByOverwriteViaKeyboard()
|
||||||
|
{
|
||||||
|
AddStep("reset taiko & global sections to default", () =>
|
||||||
|
{
|
||||||
|
panel.ChildrenOfType<VariantBindingsSubsection>().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset))
|
||||||
|
.ChildrenOfType<ResetButton>().Single().TriggerClick();
|
||||||
|
|
||||||
|
panel.ChildrenOfType<ResetButton>().First().TriggerClick();
|
||||||
|
});
|
||||||
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
|
||||||
|
scrollToAndStartBinding("Left (rim)");
|
||||||
|
AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddUntilStep("wait for popover", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Not.Null);
|
||||||
|
AddStep("press Enter", () => InputManager.Key(Key.Enter));
|
||||||
|
checkBinding("Left (centre)", InputSettingsStrings.ActionHasNoKeyBinding.ToString());
|
||||||
|
checkBinding("Left (rim)", "M1");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBindingConflictCausedByResetToDefaultOfSingleRow()
|
public void TestBindingConflictCausedByResetToDefaultOfSingleRow()
|
||||||
{
|
{
|
||||||
|
@ -87,6 +87,105 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("delete all beatmaps", () => manager.Delete());
|
AddStep("delete all beatmaps", () => manager.Delete());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpeedChange()
|
||||||
|
{
|
||||||
|
createSongSelect();
|
||||||
|
changeMods();
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
|
||||||
|
OsuModNightcore nc = new OsuModNightcore
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 1.05 }
|
||||||
|
};
|
||||||
|
changeMods(nc);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
OsuModDoubleTime dt = new OsuModDoubleTime
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 1.02 },
|
||||||
|
AdjustPitch = { Value = true },
|
||||||
|
};
|
||||||
|
changeMods(dt);
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005));
|
||||||
|
AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||||
|
|
||||||
|
OsuModHalfTime ht = new OsuModHalfTime
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 0.97 },
|
||||||
|
AdjustPitch = { Value = true },
|
||||||
|
};
|
||||||
|
Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() };
|
||||||
|
changeMods(modlist);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005));
|
||||||
|
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||||
|
AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType<ModHidden>().SingleOrDefault(), () => Is.Not.Null);
|
||||||
|
AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType<ModHardRock>().SingleOrDefault(), () => Is.Not.Null);
|
||||||
|
|
||||||
|
changeMods(new ModWindUp());
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp);
|
||||||
|
|
||||||
|
changeMods(new ModAdaptiveSpeed());
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
|
||||||
|
|
||||||
|
void increaseModSpeed() => AddStep("increase mod speed", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Up);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
void decreaseModSpeed() => AddStep("decrease mod speed", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Down);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPlaceholderBeatmapPresence()
|
public void TestPlaceholderBeatmapPresence()
|
||||||
{
|
{
|
||||||
|