mirror of
https://github.com/ppy/osu.git
synced 2025-02-23 22:42:54 +08:00
Merge branch 'master' into sudden-death-feat
This commit is contained in:
commit
cb012123ab
@ -9,7 +9,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nvika": {
|
"nvika": {
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"nvika"
|
"nvika"
|
||||||
]
|
]
|
||||||
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -114,10 +114,7 @@ jobs:
|
|||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
- name: Install .NET workloads
|
- name: Install .NET workloads
|
||||||
# since windows image 20241113.3.0, not specifying a version here
|
run: dotnet workload install android
|
||||||
# installs the .NET 7 version of android workload for very unknown reasons.
|
|
||||||
# revisit once we upgrade to .NET 9, it's probably fixed there.
|
|
||||||
run: dotnet workload install android --version (dotnet --version)
|
|
||||||
|
|
||||||
- name: Compile
|
- name: Compile
|
||||||
run: dotnet build -c Debug osu.Android.slnf
|
run: dotnet build -c Debug osu.Android.slnf
|
||||||
|
10
.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml
Normal file
10
.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="osu.Android">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
|
|||||||
# Too many noisy warnings for parsing/formatting numbers
|
# Too many noisy warnings for parsing/formatting numbers
|
||||||
dotnet_diagnostic.CA1305.severity = none
|
dotnet_diagnostic.CA1305.severity = none
|
||||||
|
|
||||||
|
# messagepack complains about "osu" not being title cased due to reserved words
|
||||||
|
dotnet_diagnostic.CS8981.severity = none
|
||||||
|
|
||||||
# CA1507: Use nameof to express symbol names
|
# CA1507: Use nameof to express symbol names
|
||||||
# Flaggs serialization name attributes
|
# Flags serialization name attributes
|
||||||
dotnet_diagnostic.CA1507.severity = suggestion
|
dotnet_diagnostic.CA1507.severity = suggestion
|
||||||
|
|
||||||
# CA1806: Do not ignore method results
|
# CA1806: Do not ignore method results
|
||||||
|
@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu!
|
|||||||
|
|
||||||
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
|
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
|
||||||
|
|
||||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
|
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
|
||||||
|
|
||||||
## Developing a custom ruleset
|
## Developing a custom ruleset
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||||
|
@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
|
|||||||
|
|
||||||
public Vector2 Position { get; set; }
|
public Vector2 Position { get; set; }
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
get => Position.X;
|
||||||
|
set => Position = new Vector2(value, Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Y
|
||||||
|
{
|
||||||
|
get => Position.Y;
|
||||||
|
set => Position = new Vector2(X, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
|
|||||||
|
|
||||||
public Vector2 Position { get; set; }
|
public Vector2 Position { get; set; }
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
get => Position.X;
|
||||||
|
set => Position = new Vector2(value, Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Y
|
||||||
|
{
|
||||||
|
get => Position.Y;
|
||||||
|
set => Position = new Vector2(X, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1220.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.204.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -1,34 +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.Content.PM;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
|
|
||||||
namespace osu.Android
|
|
||||||
{
|
|
||||||
public partial class GameplayScreenRotationLocker : Component
|
|
||||||
{
|
|
||||||
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OsuGameActivity gameActivity { get; set; } = null!;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(ILocalUserPlayInfo localUserPlayInfo)
|
|
||||||
{
|
|
||||||
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
|
|
||||||
localUserPlaying.BindValueChanged(updateLock, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
|
|
||||||
{
|
|
||||||
gameActivity.RunOnUiThread(() =>
|
|
||||||
{
|
|
||||||
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ using Android.Graphics;
|
|||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using osu.Framework.Android;
|
using osu.Framework.Android;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using Debug = System.Diagnostics.Debug;
|
using Debug = System.Diagnostics.Debug;
|
||||||
using Uri = Android.Net.Uri;
|
using Uri = Android.Net.Uri;
|
||||||
@ -50,9 +49,25 @@ namespace osu.Android
|
|||||||
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||||
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||||
|
|
||||||
private OsuGameAndroid game = null!;
|
public new bool IsTablet { get; private set; }
|
||||||
|
|
||||||
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
|
private readonly OsuGameAndroid game;
|
||||||
|
|
||||||
|
private bool gameCreated;
|
||||||
|
|
||||||
|
protected override Framework.Game CreateGame()
|
||||||
|
{
|
||||||
|
if (gameCreated)
|
||||||
|
throw new InvalidOperationException("Framework tried to create a game twice.");
|
||||||
|
|
||||||
|
gameCreated = true;
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OsuGameActivity()
|
||||||
|
{
|
||||||
|
game = new OsuGameAndroid(this);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnCreate(Bundle? savedInstanceState)
|
protected override void OnCreate(Bundle? savedInstanceState)
|
||||||
{
|
{
|
||||||
@ -76,9 +91,9 @@ namespace osu.Android
|
|||||||
WindowManager.DefaultDisplay.GetSize(displaySize);
|
WindowManager.DefaultDisplay.GetSize(displaySize);
|
||||||
#pragma warning restore CA1422
|
#pragma warning restore CA1422
|
||||||
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
|
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
|
||||||
bool isTablet = smallestWidthDp >= 600f;
|
IsTablet = smallestWidthDp >= 600f;
|
||||||
|
|
||||||
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
||||||
|
|
||||||
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
|
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
|
||||||
// The assembly files are not available as files either after native AOT.
|
// The assembly files are not available as files either after native AOT.
|
||||||
@ -95,25 +110,38 @@ namespace osu.Android
|
|||||||
|
|
||||||
private void handleIntent(Intent? intent)
|
private void handleIntent(Intent? intent)
|
||||||
{
|
{
|
||||||
switch (intent?.Action)
|
if (intent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (intent.Action)
|
||||||
{
|
{
|
||||||
case Intent.ActionDefault:
|
case Intent.ActionDefault:
|
||||||
if (intent.Scheme == ContentResolver.SchemeContent)
|
if (intent.Scheme == ContentResolver.SchemeContent)
|
||||||
handleImportFromUris(intent.Data.AsNonNull());
|
{
|
||||||
|
if (intent.Data != null)
|
||||||
|
handleImportFromUris(intent.Data);
|
||||||
|
}
|
||||||
else if (osu_url_schemes.Contains(intent.Scheme))
|
else if (osu_url_schemes.Contains(intent.Scheme))
|
||||||
game.HandleLink(intent.DataString);
|
{
|
||||||
|
if (intent.DataString != null)
|
||||||
|
game.HandleLink(intent.DataString);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Intent.ActionSend:
|
case Intent.ActionSend:
|
||||||
case Intent.ActionSendMultiple:
|
case Intent.ActionSendMultiple:
|
||||||
{
|
{
|
||||||
|
if (intent.ClipData == null)
|
||||||
|
break;
|
||||||
|
|
||||||
var uris = new List<Uri>();
|
var uris = new List<Uri>();
|
||||||
|
|
||||||
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
|
for (int i = 0; i < intent.ClipData.ItemCount; i++)
|
||||||
{
|
{
|
||||||
var content = intent.ClipData?.GetItemAt(i);
|
var item = intent.ClipData.GetItemAt(i);
|
||||||
if (content != null)
|
if (item?.Uri != null)
|
||||||
uris.Add(content.Uri.AsNonNull());
|
uris.Add(item.Uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImportFromUris(uris.ToArray());
|
handleImportFromUris(uris.ToArray());
|
||||||
|
@ -3,13 +3,16 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
|
using Android.Content.PM;
|
||||||
using Microsoft.Maui.Devices;
|
using Microsoft.Maui.Devices;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
|
using osu.Game.Screens;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Android
|
namespace osu.Android
|
||||||
{
|
{
|
||||||
@ -18,6 +21,8 @@ namespace osu.Android
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OsuGameActivity gameActivity;
|
private readonly OsuGameActivity gameActivity;
|
||||||
|
|
||||||
|
protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||||
|
|
||||||
public OsuGameAndroid(OsuGameActivity activity)
|
public OsuGameAndroid(OsuGameActivity activity)
|
||||||
: base(null)
|
: base(null)
|
||||||
{
|
{
|
||||||
@ -71,7 +76,35 @@ namespace osu.Android
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
|
UserPlayingState.BindValueChanged(_ => updateOrientation());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
|
||||||
|
{
|
||||||
|
base.ScreenChanged(current, newScreen);
|
||||||
|
|
||||||
|
if (newScreen != null)
|
||||||
|
updateOrientation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOrientation()
|
||||||
|
{
|
||||||
|
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
|
||||||
|
|
||||||
|
switch (orientation)
|
||||||
|
{
|
||||||
|
case MobileUtils.Orientation.Locked:
|
||||||
|
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MobileUtils.Orientation.Portrait:
|
||||||
|
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MobileUtils.Orientation.Default:
|
||||||
|
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetHost(GameHost host)
|
public override void SetHost(GameHost host)
|
||||||
|
@ -51,12 +51,9 @@ namespace osu.Desktop
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
|
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
private IBindable<DiscordRichPresenceMode> privacyMode = null!;
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
private IBindable<UserStatus> userStatus = null!;
|
||||||
|
private IBindable<UserActivity?> userActivity = null!;
|
||||||
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
|
|
||||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
|
||||||
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
|
|
||||||
|
|
||||||
private readonly RichPresence presence = new RichPresence
|
private readonly RichPresence presence = new RichPresence
|
||||||
{
|
{
|
||||||
@ -71,8 +68,12 @@ namespace osu.Desktop
|
|||||||
private IBindable<APIUser>? user;
|
private IBindable<APIUser>? user;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(OsuConfigManager config, SessionStatics session)
|
||||||
{
|
{
|
||||||
|
privacyMode = config.GetBindable<DiscordRichPresenceMode>(OsuSetting.DiscordRichPresence);
|
||||||
|
userStatus = config.GetBindable<UserStatus>(OsuSetting.UserOnlineStatus);
|
||||||
|
userActivity = session.GetBindable<UserActivity?>(Static.UserOnlineActivity);
|
||||||
|
|
||||||
client = new DiscordRpcClient(client_id)
|
client = new DiscordRpcClient(client_id)
|
||||||
{
|
{
|
||||||
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
||||||
@ -81,7 +82,7 @@ namespace osu.Desktop
|
|||||||
};
|
};
|
||||||
|
|
||||||
client.OnReady += onReady;
|
client.OnReady += onReady;
|
||||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
|
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -105,21 +106,11 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
|
||||||
|
|
||||||
user = api.LocalUser.GetBoundCopy();
|
user = api.LocalUser.GetBoundCopy();
|
||||||
user.BindValueChanged(u =>
|
|
||||||
{
|
|
||||||
status.UnbindBindings();
|
|
||||||
status.BindTo(u.NewValue.Status);
|
|
||||||
|
|
||||||
activity.UnbindBindings();
|
|
||||||
activity.BindTo(u.NewValue.Activity);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
status.BindValueChanged(_ => schedulePresenceUpdate());
|
userStatus.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
userActivity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
|
|
||||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||||
@ -151,13 +142,13 @@ namespace osu.Desktop
|
|||||||
if (!client.IsInitialized)
|
if (!client.IsInitialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||||
{
|
{
|
||||||
client.ClearPresence();
|
client.ClearPresence();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb;
|
||||||
|
|
||||||
updatePresence(hideIdentifiableInformation);
|
updatePresence(hideIdentifiableInformation);
|
||||||
client.SetPresence(presence);
|
client.SetPresence(presence);
|
||||||
@ -170,19 +161,19 @@ namespace osu.Desktop
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// user activity
|
// user activity
|
||||||
if (activity.Value != null)
|
if (userActivity.Value != null)
|
||||||
{
|
{
|
||||||
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
|
presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation));
|
||||||
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||||
|
|
||||||
if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
|
if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
|
||||||
{
|
{
|
||||||
presence.Buttons = new[]
|
presence.Buttons = new[]
|
||||||
{
|
{
|
||||||
new Button
|
new Button
|
||||||
{
|
{
|
||||||
Label = "View beatmap",
|
Label = "View beatmap",
|
||||||
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,12 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stableInstallPath = getStableInstallPathFromRegistry();
|
stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||||
|
return stableInstallPath;
|
||||||
|
|
||||||
|
stableInstallPath = getStableInstallPathFromRegistry("osu!");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||||
return stableInstallPath;
|
return stableInstallPath;
|
||||||
@ -89,9 +94,9 @@ namespace osu.Desktop
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
private string? getStableInstallPathFromRegistry()
|
private string? getStableInstallPathFromRegistry(string progId)
|
||||||
{
|
{
|
||||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId))
|
||||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +139,6 @@ namespace osu.Desktop
|
|||||||
if (iconStream != null)
|
if (iconStream != null)
|
||||||
host.Window.SetIconFromStream(iconStream);
|
host.Window.SetIconFromStream(iconStream);
|
||||||
|
|
||||||
host.Window.CursorState |= CursorState.Hidden;
|
|
||||||
host.Window.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,6 @@ namespace osu.Desktop.Security
|
|||||||
|
|
||||||
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
||||||
{
|
{
|
||||||
public override bool IsImportant => true;
|
|
||||||
|
|
||||||
public ElevatedPrivilegesNotification()
|
public ElevatedPrivilegesNotification()
|
||||||
{
|
{
|
||||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
||||||
|
@ -17,6 +17,7 @@ namespace osu.Desktop.Windows
|
|||||||
public static class WindowsAssociationManager
|
public static class WindowsAssociationManager
|
||||||
{
|
{
|
||||||
private const string software_classes = @"Software\Classes";
|
private const string software_classes = @"Software\Classes";
|
||||||
|
private const string software_registered_applications = @"Software\RegisteredApplications";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sub key for setting the icon.
|
/// Sub key for setting the icon.
|
||||||
@ -36,7 +37,11 @@ namespace osu.Desktop.Windows
|
|||||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string program_id_prefix = "osu.File";
|
private const string program_id_file_prefix = "osu.File";
|
||||||
|
|
||||||
|
private const string program_id_protocol_prefix = "osu.Uri";
|
||||||
|
|
||||||
|
private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)");
|
||||||
|
|
||||||
private static readonly FileAssociation[] file_associations =
|
private static readonly FileAssociation[] file_associations =
|
||||||
{
|
{
|
||||||
@ -56,14 +61,13 @@ namespace osu.Desktop.Windows
|
|||||||
/// Installs file and URI associations.
|
/// Installs file and URI associations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static void InstallAssociations()
|
public static void InstallAssociations()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
updateAssociations();
|
updateAssociations();
|
||||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
|
||||||
NotifyShellUpdate();
|
NotifyShellUpdate();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -76,17 +80,13 @@ namespace osu.Desktop.Windows
|
|||||||
/// Updates associations with latest definitions.
|
/// Updates associations with latest definitions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static void UpdateAssociations()
|
public static void UpdateAssociations()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
updateAssociations();
|
updateAssociations();
|
||||||
|
|
||||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
|
||||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
|
||||||
|
|
||||||
NotifyShellUpdate();
|
NotifyShellUpdate();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -95,11 +95,19 @@ namespace osu.Desktop.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
// TODO: call this sometime.
|
||||||
|
public static void LocaliseDescriptions(LocalisationManager localisationManager)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
updateDescriptions(localisationManager);
|
application_capability.LocaliseDescription(localisationManager);
|
||||||
|
|
||||||
|
foreach (var association in file_associations)
|
||||||
|
association.LocaliseDescription(localisationManager);
|
||||||
|
|
||||||
|
foreach (var association in uri_associations)
|
||||||
|
association.LocaliseDescription(localisationManager);
|
||||||
|
|
||||||
NotifyShellUpdate();
|
NotifyShellUpdate();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -112,6 +120,8 @@ namespace osu.Desktop.Windows
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
application_capability.Uninstall();
|
||||||
|
|
||||||
foreach (var association in file_associations)
|
foreach (var association in file_associations)
|
||||||
association.Uninstall();
|
association.Uninstall();
|
||||||
|
|
||||||
@ -133,22 +143,16 @@ namespace osu.Desktop.Windows
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static void updateAssociations()
|
private static void updateAssociations()
|
||||||
{
|
{
|
||||||
|
application_capability.Install();
|
||||||
|
|
||||||
foreach (var association in file_associations)
|
foreach (var association in file_associations)
|
||||||
association.Install();
|
association.Install();
|
||||||
|
|
||||||
foreach (var association in uri_associations)
|
foreach (var association in uri_associations)
|
||||||
association.Install();
|
association.Install();
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateDescriptions(LocalisationManager? localisation)
|
application_capability.RegisterFileAssociations(file_associations);
|
||||||
{
|
application_capability.RegisterUriAssociations(uri_associations);
|
||||||
foreach (var association in file_associations)
|
|
||||||
association.UpdateDescription(getLocalisedString(association.Description));
|
|
||||||
|
|
||||||
foreach (var association in uri_associations)
|
|
||||||
association.UpdateDescription(getLocalisedString(association.Description));
|
|
||||||
|
|
||||||
string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Native interop
|
#region Native interop
|
||||||
@ -174,9 +178,87 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
private class ApplicationCapability
|
||||||
{
|
{
|
||||||
private string programId => $@"{program_id_prefix}{Extension}";
|
private string uniqueName { get; }
|
||||||
|
private string capabilityPath { get; }
|
||||||
|
private LocalisableString description { get; }
|
||||||
|
|
||||||
|
public ApplicationCapability(string uniqueName, string capabilityPath, LocalisableString description)
|
||||||
|
{
|
||||||
|
this.uniqueName = uniqueName;
|
||||||
|
this.capabilityPath = capabilityPath;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs">
|
||||||
|
/// Registering an Application for Use with Default Programs</see>.
|
||||||
|
/// </summary>
|
||||||
|
public void Install()
|
||||||
|
{
|
||||||
|
using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
|
||||||
|
{
|
||||||
|
capability.SetValue(@"ApplicationDescription", description.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
|
||||||
|
registeredApplications?.SetValue(uniqueName, capabilityPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterFileAssociations(FileAssociation[] associations)
|
||||||
|
{
|
||||||
|
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
|
||||||
|
if (capability == null) return;
|
||||||
|
|
||||||
|
using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
|
||||||
|
|
||||||
|
foreach (var association in associations)
|
||||||
|
fileAssociations.SetValue(association.Extension, association.ProgramId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterUriAssociations(UriAssociation[] associations)
|
||||||
|
{
|
||||||
|
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
|
||||||
|
if (capability == null) return;
|
||||||
|
|
||||||
|
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
|
||||||
|
|
||||||
|
foreach (var association in associations)
|
||||||
|
urlAssociations.SetValue(association.Protocol, association.ProgramId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||||
|
{
|
||||||
|
using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
|
||||||
|
{
|
||||||
|
capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Uninstall()
|
||||||
|
{
|
||||||
|
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
|
||||||
|
registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
|
||||||
|
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FileAssociation
|
||||||
|
{
|
||||||
|
public string ProgramId => $@"{program_id_file_prefix}{Extension}";
|
||||||
|
|
||||||
|
public string Extension { get; }
|
||||||
|
private LocalisableString description { get; }
|
||||||
|
private string iconPath { get; }
|
||||||
|
|
||||||
|
public FileAssociation(string extension, LocalisableString description, string iconPath)
|
||||||
|
{
|
||||||
|
Extension = extension;
|
||||||
|
this.description = description;
|
||||||
|
this.iconPath = iconPath;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||||
@ -187,10 +269,12 @@ namespace osu.Desktop.Windows
|
|||||||
if (classes == null) return;
|
if (classes == null) return;
|
||||||
|
|
||||||
// register a program id for the given extension
|
// register a program id for the given extension
|
||||||
using (var programKey = classes.CreateSubKey(programId))
|
using (var programKey = classes.CreateSubKey(ProgramId))
|
||||||
{
|
{
|
||||||
|
programKey.SetValue(null, description.ToString());
|
||||||
|
|
||||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||||
defaultIconKey.SetValue(null, IconPath);
|
defaultIconKey.SetValue(null, iconPath);
|
||||||
|
|
||||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||||
@ -198,23 +282,25 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||||
{
|
{
|
||||||
// set ourselves as the default program
|
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer,
|
||||||
extensionKey.SetValue(null, programId);
|
// so having it here is just confusing and may override user preferences.
|
||||||
|
if (extensionKey.GetValue(null) is string s && s == ProgramId)
|
||||||
|
extensionKey.SetValue(null, string.Empty);
|
||||||
|
|
||||||
// add to the open with dialog
|
// add to the open with dialog
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||||
openWithKey.SetValue(programId, string.Empty);
|
openWithKey.SetValue(ProgramId, string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateDescription(string description)
|
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||||
{
|
{
|
||||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
if (classes == null) return;
|
if (classes == null) return;
|
||||||
|
|
||||||
using (var programKey = classes.OpenSubKey(programId, true))
|
using (var programKey = classes.OpenSubKey(ProgramId, true))
|
||||||
programKey?.SetValue(null, description);
|
programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -227,26 +313,34 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||||
{
|
{
|
||||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
|
||||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
|
||||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
|
||||||
extensionKey.SetValue(null, string.Empty);
|
|
||||||
|
|
||||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
private class UriAssociation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string URL_PROTOCOL = @"URL Protocol";
|
private const string url_protocol = @"URL Protocol";
|
||||||
|
|
||||||
|
public string Protocol { get; }
|
||||||
|
private LocalisableString description { get; }
|
||||||
|
private string iconPath { get; }
|
||||||
|
|
||||||
|
public UriAssociation(string protocol, LocalisableString description, string iconPath)
|
||||||
|
{
|
||||||
|
Protocol = protocol;
|
||||||
|
this.description = description;
|
||||||
|
this.iconPath = iconPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||||
@ -258,29 +352,38 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||||
{
|
{
|
||||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
protocolKey.SetValue(null, $@"URL:{description}");
|
||||||
|
protocolKey.SetValue(url_protocol, string.Empty);
|
||||||
|
|
||||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
// clear out old data
|
||||||
defaultIconKey.SetValue(null, IconPath);
|
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
|
||||||
|
protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
|
||||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
// register a program id for the given protocol
|
||||||
|
using (var programKey = classes.CreateSubKey(ProgramId))
|
||||||
|
{
|
||||||
|
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||||
|
defaultIconKey.SetValue(null, iconPath);
|
||||||
|
|
||||||
|
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateDescription(string description)
|
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||||
{
|
{
|
||||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
if (classes == null) return;
|
if (classes == null) return;
|
||||||
|
|
||||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Uninstall()
|
public void Uninstall()
|
||||||
{
|
{
|
||||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,9 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Velopack" Version="0.0.915" />
|
<PackageReference Include="Velopack" Version="0.0.1053" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -12,7 +12,6 @@ using osu.Framework.Testing;
|
|||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Edit;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
protected override void UpdatePlacementTimeAndPosition()
|
||||||
{
|
{
|
||||||
var result = base.SnapForBlueprint(blueprint);
|
var position = InputManager.CurrentState.Mouse.Position;
|
||||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
|
||||||
return result;
|
CurrentBlueprint.UpdateTimeAndPosition(position, time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
||||||
{
|
{
|
||||||
private JuiceStream hitObject;
|
private JuiceStream hitObject = null!;
|
||||||
|
|
||||||
private readonly ManualClock manualClock = new ManualClock();
|
private readonly ManualClock manualClock = new ManualClock();
|
||||||
|
|
||||||
@ -193,6 +191,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addVertexCheckStep(1, 0, times[0], positions[0]);
|
addVertexCheckStep(1, 0, times[0], positions[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeletingSecondVertexDeletesEntireJuiceStream()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 400 };
|
||||||
|
float[] positions = { 100, 150 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDeleteVertexSteps(times[1], positions[1]);
|
||||||
|
AddAssert("juice stream deleted", () => EditorBeatmap.HitObjects, () => Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestVertexResampling()
|
public void TestVertexResampling()
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Difficulty;
|
using osu.Game.Rulesets.Catch.Difficulty;
|
||||||
using osu.Game.Rulesets.Catch.Edit;
|
using osu.Game.Rulesets.Catch.Edit;
|
||||||
|
using osu.Game.Rulesets.Catch.Edit.Setup;
|
||||||
using osu.Game.Rulesets.Catch.Mods;
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Catch.Replays;
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
new MetadataSection(),
|
new MetadataSection(),
|
||||||
new DifficultySection(),
|
new CatchDifficultySection(),
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
return base.OnMouseDown(e);
|
return base.OnMouseDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
if (!(result.Time is double time)) return;
|
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||||
|
|
||||||
|
if (!(result.Time is double time)) return result;
|
||||||
|
|
||||||
switch (PlacementActive)
|
switch (PlacementActive)
|
||||||
{
|
{
|
||||||
@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
|
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
|
||||||
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
|
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
{
|
{
|
||||||
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||||
where THitObject : CatchHitObject, new()
|
where THitObject : CatchHitObject, new()
|
||||||
{
|
{
|
||||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||||
@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private Playfield playfield { get; set; } = null!;
|
private Playfield playfield { get; set; } = null!;
|
||||||
|
|
||||||
public CatchPlacementBlueprint()
|
[Resolved]
|
||||||
|
protected CatchHitObjectComposer? Composer { get; private set; }
|
||||||
|
|
||||||
|
protected CatchPlacementBlueprint()
|
||||||
: base(new THitObject())
|
: base(new THitObject())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
public virtual void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
// The SV setting may need to be changed for the current path.
|
// The SV setting may need to be changed for the current path.
|
||||||
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||||
|
@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
EditorBeatmap?.EndChange();
|
EditorBeatmap?.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
base.UpdateHitObjectFromPath(hitObject);
|
||||||
|
|
||||||
|
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||||
|
EditorBeatmap?.Remove(hitObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||||
|
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||||
|
|
||||||
|
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||||
|
? distanceSnapResult
|
||||||
|
: gridSnapResult;
|
||||||
|
|
||||||
|
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||||
|
|
||||||
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
|
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,15 +83,22 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
return base.OnMouseDown(e);
|
return base.OnMouseDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
|
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||||
|
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||||
|
|
||||||
|
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||||
|
? distanceSnapResult
|
||||||
|
: gridSnapResult;
|
||||||
|
|
||||||
switch (PlacementActive)
|
switch (PlacementActive)
|
||||||
{
|
{
|
||||||
case PlacementState.Waiting:
|
case PlacementState.Waiting:
|
||||||
if (!(result.Time is double snappedTime)) return;
|
|
||||||
|
|
||||||
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
|
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||||
HitObject.StartTime = snappedTime;
|
if (result.Time is double snappedTime)
|
||||||
|
HitObject.StartTime = snappedTime;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlacementState.Active:
|
case PlacementState.Active:
|
||||||
@ -100,28 +107,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the up-to-date position is used for outlines.
|
// Make sure the up-to-date position is used for outlines.
|
||||||
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
||||||
|
|
||||||
updateHitObjectFromPath();
|
if (lastEditablePathId != editablePath.PathId)
|
||||||
}
|
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||||
|
lastEditablePathId = editablePath.PathId;
|
||||||
|
|
||||||
private void updateHitObjectFromPath()
|
|
||||||
{
|
|
||||||
if (lastEditablePathId == editablePath.PathId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
editablePath.UpdateHitObjectFromPath(HitObject);
|
|
||||||
ApplyDefaultsToHitObject();
|
ApplyDefaultsToHitObject();
|
||||||
|
|
||||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||||
|
return result;
|
||||||
lastEditablePathId = editablePath.PathId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double positionToTime(float relativeYPosition)
|
private double positionToTime(float relativeYPosition)
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
|
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
|
||||||
{
|
{
|
||||||
|
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
|
||||||
|
|
||||||
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
||||||
: base(composer)
|
: base(composer)
|
||||||
{
|
{
|
||||||
@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||||
|
|
||||||
|
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||||
|
{
|
||||||
|
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||||
|
|
||||||
|
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||||
|
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||||
|
|
||||||
|
// Retrieve a snapped position.
|
||||||
|
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
|
||||||
|
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
|
||||||
|
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||||
|
|
||||||
|
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||||
|
? distanceSnapResult
|
||||||
|
: gridSnapResult;
|
||||||
|
|
||||||
|
var referenceBlueprint = blueprints.First().blueprint;
|
||||||
|
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||||
|
if (moved)
|
||||||
|
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
//
|
//
|
||||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||||
|
|
||||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
|
||||||
|
|
||||||
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
|
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
|
||||||
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
|
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
|
||||||
|
@ -18,15 +18,15 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
|
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
private const float distance_snap_radius = 50;
|
public const float DISTANCE_SNAP_RADIUS = 50;
|
||||||
|
|
||||||
private CatchDistanceSnapGrid distanceSnapGrid = null!;
|
private CatchDistanceSnapGrid distanceSnapGrid = null!;
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
=> base.CreateTernaryButtons()
|
=> base.CreateTernaryButtons()
|
||||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||||
|
|
||||||
@ -136,22 +136,12 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
|
||||||
|
return snapResult;
|
||||||
|
|
||||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
return null;
|
||||||
|
|
||||||
if (snapType.HasFlag(SnapType.RelativeGrids))
|
|
||||||
{
|
|
||||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
|
||||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
|
||||||
{
|
|
||||||
result = snapResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
|
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
|
||||||
|
125
osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs
Normal file
125
osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||||
|
{
|
||||||
|
public partial class CatchDifficultySection : SetupSection
|
||||||
|
{
|
||||||
|
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||||
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
|
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||||
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
|
|
||||||
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
circleSizeSlider = new FormSliderBar<float>
|
||||||
|
{
|
||||||
|
Caption = BeatmapsetsStrings.ShowStatsCs,
|
||||||
|
HintText = EditorSetupStrings.CircleSizeDescription,
|
||||||
|
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||||
|
{
|
||||||
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 0.1f,
|
||||||
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
|
{
|
||||||
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
|
{
|
||||||
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 0.1f,
|
||||||
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
approachRateSlider = new FormSliderBar<float>
|
||||||
|
{
|
||||||
|
Caption = BeatmapsetsStrings.ShowStatsAr,
|
||||||
|
HintText = EditorSetupStrings.ApproachRateDescription,
|
||||||
|
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||||
|
{
|
||||||
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 0.1f,
|
||||||
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
|
{
|
||||||
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
|
{
|
||||||
|
Default = 1.4,
|
||||||
|
MinValue = 0.4,
|
||||||
|
MaxValue = 3.6,
|
||||||
|
Precision = 0.01f,
|
||||||
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
|
{
|
||||||
|
Caption = EditorSetupStrings.TickRate,
|
||||||
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
|
{
|
||||||
|
Default = 1,
|
||||||
|
MinValue = 1,
|
||||||
|
MaxValue = 4,
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||||
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
|
|
||||||
|
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||||
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateValues()
|
||||||
|
{
|
||||||
|
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||||
|
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||||
|
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||||
|
|
||||||
|
Beatmap.UpdateAllHitObjects();
|
||||||
|
Beatmap.SaveState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -159,27 +159,26 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
// Note that this implementation is shared with the osu! ruleset's implementation.
|
// Note that this implementation is shared with the osu! ruleset's implementation.
|
||||||
// If a change is made here, OsuHitObject.cs should also be updated.
|
// If a change is made here, OsuHitObject.cs should also be updated.
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||||
|
|
||||||
if (this is BananaShower)
|
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||||
|
// - At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
||||||
|
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||||
|
if (this is not BananaShower && (NewCombo || lastObj == null || lastObj is BananaShower))
|
||||||
{
|
{
|
||||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
inCurrentCombo = 0;
|
||||||
return;
|
index++;
|
||||||
}
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
|
||||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
|
||||||
if (NewCombo || lastObj == null || lastObj is BananaShower)
|
|
||||||
{
|
|
||||||
IndexInCurrentCombo = 0;
|
|
||||||
ComboIndex++;
|
|
||||||
ComboIndexWithOffsets += ComboOffset + 1;
|
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
@ -210,11 +209,27 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
||||||
|
|
||||||
float IHasXPosition.X => OriginalX;
|
float IHasXPosition.X
|
||||||
|
{
|
||||||
|
get => OriginalX;
|
||||||
|
set => OriginalX = value;
|
||||||
|
}
|
||||||
|
|
||||||
float IHasYPosition.Y => LegacyConvertedY;
|
float IHasYPosition.Y
|
||||||
|
{
|
||||||
|
get => LegacyConvertedY;
|
||||||
|
set => LegacyConvertedY = value;
|
||||||
|
}
|
||||||
|
|
||||||
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
|
Vector2 IHasPosition.Position
|
||||||
|
{
|
||||||
|
get => new Vector2(OriginalX, LegacyConvertedY);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
((IHasXPosition)this).X = value.X;
|
||||||
|
((IHasYPosition)this).Y = value.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
return new DefaultSkinComponentsContainer(container =>
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
{
|
{
|
||||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
if (keyCounter != null)
|
if (keyCounter != null)
|
||||||
{
|
{
|
||||||
@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
keyCounter.Origin = Anchor.TopRight;
|
keyCounter.Origin = Anchor.TopRight;
|
||||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (spectatorList != null)
|
||||||
|
{
|
||||||
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
|
spectatorList.Origin = Anchor.BottomLeft;
|
||||||
|
spectatorList.Position = new Vector2(10, -10);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new LegacyKeyCounterDisplay(),
|
new LegacyKeyCounterDisplay(),
|
||||||
|
new SpectatorList(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ 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.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Rulesets.Edit;
|
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
protected override void UpdatePlacementTimeAndPosition()
|
||||||
{
|
{
|
||||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||||
var pos = column.ScreenSpacePositionAtTime(time);
|
var pos = column.ScreenSpacePositionAtTime(time);
|
||||||
|
CurrentBlueprint.UpdateTimeAndPosition(pos, time);
|
||||||
return new SnapResult(pos, time, column);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
||||||
|
@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
|
|||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||||
{
|
{
|
||||||
@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
{
|
{
|
||||||
set => InternalChild = value;
|
set => InternalChild = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
{
|
{
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new ManiaModHidden(),
|
Mod = new ManiaModFadeIn(),
|
||||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
{
|
{
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new ManiaModHidden(),
|
Mod = new ManiaModFadeIn(),
|
||||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
{
|
{
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new ManiaModHidden(),
|
Mod = new ManiaModFadeIn(),
|
||||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
{
|
{
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new ManiaModHidden(),
|
Mod = new ManiaModFadeIn(),
|
||||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
{
|
{
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new ManiaModHidden(),
|
Mod = new ManiaModFadeIn(),
|
||||||
CreateBeatmap = () => new Beatmap
|
CreateBeatmap = () => new Beatmap
|
||||||
{
|
{
|
||||||
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
||||||
|
@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
Child = new ColumnHitObjectArea
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new HitObjectContainer(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new ColumnTestContainer(1, ManiaAction.Key2)
|
new ColumnTestContainer(1, ManiaAction.Key2)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
Child = new ColumnHitObjectArea
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new HitObjectContainer(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
68
osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs
Normal file
68
osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// 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.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 TestSceneManiaTouchInput : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchInput()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
|
||||||
|
AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action sent",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Contain(getColumn(index).Action.Value));
|
||||||
|
|
||||||
|
AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action released",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Not.Contain(getColumn(index).Action.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOneColumnMultipleTouches()
|
||||||
|
{
|
||||||
|
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action sent",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Contain(getColumn(0).Action.Value));
|
||||||
|
|
||||||
|
AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action still pressed",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Contain(getColumn(0).Action.Value));
|
||||||
|
|
||||||
|
AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action still pressed",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Contain(getColumn(0).Action.Value));
|
||||||
|
|
||||||
|
AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action released",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
|
||||||
|
}
|
||||||
|
}
|
@ -1,49 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
|
|
||||||
private double originalStartTime;
|
private double originalStartTime;
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
if (PlacementActive == PlacementState.Active)
|
if (PlacementActive == PlacementState.Active)
|
||||||
{
|
{
|
||||||
@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
if (result.Time is double startTime)
|
if (result.Time is double startTime)
|
||||||
originalStartTime = HitObject.StartTime = startTime;
|
originalStartTime = HitObject.StartTime = startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Game.Rulesets.Edit;
|
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
private EditorBeatmap? editorBeatmap { get; set; }
|
private EditorBeatmap? editorBeatmap { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
private ManiaHitObjectComposer? positionSnapProvider { get; set; }
|
||||||
|
|
||||||
private EditBodyPiece body = null!;
|
private EditBodyPiece body = null!;
|
||||||
private EditHoldNoteEndPiece head = null!;
|
private EditHoldNoteEndPiece head = null!;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
protected new T HitObject => (T)base.HitObject;
|
protected new T HitObject => (T)base.HitObject;
|
||||||
|
|
||||||
private Column column;
|
[Resolved]
|
||||||
|
private ManiaHitObjectComposer? composer { get; set; }
|
||||||
|
|
||||||
public Column Column
|
private Column? column;
|
||||||
|
|
||||||
|
public Column? Column
|
||||||
{
|
{
|
||||||
get => column;
|
get => column;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
if (value == column)
|
if (value == column)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
|
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||||
|
|
||||||
if (result.Playfield is Column col)
|
if (result.Playfield is Column col)
|
||||||
{
|
{
|
||||||
@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
if (PlacementActive == PlacementState.Waiting)
|
if (PlacementActive == PlacementState.Waiting)
|
||||||
Column = col;
|
Column = col;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private float getNoteHeight(Column resultPlayfield) =>
|
private float getNoteHeight(Column resultPlayfield) =>
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime);
|
||||||
|
|
||||||
if (result.Playfield != null)
|
if (result.Playfield != null)
|
||||||
{
|
{
|
||||||
piece.Width = result.Playfield.DrawWidth;
|
piece.Width = result.Playfield.DrawWidth;
|
||||||
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
|
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit
|
namespace osu.Game.Rulesets.Mania.Edit
|
||||||
{
|
{
|
||||||
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
|
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
|
||||||
{
|
{
|
||||||
public ManiaBlueprintContainer(HitObjectComposer composer)
|
public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer;
|
||||||
|
|
||||||
|
public ManiaBlueprintContainer(ManiaHitObjectComposer composer)
|
||||||
: base(composer)
|
: base(composer)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
|
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
|
||||||
|
|
||||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||||
|
|
||||||
|
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||||
|
{
|
||||||
|
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||||
|
|
||||||
|
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||||
|
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||||
|
|
||||||
|
// Retrieve a snapped position.
|
||||||
|
var result = Composer.FindSnappedPositionAndTime(movePosition);
|
||||||
|
|
||||||
|
var referenceBlueprint = blueprints.First().blueprint;
|
||||||
|
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||||
|
if (moved)
|
||||||
|
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit
|
namespace osu.Game.Rulesets.Mania.Edit
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||||
{
|
{
|
||||||
private DrawableManiaEditorRuleset drawableRuleset = null!;
|
private DrawableManiaEditorRuleset drawableRuleset = null!;
|
||||||
@ -64,11 +65,11 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||||
string[] objectDescriptions = objectDescription.Split(',').ToArray();
|
string[] objectDescriptions = objectDescription.Split(',');
|
||||||
|
|
||||||
for (int i = 0; i < objectDescriptions.Length; i++)
|
for (int i = 0; i < objectDescriptions.Length; i++)
|
||||||
{
|
{
|
||||||
string[] split = objectDescriptions[i].Split('|').ToArray();
|
string[] split = objectDescriptions[i].Split('|');
|
||||||
if (split.Length != 2)
|
if (split.Length != 2)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania
|
namespace osu.Game.Rulesets.Mania
|
||||||
{
|
{
|
||||||
[Cached] // Used for touch input, see ColumnTouchInputArea.
|
[Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp.
|
||||||
public partial class ManiaInputManager : RulesetInputManager<ManiaAction>
|
public partial class ManiaInputManager : RulesetInputManager<ManiaAction>
|
||||||
{
|
{
|
||||||
public ManiaInputManager(RulesetInfo ruleset, int variant)
|
public ManiaInputManager(RulesetInfo ruleset, int variant)
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
|
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
|
||||||
{
|
{
|
||||||
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
|
HitObjectContainer hoc = column.HitObjectContainer;
|
||||||
Container hocParent = (Container)hoc.Parent!;
|
Container hocParent = (Container)hoc.Parent!;
|
||||||
|
|
||||||
hocParent.Remove(hoc, false);
|
hocParent.Remove(hoc, false);
|
||||||
|
@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
|
|
||||||
#region LegacyBeatmapEncoder
|
#region LegacyBeatmapEncoder
|
||||||
|
|
||||||
float IHasXPosition.X => Column;
|
float IHasXPosition.X
|
||||||
|
{
|
||||||
|
get => Column;
|
||||||
|
set => Column = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -12,6 +13,7 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -19,25 +21,29 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
{
|
{
|
||||||
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||||
{
|
{
|
||||||
private const float judgement_y_position = 160;
|
private const float judgement_y_position = -180f;
|
||||||
|
|
||||||
private RingExplosion? ringExplosion;
|
private RingExplosion? ringExplosion;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; } = null!;
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
private IBindable<ScrollingDirection> direction = null!;
|
||||||
|
|
||||||
public ArgonJudgementPiece(HitResult result)
|
public ArgonJudgementPiece(HitResult result)
|
||||||
: base(result)
|
: base(result)
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
Y = judgement_y_position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(IScrollingInfo scrollingInfo)
|
||||||
{
|
{
|
||||||
|
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||||
|
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||||
|
|
||||||
if (Result.IsHit())
|
if (Result.IsHit())
|
||||||
{
|
{
|
||||||
AddInternal(ringExplosion = new RingExplosion(Result)
|
AddInternal(ringExplosion = new RingExplosion(Result)
|
||||||
@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||||
|
|
||||||
protected override SpriteText CreateJudgementText() =>
|
protected override SpriteText CreateJudgementText() =>
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
this.ScaleTo(1.6f);
|
this.ScaleTo(1.6f);
|
||||||
this.ScaleTo(1, 100, Easing.In);
|
this.ScaleTo(1, 100, Easing.In);
|
||||||
|
|
||||||
this.MoveToY(judgement_y_position);
|
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
|
||||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||||
|
|
||||||
this.RotateTo(0);
|
this.RotateTo(0);
|
||||||
|
@ -9,7 +9,9 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||||
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
return new DefaultSkinComponentsContainer(container =>
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
{
|
{
|
||||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
if (combo != null)
|
if (combo != null)
|
||||||
{
|
{
|
||||||
@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
combo.Origin = Anchor.Centre;
|
combo.Origin = Anchor.Centre;
|
||||||
combo.Y = 200;
|
combo.Y = 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (spectatorList != null)
|
||||||
|
spectatorList.Position = new Vector2(36, -66);
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
new ArgonManiaComboCounter(),
|
new ArgonManiaComboCounter(),
|
||||||
|
new SpectatorList
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Animations;
|
using osu.Framework.Graphics.Animations;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||||
@ -23,21 +24,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
this.result = result;
|
this.result = result;
|
||||||
this.animation = animation;
|
this.animation = animation;
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.BottomCentre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IBindable<ScrollingDirection> direction = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ISkinSource skin { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin)
|
private void load(IScrollingInfo scrollingInfo)
|
||||||
{
|
{
|
||||||
float? scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value;
|
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||||
|
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||||
if (scorePosition != null)
|
|
||||||
scorePosition -= Stage.HIT_TARGET_POSITION + 150;
|
|
||||||
|
|
||||||
Y = scorePosition ?? 0;
|
|
||||||
|
|
||||||
InternalChild = animation.With(d =>
|
InternalChild = animation.With(d =>
|
||||||
{
|
{
|
||||||
@ -46,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onDirectionChanged()
|
||||||
|
{
|
||||||
|
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
|
||||||
|
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
|
||||||
|
|
||||||
|
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||||
|
float finalPosition = scorePosition - absoluteHitPosition;
|
||||||
|
|
||||||
|
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
|
||||||
|
}
|
||||||
|
|
||||||
public void PlayAnimation()
|
public void PlayAnimation()
|
||||||
{
|
{
|
||||||
(animation as IFramedAnimation)?.GotoFrame(0);
|
(animation as IFramedAnimation)?.GotoFrame(0);
|
||||||
|
@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects.Legacy;
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||||
{
|
{
|
||||||
@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
return new DefaultSkinComponentsContainer(container =>
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
{
|
{
|
||||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
if (combo != null)
|
if (combo != null)
|
||||||
{
|
{
|
||||||
@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
combo.Origin = Anchor.Centre;
|
combo.Origin = Anchor.Centre;
|
||||||
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
|
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (spectatorList != null)
|
||||||
|
{
|
||||||
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
|
spectatorList.Origin = Anchor.BottomLeft;
|
||||||
|
spectatorList.Position = new Vector2(10, -10);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
new LegacyManiaComboCounter(),
|
new LegacyManiaComboCounter(),
|
||||||
|
new SpectatorList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Pooling;
|
using osu.Framework.Graphics.Pooling;
|
||||||
@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
|
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
private DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
private DrawablePool<PoolableHitExplosion> hitExplosionPool = null!;
|
||||||
private readonly OrderedHitPolicy hitPolicy;
|
private readonly OrderedHitPolicy hitPolicy;
|
||||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||||
|
|
||||||
private GameplaySampleTriggerSource sampleTriggerSource;
|
private GameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this is a special (ie. scratch) column.
|
/// Whether this is a special (ie. scratch) column.
|
||||||
@ -67,11 +66,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
Width = COLUMN_WIDTH;
|
Width = COLUMN_WIDTH;
|
||||||
|
|
||||||
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||||
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both };
|
HitObjectArea = new ColumnHitObjectArea
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = HitObjectContainer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ISkinSource skin { get; set; }
|
private ISkinSource skin { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host)
|
private void load(GameHost host)
|
||||||
@ -132,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (skin != null)
|
if (skin.IsNotNull())
|
||||||
skin.SourceChanged -= onSourceChanged;
|
skin.SourceChanged -= onSourceChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,5 +183,29 @@ 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));
|
||||||
|
|
||||||
|
#region Touch Input
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ManiaInputManager? maniaInputManager { get; set; }
|
||||||
|
|
||||||
|
private int touchActivationCount;
|
||||||
|
|
||||||
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
|
{
|
||||||
|
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||||
|
touchActivationCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnTouchUp(TouchUpEvent e)
|
||||||
|
{
|
||||||
|
touchActivationCount--;
|
||||||
|
|
||||||
|
if (touchActivationCount == 0)
|
||||||
|
maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,12 @@
|
|||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI.Components
|
namespace osu.Game.Rulesets.Mania.UI.Components
|
||||||
{
|
{
|
||||||
public partial class ColumnHitObjectArea : HitObjectArea
|
public partial class ColumnHitObjectArea : HitPositionPaddedContainer
|
||||||
{
|
{
|
||||||
public readonly Container Explosions;
|
public readonly Container Explosions;
|
||||||
|
|
||||||
@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
|||||||
|
|
||||||
private readonly Drawable hitTarget;
|
private readonly Drawable hitTarget;
|
||||||
|
|
||||||
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
|
protected override Container<Drawable> Content => content;
|
||||||
: base(hitObjectContainer)
|
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
public ColumnHitObjectArea()
|
||||||
{
|
{
|
||||||
AddRangeInternal(new[]
|
AddRangeInternal(new[]
|
||||||
{
|
{
|
||||||
UnderlayElements = new Container
|
UnderlayElements = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Depth = 2,
|
|
||||||
},
|
},
|
||||||
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
|
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Depth = 1
|
},
|
||||||
|
content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
Explosions = new Container
|
Explosions = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Depth = -1,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,38 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Mania.Skinning;
|
using osu.Game.Rulesets.Mania.Skinning;
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI.Components
|
namespace osu.Game.Rulesets.Mania.UI.Components
|
||||||
{
|
{
|
||||||
public partial class HitObjectArea : SkinReloadableDrawable
|
public partial class HitPositionPaddedContainer : Container
|
||||||
{
|
{
|
||||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||||
public readonly HitObjectContainer HitObjectContainer;
|
|
||||||
|
|
||||||
public HitObjectArea(HitObjectContainer hitObjectContainer)
|
[Resolved]
|
||||||
{
|
private ISkinSource skin { get; set; } = null!;
|
||||||
InternalChild = new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Child = HitObjectContainer = hitObjectContainer
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(IScrollingInfo scrollingInfo)
|
private void load(IScrollingInfo scrollingInfo)
|
||||||
{
|
{
|
||||||
Direction.BindTo(scrollingInfo.Direction);
|
Direction.BindTo(scrollingInfo.Direction);
|
||||||
Direction.BindValueChanged(onDirectionChanged, true);
|
Direction.BindValueChanged(_ => UpdateHitPosition(), true);
|
||||||
|
|
||||||
|
skin.SourceChanged += onSkinChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void SkinChanged(ISkinSource skin)
|
private void onSkinChanged() => UpdateHitPosition();
|
||||||
{
|
|
||||||
base.SkinChanged(skin);
|
|
||||||
UpdateHitPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
|
||||||
{
|
|
||||||
UpdateHitPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void UpdateHitPosition()
|
protected virtual void UpdateHitPosition()
|
||||||
{
|
{
|
||||||
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
||||||
?? Stage.HIT_TARGET_POSITION;
|
?? Stage.HIT_TARGET_POSITION;
|
||||||
|
|
||||||
@ -54,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
|||||||
? new MarginPadding { Top = hitPosition }
|
? new MarginPadding { Top = hitPosition }
|
||||||
: new MarginPadding { Bottom = hitPosition };
|
: new MarginPadding { Bottom = hitPosition };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (skin.IsNotNull())
|
||||||
|
skin.SourceChanged -= onSkinChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
75
osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs
Normal file
75
osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
|
{
|
||||||
|
public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||||
|
{
|
||||||
|
private const float judgement_y_position = -180f;
|
||||||
|
|
||||||
|
private IBindable<ScrollingDirection> direction = null!;
|
||||||
|
|
||||||
|
public DefaultManiaJudgementPiece(HitResult result)
|
||||||
|
: base(result)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(IScrollingInfo scrollingInfo)
|
||||||
|
{
|
||||||
|
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||||
|
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PlayAnimation()
|
||||||
|
{
|
||||||
|
switch (Result)
|
||||||
|
{
|
||||||
|
case HitResult.None:
|
||||||
|
this.FadeOutFromOne(800);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Miss:
|
||||||
|
this.ScaleTo(1.6f);
|
||||||
|
this.ScaleTo(1, 100, Easing.In);
|
||||||
|
|
||||||
|
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
|
||||||
|
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||||
|
|
||||||
|
this.RotateTo(0);
|
||||||
|
this.RotateTo(40, 800, Easing.InQuint);
|
||||||
|
|
||||||
|
this.FadeOutFromOne(800);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.ScaleTo(0.8f);
|
||||||
|
this.ScaleTo(1, 250, Easing.OutElastic);
|
||||||
|
|
||||||
|
this.Delay(50)
|
||||||
|
.ScaleTo(0.75f, 250)
|
||||||
|
.FadeOut(200);
|
||||||
|
|
||||||
|
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,51 +3,32 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
public partial class DrawableManiaJudgement : DrawableJudgement
|
public partial class DrawableManiaJudgement : DrawableJudgement
|
||||||
{
|
{
|
||||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
private IBindable<ScrollingDirection> direction;
|
||||||
|
|
||||||
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(IScrollingInfo scrollingInfo)
|
||||||
{
|
{
|
||||||
public DefaultManiaJudgementPiece(HitResult result)
|
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||||
: base(result)
|
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void PlayAnimation()
|
|
||||||
{
|
|
||||||
switch (Result)
|
|
||||||
{
|
|
||||||
case HitResult.None:
|
|
||||||
case HitResult.Miss:
|
|
||||||
base.PlayAnimation();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.ScaleTo(0.8f);
|
|
||||||
this.ScaleTo(1, 250, Easing.OutElastic);
|
|
||||||
|
|
||||||
this.Delay(50)
|
|
||||||
.ScaleTo(0.75f, 250)
|
|
||||||
.FadeOut(200);
|
|
||||||
|
|
||||||
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onDirectionChanged()
|
||||||
|
{
|
||||||
|
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ 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>
|
||||||
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
public IEnumerable<BarLine> BarLines;
|
public IEnumerable<BarLine> BarLines;
|
||||||
|
|
||||||
|
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
|
||||||
|
|
||||||
protected override bool RelativeScaleBeatLengths => true;
|
protected override bool RelativeScaleBeatLengths => true;
|
||||||
|
|
||||||
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
||||||
@ -110,8 +111,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||||
|
|
||||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
TimeRange.Value = TargetTimeRange = currentTimeRange = 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;
|
||||||
@ -162,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// <returns>The scroll time.</returns>
|
/// <returns>The scroll time.</returns>
|
||||||
public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
|
public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
|
||||||
|
|
||||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
|
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this);
|
||||||
|
|
||||||
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
|
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
|
||||||
|
|
||||||
|
@ -1,17 +1,63 @@
|
|||||||
// 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;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
|
public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
|
||||||
{
|
{
|
||||||
public ManiaPlayfieldAdjustmentContainer()
|
protected override Container<Drawable> Content { get; }
|
||||||
|
|
||||||
|
private readonly DrawSizePreservingFillContainer scalingContainer;
|
||||||
|
|
||||||
|
private readonly DrawableManiaRuleset drawableManiaRuleset;
|
||||||
|
|
||||||
|
public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre;
|
this.drawableManiaRuleset = drawableManiaRuleset;
|
||||||
Origin = Anchor.Centre;
|
InternalChild = scalingContainer = new DrawSizePreservingFillContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = Content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
float aspectRatio = DrawWidth / DrawHeight;
|
||||||
|
bool isPortrait = aspectRatio < 1f;
|
||||||
|
|
||||||
|
if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1)
|
||||||
|
{
|
||||||
|
// Scale playfield up by 25% to become playable on mobile devices,
|
||||||
|
// and leave a 10% horizontal gap if the playfield is scaled down due to being too wide.
|
||||||
|
const float base_scale = 1.25f;
|
||||||
|
const float base_width = 768f / base_scale;
|
||||||
|
const float side_gap = 0.9f;
|
||||||
|
|
||||||
|
scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum;
|
||||||
|
float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth;
|
||||||
|
scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum;
|
||||||
|
scalingContainer.Scale = new Vector2(1f);
|
||||||
|
scalingContainer.Size = new Vector2(1f);
|
||||||
|
scalingContainer.TargetDrawSize = new Vector2(1024, 768);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.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 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
Width = 1366, // Bar lines should only be masked on the vertical axis
|
Width = 1366, // Bar lines should only be masked on the vertical axis
|
||||||
BypassAutoSizeAxes = Axes.Both,
|
BypassAutoSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
Child = barLineContainer = new HitObjectArea(HitObjectContainer)
|
Child = barLineContainer = new HitPositionPaddedContainer
|
||||||
{
|
{
|
||||||
Name = "Bar lines",
|
Name = "Bar lines",
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Child = HitObjectContainer,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columnFlow = new ColumnFlow<Column>(definition)
|
columnFlow = new ColumnFlow<Column>(definition)
|
||||||
@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
judgements = new JudgementContainer<DrawableManiaJudgement>
|
new HitPositionPaddedContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Y = HIT_TARGET_POSITION + 150
|
Child = judgements = new JudgementContainer<DrawableManiaJudgement>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
}
|
}
|
||||||
@ -214,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
judgements.Clear(false);
|
judgements.Clear(false);
|
||||||
judgements.Add(judgementPooler.Get(result.Type, j =>
|
judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!);
|
||||||
{
|
|
||||||
j.Apply(result, judgedObject);
|
|
||||||
|
|
||||||
j.Anchor = Anchor.Centre;
|
|
||||||
j.Origin = Anchor.Centre;
|
|
||||||
})!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
@ -10,7 +10,9 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
@ -261,6 +263,163 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint()
|
||||||
|
{
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Position = new Vector2(100, 100),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint { Type = PathType.LINEAR },
|
||||||
|
new PathControlPoint(new Vector2(100, 0)),
|
||||||
|
new PathControlPoint(new Vector2(100)),
|
||||||
|
new PathControlPoint(new Vector2(0, 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||||
|
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||||
|
|
||||||
|
AddStep("select second node", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("also select third node", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddStep("quick-delete fourth node", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(3));
|
||||||
|
InputManager.Click(MouseButton.Middle);
|
||||||
|
});
|
||||||
|
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||||
|
AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection()
|
||||||
|
{
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Position = new Vector2(100, 100),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint { Type = PathType.LINEAR },
|
||||||
|
new PathControlPoint(new Vector2(100, 0)),
|
||||||
|
new PathControlPoint(new Vector2(100)),
|
||||||
|
new PathControlPoint(new Vector2(0, 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||||
|
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||||
|
|
||||||
|
AddStep("select second node", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("also select third node", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddStep("quick-delete second node", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||||
|
InputManager.Click(MouseButton.Middle);
|
||||||
|
});
|
||||||
|
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||||
|
AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu()
|
||||||
|
{
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Position = new Vector2(100, 100),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint { Type = PathType.LINEAR },
|
||||||
|
new PathControlPoint(new Vector2(50, 100)),
|
||||||
|
new PathControlPoint(new Vector2(145, 100)),
|
||||||
|
},
|
||||||
|
ExpectedDistance = { Value = 162.62 }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||||
|
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||||
|
|
||||||
|
AddStep("select last node", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().Last());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("right click node", () => InputManager.Click(MouseButton.Right));
|
||||||
|
AddUntilStep("context menu open", () => this.ChildrenOfType<ContextMenuContainer>().Single().ChildrenOfType<Menu>().All(m => m.State == MenuState.Open));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath()
|
||||||
|
{
|
||||||
|
var firstSlider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Position = new Vector2(10, 50),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var secondSlider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 500,
|
||||||
|
Position = new Vector2(200, 0),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(-100, 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||||
|
AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider));
|
||||||
|
|
||||||
|
AddStep("move to marker", () =>
|
||||||
|
{
|
||||||
|
var marker = this.ChildrenOfType<SliderEndDragMarker>().First();
|
||||||
|
var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
|
||||||
|
InputManager.MoveMouseTo(position);
|
||||||
|
});
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider));
|
||||||
|
}
|
||||||
|
|
||||||
private ComposeBlueprintContainer blueprintContainer
|
private ComposeBlueprintContainer blueprintContainer
|
||||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||||
|
|
||||||
|
100
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs
Normal file
100
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Replays;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneOsuModRelax : OsuModTestScene
|
||||||
|
{
|
||||||
|
private readonly HitCircle hitObject;
|
||||||
|
private readonly HitWindows hitWindows = new OsuHitWindows();
|
||||||
|
|
||||||
|
public TestSceneOsuModRelax()
|
||||||
|
{
|
||||||
|
hitWindows.SetDifficulty(9);
|
||||||
|
|
||||||
|
hitObject = new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
Position = new Vector2(100, 100),
|
||||||
|
HitWindows = hitWindows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRelax() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModRelax(),
|
||||||
|
Autoplay = false,
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject> { hitObject }
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(0, new Vector2()),
|
||||||
|
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRelaxLeniency() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModRelax(),
|
||||||
|
Autoplay = false,
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject> { hitObject }
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
|
||||||
|
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
|
||||||
|
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||||
|
});
|
||||||
|
|
||||||
|
protected partial class ModRelaxTestPlayer : ModTestPlayer
|
||||||
|
{
|
||||||
|
private readonly ModTestData currentTestData;
|
||||||
|
|
||||||
|
public ModRelaxTestPlayer(ModTestData data, bool allowFail)
|
||||||
|
: base(data, allowFail)
|
||||||
|
{
|
||||||
|
currentTestData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PrepareReplay()
|
||||||
|
{
|
||||||
|
// We need to set IsLegacyScore to true otherwise the mod assumes that presses are already embedded into the replay
|
||||||
|
DrawableRuleset?.SetReplayScore(new Score
|
||||||
|
{
|
||||||
|
Replay = new Replay { Frames = currentTestData.ReplayFrames! },
|
||||||
|
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" }, IsLegacyScore = true, Mods = new Mod[] { new OsuModRelax() } },
|
||||||
|
});
|
||||||
|
|
||||||
|
DrawableRuleset?.SetRecordTarget(Score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures;
|
|||||||
using osu.Framework.Testing;
|
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;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRotation()
|
||||||
|
{
|
||||||
|
createTest(() =>
|
||||||
|
{
|
||||||
|
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true);
|
||||||
|
var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer)
|
||||||
|
{
|
||||||
|
NewPartScale = new Vector2(10)
|
||||||
|
};
|
||||||
|
|
||||||
|
skinContainer.Child = legacyCursorTrail;
|
||||||
|
|
||||||
|
return skinContainer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private readonly IRenderer renderer;
|
private readonly IRenderer renderer;
|
||||||
private readonly bool provideMiddle;
|
private readonly bool provideMiddle;
|
||||||
private readonly bool provideCursor;
|
private readonly bool provideCursor;
|
||||||
|
private readonly bool enableRotation;
|
||||||
|
|
||||||
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
|
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false)
|
||||||
{
|
{
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.provideMiddle = provideMiddle;
|
this.provideMiddle = provideMiddle;
|
||||||
this.provideCursor = provideCursor;
|
this.provideCursor = provideCursor;
|
||||||
|
this.enableRotation = enableRotation;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||||
|
|
||||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||||
|
{
|
||||||
|
switch (lookup)
|
||||||
|
{
|
||||||
|
case OsuSkinConfiguration osuLookup:
|
||||||
|
if (osuLookup == OsuSkinConfiguration.CursorTrailRotate)
|
||||||
|
return SkinUtils.As<TValue>(new BindableBool(enableRotation));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||||
|
|
||||||
@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
|
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class LegacyRotatingCursorTrail : LegacyCursorTrail
|
||||||
|
{
|
||||||
|
public LegacyRotatingCursorTrail([NotNull] ISkin skin)
|
||||||
|
: base(skin)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
PartRotation += (float)(Time.Elapsed * 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
public partial class GridPlacementBlueprint : PlacementBlueprint
|
public partial class GridPlacementBlueprint : PlacementBlueprint
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private HitObjectComposer? hitObjectComposer { get; set; }
|
private OsuHitObjectComposer? hitObjectComposer { get; set; }
|
||||||
|
|
||||||
private OsuGridToolboxGroup gridToolboxGroup = null!;
|
private OsuGridToolboxGroup gridToolboxGroup = null!;
|
||||||
private Vector2 originalOrigin;
|
private Vector2 originalOrigin;
|
||||||
@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
base.OnDragEnd(e);
|
base.OnDragEnd(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapType SnapType => ~SnapType.GlobalGrids;
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
|
||||||
{
|
{
|
||||||
if (State.Value == Visibility.Hidden)
|
if (State.Value == Visibility.Hidden)
|
||||||
return;
|
return new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
|
var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
var pos = ToLocalSpace(result.ScreenSpacePosition);
|
var pos = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
|
|
||||||
@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
|
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||||
@ -15,12 +20,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
|||||||
|
|
||||||
private readonly HitCirclePiece circlePiece;
|
private readonly HitCirclePiece circlePiece;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuHitObjectComposer? composer { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorClock? editorClock { get; set; }
|
||||||
|
|
||||||
|
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||||
|
|
||||||
public HitCirclePlacementBlueprint()
|
public HitCirclePlacementBlueprint()
|
||||||
: base(new HitCircle())
|
: base(new HitCircle())
|
||||||
{
|
{
|
||||||
InternalChild = circlePiece = new HitCirclePiece();
|
InternalChild = circlePiece = new HitCirclePiece();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuConfigManager config)
|
||||||
|
{
|
||||||
|
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -45,10 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
|||||||
return base.OnMouseDown(e);
|
return base.OnMouseDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||||
|
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
|
||||||
|
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
|
||||||
|
result = gridSnapResult;
|
||||||
|
result ??= new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
|
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using System.Collections.Specialized;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -20,6 +21,7 @@ using osu.Framework.Input;
|
|||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -32,7 +34,7 @@ using osuTK.Input;
|
|||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||||
{
|
{
|
||||||
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||||
where T : OsuHitObject, IHasPath
|
where T : OsuHitObject, IHasPath, IHasSliderVelocity
|
||||||
{
|
{
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
|
||||||
|
|
||||||
@ -48,11 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
[CanBeNull]
|
||||||
|
private OsuHitObjectComposer positionSnapProvider { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||||
|
|
||||||
|
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||||
|
|
||||||
public PathControlPointVisualiser(T hitObject, bool allowSelection)
|
public PathControlPointVisualiser(T hitObject, bool allowSelection)
|
||||||
{
|
{
|
||||||
this.hitObject = hitObject;
|
this.hitObject = hitObject;
|
||||||
@ -67,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuConfigManager config)
|
||||||
|
{
|
||||||
|
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -137,11 +148,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns>Whether any change actually took place.</returns>
|
||||||
public bool DeleteSelected()
|
public bool DeleteSelected()
|
||||||
{
|
{
|
||||||
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
||||||
|
|
||||||
|
if (!Delete(toRemove))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||||
|
foreach (var piece in Pieces)
|
||||||
|
piece.IsSelected.Value = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete the specified <see cref="PathControlPoint"/>s.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether any change actually took place.</returns>
|
||||||
|
public bool Delete(List<PathControlPoint> toRemove)
|
||||||
|
{
|
||||||
// Ensure that there are any points to be deleted
|
// Ensure that there are any points to be deleted
|
||||||
if (toRemove.Count == 0)
|
if (toRemove.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
@ -149,11 +176,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
|
|
||||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
|
||||||
foreach (var piece in Pieces)
|
|
||||||
piece.IsSelected.Value = false;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,12 +444,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
{
|
{
|
||||||
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
||||||
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime);
|
||||||
|
result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null);
|
||||||
|
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
|
||||||
|
result = gridSnapResult;
|
||||||
|
result ??= new SnapResult(newHeadPosition, oldStartTime);
|
||||||
|
|
||||||
|
Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position;
|
||||||
|
|
||||||
hitObject.Position += movementDelta;
|
hitObject.Position += movementDelta;
|
||||||
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
|
hitObject.StartTime = result.Time ?? hitObject.StartTime;
|
||||||
|
|
||||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||||
{
|
{
|
||||||
@ -442,7 +469,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||||
{
|
{
|
||||||
@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
base.OnDragEnd(e);
|
base.OnDragEnd(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left;
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e) => true;
|
protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left;
|
||||||
|
|
||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
|
@ -5,10 +5,12 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -25,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
public new Slider HitObject => (Slider)base.HitObject;
|
public new Slider HitObject => (Slider)base.HitObject;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuHitObjectComposer? composer { get; set; }
|
||||||
|
|
||||||
private SliderBodyPiece bodyPiece = null!;
|
private SliderBodyPiece bodyPiece = null!;
|
||||||
private HitCirclePiece headCirclePiece = null!;
|
private HitCirclePiece headCirclePiece = null!;
|
||||||
private HitCirclePiece tailCirclePiece = null!;
|
private HitCirclePiece tailCirclePiece = null!;
|
||||||
@ -40,15 +45,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
private int currentSegmentLength;
|
private int currentSegmentLength;
|
||||||
private bool usingCustomSegmentType;
|
private bool usingCustomSegmentType;
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorClock? editorClock { get; set; }
|
||||||
|
|
||||||
|
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||||
|
|
||||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||||
@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
};
|
};
|
||||||
|
|
||||||
state = SliderPlacementState.Initial;
|
state = SliderPlacementState.Initial;
|
||||||
|
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -106,9 +114,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||||
|
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
|
||||||
|
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
|
||||||
|
result = gridSnapResult;
|
||||||
|
result ??= new SnapResult(screenSpacePosition, fallbackTime);
|
||||||
|
|
||||||
|
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||||
|
|
||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
@ -131,6 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
updateCursor();
|
updateCursor();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
@ -375,7 +391,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private Vector2 getCursorPosition()
|
private Vector2 getCursorPosition()
|
||||||
{
|
{
|
||||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
|
SnapResult? result = null;
|
||||||
|
var mousePosition = inputManager.CurrentState.Mouse.Position;
|
||||||
|
|
||||||
|
if (state != SliderPlacementState.ControlPoints)
|
||||||
|
{
|
||||||
|
result ??= composer?.TrySnapToNearbyObjects(mousePosition);
|
||||||
|
result ??= composer?.TrySnapToDistanceGrid(mousePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
result ??= composer?.TrySnapToPositionGrid(mousePosition);
|
||||||
|
|
||||||
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
if (state == SliderPlacementState.Drawing)
|
if (state == SliderPlacementState.Drawing)
|
||||||
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
||||||
else
|
else
|
||||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
|
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance;
|
||||||
|
|
||||||
bodyPiece.UpdateFrom(HitObject);
|
bodyPiece.UpdateFrom(HitObject);
|
||||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||||
|
@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
if (hoveredControlPoint == null)
|
if (hoveredControlPoint == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
hoveredControlPoint.IsSelected.Value = true;
|
if (hoveredControlPoint.IsSelected.Value)
|
||||||
ControlPointVisualiser?.DeleteSelected();
|
ControlPointVisualiser?.DeleteSelected();
|
||||||
|
else
|
||||||
|
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
|
||||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
|
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||||
{
|
{
|
||||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
|
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (ControlPointVisualiser == null)
|
if (ControlPointVisualiser == null)
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
@ -8,16 +14,27 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
|||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
public partial class OsuBlueprintContainer : ComposeBlueprintContainer
|
public partial class OsuBlueprintContainer : ComposeBlueprintContainer
|
||||||
{
|
{
|
||||||
public OsuBlueprintContainer(HitObjectComposer composer)
|
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||||
|
|
||||||
|
public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer;
|
||||||
|
|
||||||
|
public OsuBlueprintContainer(OsuHitObjectComposer composer)
|
||||||
: base(composer)
|
: base(composer)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuConfigManager config)
|
||||||
|
{
|
||||||
|
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||||
|
}
|
||||||
|
|
||||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
|
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
|
||||||
|
|
||||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||||
@ -36,5 +53,68 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
return base.CreateHitObjectBlueprintFor(hitObject);
|
return base.CreateHitObjectBlueprintFor(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||||
|
{
|
||||||
|
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||||
|
|
||||||
|
for (int i = 0; i < blueprints.Count; i++)
|
||||||
|
{
|
||||||
|
if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no positional snapping could be performed, try unrestricted snapping from the earliest
|
||||||
|
// item in the selection.
|
||||||
|
|
||||||
|
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||||
|
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||||
|
var referenceBlueprint = blueprints.First().blueprint;
|
||||||
|
|
||||||
|
// Retrieve a snapped position.
|
||||||
|
var result = Composer.TrySnapToNearbyObjects(movePosition);
|
||||||
|
result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null);
|
||||||
|
if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult)
|
||||||
|
result = gridSnapResult;
|
||||||
|
result ??= new SnapResult(movePosition, null);
|
||||||
|
|
||||||
|
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||||
|
if (moved)
|
||||||
|
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||||
|
return moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check for positional snap for given blueprint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="blueprint">The blueprint to check for snapping.</param>
|
||||||
|
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
|
||||||
|
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
|
||||||
|
/// <returns>Whether an object to snap to was found.</returns>
|
||||||
|
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<HitObject> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
|
||||||
|
{
|
||||||
|
var currentPositions = blueprint.ScreenSpaceSnapPoints;
|
||||||
|
|
||||||
|
for (int i = 0; i < originalPositions.Length; i++)
|
||||||
|
{
|
||||||
|
Vector2 originalPosition = originalPositions[i];
|
||||||
|
var testPosition = originalPosition + distanceTravelled;
|
||||||
|
|
||||||
|
var positionalResult = Composer.TrySnapToNearbyObjects(testPosition);
|
||||||
|
|
||||||
|
if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||||
|
|
||||||
|
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
|
||||||
|
|
||||||
|
// attempt to move the objects, and apply any time based snapping if we can.
|
||||||
|
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(blueprint, delta)))
|
||||||
|
{
|
||||||
|
ApplySnapResultTime(positionalResult, blueprint.Item.StartTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
|
||||||
@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||||
{
|
{
|
||||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
|
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
|
||||||
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1)
|
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource)
|
||||||
{
|
{
|
||||||
Masking = true;
|
Masking = true;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||||
{
|
{
|
||||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
|
||||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||||
|
|
||||||
return actualDistance / expectedDistance;
|
return actualDistance / expectedDistance;
|
||||||
|
@ -7,6 +7,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
@ -22,6 +23,7 @@ using osu.Game.Rulesets.Edit;
|
|||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -31,6 +33,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
|
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
|
||||||
{
|
{
|
||||||
public OsuHitObjectComposer(Ruleset ruleset)
|
public OsuHitObjectComposer(Ruleset ruleset)
|
||||||
@ -53,9 +56,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
=> base.CreateTernaryButtons()
|
=> base.CreateTernaryButtons()
|
||||||
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
|
.Append(new DrawableTernaryButton
|
||||||
|
{
|
||||||
|
Current = rectangularGridSnapToggle,
|
||||||
|
Description = "Grid Snap",
|
||||||
|
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap },
|
||||||
|
})
|
||||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||||
|
|
||||||
private BindableList<HitObject> selectedHitObjects;
|
private BindableList<HitObject> selectedHitObjects;
|
||||||
@ -173,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||||
string[] splitDescription = objectDescription.Split(',').ToArray();
|
string[] splitDescription = objectDescription.Split(',');
|
||||||
|
|
||||||
for (int i = 0; i < splitDescription.Length; i++)
|
for (int i = 0; i < splitDescription.Length; i++)
|
||||||
{
|
{
|
||||||
@ -217,56 +225,56 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
[CanBeNull]
|
||||||
|
public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null)
|
||||||
{
|
{
|
||||||
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||||
{
|
return null;
|
||||||
// In the case of snapping to nearby objects, a time value is not provided.
|
|
||||||
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
|
|
||||||
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
|
|
||||||
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
|
|
||||||
//
|
|
||||||
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
|
|
||||||
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
|
|
||||||
// the time value if the proposed positions are roughly the same.
|
|
||||||
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
|
||||||
{
|
|
||||||
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
|
|
||||||
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
|
|
||||||
snapResult.Time = distanceSnappedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||||
return snapResult;
|
return snapResult;
|
||||||
}
|
|
||||||
|
|
||||||
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
// In the case of snapping to nearby objects, a time value is not provided.
|
||||||
|
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
|
||||||
|
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
|
||||||
|
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
|
||||||
|
//
|
||||||
|
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
|
||||||
|
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
|
||||||
|
// the time value if the proposed positions are roughly the same.
|
||||||
|
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
|
||||||
|
snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)
|
||||||
|
? distanceSnappedTime
|
||||||
|
: fallbackTime;
|
||||||
|
|
||||||
if (snapType.HasFlag(SnapType.RelativeGrids))
|
return snapResult;
|
||||||
{
|
}
|
||||||
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
|
||||||
{
|
|
||||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
|
||||||
|
|
||||||
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
|
[CanBeNull]
|
||||||
result.Time = time;
|
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
|
||||||
}
|
{
|
||||||
}
|
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (snapType.HasFlag(SnapType.GlobalGrids))
|
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||||
{
|
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||||
{
|
}
|
||||||
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
|
||||||
|
|
||||||
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
|
[CanBeNull]
|
||||||
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
|
public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null)
|
||||||
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
{
|
||||||
|
if (rectangularGridSnapToggle.Value != TernaryState.True)
|
||||||
|
return null;
|
||||||
|
|
||||||
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos);
|
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
|
||||||
|
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
|
||||||
|
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||||
|
|
||||||
|
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||||
|
return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||||
@ -399,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
|
||||||
|
|
||||||
int sourceIndex = -1;
|
int positionSourceObjectIndex = -1;
|
||||||
|
IHasSliderVelocity sliderVelocitySource = null;
|
||||||
|
|
||||||
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
|
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
|
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
sourceIndex = i;
|
positionSourceObjectIndex = i;
|
||||||
|
|
||||||
|
if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity)
|
||||||
|
sliderVelocitySource = hasSliderVelocity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceIndex == -1)
|
if (positionSourceObjectIndex == -1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
|
HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex];
|
||||||
|
|
||||||
int targetIndex = sourceIndex + targetOffset;
|
int targetIndex = positionSourceObjectIndex + targetOffset;
|
||||||
HitObject targetObject = null;
|
HitObject targetObject = null;
|
||||||
|
|
||||||
// Keep advancing the target object while its start time falls before the end time of the source object
|
// Keep advancing the target object while its start time falls before the end time of the source object
|
||||||
@ -435,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (sourceObject is Spinner)
|
if (sourceObject is Spinner)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
|
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
186
osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs
Normal file
186
osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
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.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PreciseMovementPopover : OsuPopover
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
private readonly Dictionary<HitObject, Vector2> initialPositions = new Dictionary<HitObject, Vector2>();
|
||||||
|
private RectangleF initialSurroundingQuad;
|
||||||
|
|
||||||
|
private BindableNumber<float> xBindable = null!;
|
||||||
|
private BindableNumber<float> yBindable = null!;
|
||||||
|
|
||||||
|
private SliderWithTextBoxInput<float> xInput = null!;
|
||||||
|
private OsuCheckbox relativeCheckbox = null!;
|
||||||
|
|
||||||
|
public PreciseMovementPopover()
|
||||||
|
{
|
||||||
|
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[]
|
||||||
|
{
|
||||||
|
xInput = new SliderWithTextBoxInput<float>("X:")
|
||||||
|
{
|
||||||
|
Current = xBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
new SliderWithTextBoxInput<float>("Y:")
|
||||||
|
{
|
||||||
|
Current = yBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
relativeCheckbox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "Relative movement",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() => xInput.TakeFocus());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
editorBeatmap.BeginChange();
|
||||||
|
initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair<HitObject, Vector2>(ho, ((IHasPosition)ho).Position)));
|
||||||
|
initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast<IHasPosition>()).AABBFloat;
|
||||||
|
|
||||||
|
Debug.Assert(initialPositions.Count > 0);
|
||||||
|
|
||||||
|
if (initialPositions.Count > 1)
|
||||||
|
{
|
||||||
|
relativeCheckbox.Current.Value = true;
|
||||||
|
relativeCheckbox.Current.Disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true);
|
||||||
|
xBindable.BindValueChanged(_ => applyPosition());
|
||||||
|
yBindable.BindValueChanged(_ => applyPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
if (IsLoaded) editorBeatmap.EndChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void relativeChanged()
|
||||||
|
{
|
||||||
|
// reset bindable bounds to something that is guaranteed to be larger than any previous value.
|
||||||
|
// this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic -
|
||||||
|
// if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue.
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||||
|
|
||||||
|
float previousX = xBindable.Value;
|
||||||
|
float previousY = yBindable.Value;
|
||||||
|
|
||||||
|
if (relativeCheckbox.Current.Value)
|
||||||
|
{
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||||
|
|
||||||
|
xBindable.Default = yBindable.Default = 0;
|
||||||
|
|
||||||
|
if (initialPositions.Count == 1)
|
||||||
|
{
|
||||||
|
var initialPosition = initialPositions.Single().Value;
|
||||||
|
xBindable.Value = previousX - initialPosition.X;
|
||||||
|
yBindable.Value = previousY - initialPosition.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(initialPositions.Count == 1);
|
||||||
|
var initialPosition = initialPositions.Single().Value;
|
||||||
|
|
||||||
|
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||||
|
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||||
|
|
||||||
|
xBindable.Default = initialPosition.X;
|
||||||
|
yBindable.Default = initialPosition.Y;
|
||||||
|
|
||||||
|
xBindable.Value = xBindable.Default + previousX;
|
||||||
|
yBindable.Value = yBindable.Default + previousY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPosition()
|
||||||
|
{
|
||||||
|
editorBeatmap.PerformOnSelection(ho =>
|
||||||
|
{
|
||||||
|
if (!initialPositions.TryGetValue(ho, out var initialPosition))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pos = new Vector2(xBindable.Value, yBindable.Value);
|
||||||
|
if (relativeCheckbox.Current.Value)
|
||||||
|
((IHasPosition)ho).Position = initialPosition + pos;
|
||||||
|
else
|
||||||
|
((IHasPosition)ho).Position = pos;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == GlobalAction.Select && !e.Repeat)
|
||||||
|
{
|
||||||
|
this.HidePopover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() =>
|
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
||||||
{
|
|
||||||
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 });
|
||||||
|
|
||||||
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||||
|
@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() =>
|
ScheduleAfterChildren(() => scaleInput.TakeFocus());
|
||||||
{
|
|
||||||
scaleInput.TakeFocus();
|
|
||||||
scaleInput.SelectAll();
|
|
||||||
});
|
|
||||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
xCheckBox.Current.BindValueChanged(_ =>
|
xCheckBox.Current.BindValueChanged(_ =>
|
||||||
|
@ -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.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
|
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||||
|
private readonly BindableBool canMove = new BindableBool();
|
||||||
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
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 readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
|
||||||
|
private EditorToolButton moveButton = null!;
|
||||||
private EditorToolButton rotateButton = null!;
|
private EditorToolButton rotateButton = null!;
|
||||||
private EditorToolButton scaleButton = null!;
|
private EditorToolButton scaleButton = null!;
|
||||||
|
|
||||||
@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
{
|
{
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Spacing = new Vector2(5),
|
Spacing = new Vector2(5),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
moveButton = new EditorToolButton("Move",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
|
() => new PreciseMovementPopover()),
|
||||||
rotateButton = new EditorToolButton("Rotate",
|
rotateButton = new EditorToolButton("Rotate",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||||
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||||
scaleButton = new EditorToolButton("Scale",
|
scaleButton = new EditorToolButton("Scale",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt },
|
||||||
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true);
|
||||||
|
|
||||||
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||||
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
|
|
||||||
@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
// 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.
|
||||||
|
canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true);
|
||||||
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||||
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||||
}
|
}
|
||||||
@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
|
case GlobalAction.EditorToggleMoveControl:
|
||||||
|
{
|
||||||
|
moveButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case GlobalAction.EditorToggleRotateControl:
|
case GlobalAction.EditorToggleRotateControl:
|
||||||
{
|
{
|
||||||
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override LocalisableString Description => "Flip objects on the chosen axes.";
|
public override LocalisableString Description => "Flip objects on the chosen axes.";
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
|
||||||
|
|
||||||
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
|
[SettingSource("Flipped axes")]
|
||||||
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
|
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
|
||||||
|
|
||||||
public void ApplyToHitObject(HitObject hitObject)
|
public void ApplyToHitObject(HitObject hitObject)
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// How early before a hitobject's start time to trigger a hit.
|
/// How early before a hitobject's start time to trigger a hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const float relax_leniency = 3;
|
public const float RELAX_LENIENCY = 12;
|
||||||
|
|
||||||
private bool isDownState;
|
private bool isDownState;
|
||||||
private bool wasLeft;
|
private bool wasLeft;
|
||||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
||||||
{
|
{
|
||||||
// we are not yet close enough to the object.
|
// we are not yet close enough to the object.
|
||||||
if (time < h.HitObject.StartTime - relax_leniency)
|
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// already hit or beyond the hittable end time.
|
// already hit or beyond the hittable end time.
|
||||||
|
@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
// If samples aren't available at the exact start time of the object,
|
// If samples aren't available at the exact start time of the object,
|
||||||
// use samples (without additions) in the closest original hit object instead
|
// use samples (without additions) in the closest original hit object instead
|
||||||
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList();
|
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.ALL_ADDITIONS.Contains(s.Name)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,13 +377,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
UpdateState(ArmedState.Idle);
|
UpdateState(ArmedState.Idle);
|
||||||
HeadCircle.SuppressHitAnimations();
|
HeadCircle.SuppressHitAnimations();
|
||||||
|
|
||||||
|
foreach (var repeat in repeatContainer)
|
||||||
|
repeat.SuppressHitAnimations();
|
||||||
|
|
||||||
TailCircle.SuppressHitAnimations();
|
TailCircle.SuppressHitAnimations();
|
||||||
|
|
||||||
|
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||||
|
|
||||||
|
if (Time.Current >= HitStateUpdateTime)
|
||||||
|
{
|
||||||
|
// Apply the slider's alpha to *only* the body.
|
||||||
|
// This allows start and – more importantly – end circles to fade slower than the overall slider.
|
||||||
|
if (Alpha < 1)
|
||||||
|
Body.Alpha = Alpha;
|
||||||
|
Alpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LifetimeEnd = HitStateUpdateTime + 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RestoreHitAnimations()
|
internal void RestoreHitAnimations()
|
||||||
{
|
{
|
||||||
UpdateState(ArmedState.Hit);
|
UpdateState(ArmedState.Hit);
|
||||||
HeadCircle.RestoreHitAnimations();
|
HeadCircle.RestoreHitAnimations();
|
||||||
|
|
||||||
|
foreach (var repeat in repeatContainer)
|
||||||
|
repeat.RestoreHitAnimations();
|
||||||
|
|
||||||
TailCircle.RestoreHitAnimations();
|
TailCircle.RestoreHitAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Slider slider = drawableSlider.HitObject;
|
Slider slider = drawableSlider.HitObject;
|
||||||
Position = slider.CurvePositionAt(completionProgress);
|
Position = slider.CurvePositionAt(completionProgress);
|
||||||
|
|
||||||
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
// 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));
|
double checkDistance = 0.1 / slider.Path.Distance;
|
||||||
|
var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance));
|
||||||
|
|
||||||
// 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.
|
// Needed for when near completion, or in case of a very short slider.
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -163,5 +164,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
|
||||||
|
|
||||||
|
internal void SuppressHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Idle);
|
||||||
|
UpdateComboColour();
|
||||||
|
|
||||||
|
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||||
|
|
||||||
|
bool hit = Time.Current >= HitStateUpdateTime;
|
||||||
|
|
||||||
|
if (hit)
|
||||||
|
{
|
||||||
|
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
|
||||||
|
AccentColour.Value = Color4.White;
|
||||||
|
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrow.Alpha = hit ? 0 : 1;
|
||||||
|
|
||||||
|
LifetimeEnd = HitStateUpdateTime + 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RestoreHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Hit);
|
||||||
|
UpdateComboColour();
|
||||||
|
Arrow.Alpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set => position.Value = value;
|
set => position.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
get => Position.X;
|
||||||
|
set => Position = new Vector2(value, Position.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Y
|
||||||
|
{
|
||||||
|
get => Position.Y;
|
||||||
|
set => Position = new Vector2(Position.X, value);
|
||||||
|
}
|
||||||
|
|
||||||
public Vector2 StackedPosition => Position + StackOffset;
|
public Vector2 StackedPosition => Position + StackOffset;
|
||||||
|
|
||||||
@ -175,27 +184,26 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
||||||
// If a change is made here, CatchHitObject.cs should also be updated.
|
// If a change is made here, CatchHitObject.cs should also be updated.
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||||
|
|
||||||
if (this is Spinner)
|
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||||
|
// - At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
|
||||||
|
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||||
|
if (this is not Spinner && (NewCombo || lastObj == null || lastObj is Spinner))
|
||||||
{
|
{
|
||||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
inCurrentCombo = 0;
|
||||||
return;
|
index++;
|
||||||
}
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
|
|
||||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
|
||||||
if (NewCombo || lastObj == null || lastObj is Spinner)
|
|
||||||
{
|
|
||||||
IndexInCurrentCombo = 0;
|
|
||||||
ComboIndex++;
|
|
||||||
ComboIndexWithOffsets += ComboOffset + 1;
|
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||||
|
@ -5,12 +5,12 @@ using System;
|
|||||||
using osu.Framework.Allocation;
|
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.Extensions.ObjectExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
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;
|
||||||
@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||||
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
||||||
|
|
||||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
protected override void Update()
|
||||||
{
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||||
|
{
|
||||||
|
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||||
|
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Scale = Vector2.One;
|
||||||
|
|
||||||
const float move_distance = -12;
|
const float move_distance = -12;
|
||||||
|
const float scale_amount = 1.3f;
|
||||||
|
|
||||||
const double move_out_duration = 35;
|
const double move_out_duration = 35;
|
||||||
const double move_in_duration = 250;
|
const double move_in_duration = 250;
|
||||||
const double total = 300;
|
const double total = 300;
|
||||||
|
|
||||||
switch (state)
|
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||||
{
|
|
||||||
case ArmedState.Idle:
|
|
||||||
main.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
|
||||||
.Then()
|
|
||||||
.ScaleTo(1f, move_in_duration, Easing.Out)
|
|
||||||
.Loop(total - (move_in_duration + move_out_duration));
|
|
||||||
side
|
|
||||||
.MoveToX(move_distance, move_out_duration, Easing.Out)
|
|
||||||
.Then()
|
|
||||||
.MoveToX(0, move_in_duration, Easing.Out)
|
|
||||||
.Loop(total - (move_in_duration + move_out_duration));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ArmedState.Hit:
|
if (loopCurrentTime < move_out_duration)
|
||||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||||
this.ScaleTo(1.5f, animDuration, Easing.Out);
|
else
|
||||||
break;
|
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
if (loopCurrentTime < move_out_duration)
|
||||||
{
|
side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out);
|
||||||
base.Dispose(isDisposing);
|
else
|
||||||
|
side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out);
|
||||||
if (drawableRepeat.IsNotNull())
|
|
||||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
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;
|
||||||
@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
private void load(DrawableHitObject drawableObject)
|
private void load(DrawableHitObject drawableObject)
|
||||||
{
|
{
|
||||||
drawableRepeat = (DrawableSliderRepeat)drawableObject;
|
drawableRepeat = (DrawableSliderRepeat)drawableObject;
|
||||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
protected override void Update()
|
||||||
{
|
{
|
||||||
const double move_out_duration = 35;
|
base.Update();
|
||||||
const double move_in_duration = 250;
|
|
||||||
const double total = 300;
|
|
||||||
|
|
||||||
switch (state)
|
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||||
{
|
{
|
||||||
case ArmedState.Idle:
|
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||||
InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||||
.Then()
|
|
||||||
.ScaleTo(1f, move_in_duration, Easing.Out)
|
|
||||||
.Loop(total - (move_in_duration + move_out_duration));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ArmedState.Hit:
|
|
||||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
|
||||||
InternalChild.ScaleTo(1.5f, animDuration, Easing.Out);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
const float scale_amount = 1.3f;
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
const double move_out_duration = 35;
|
||||||
{
|
const double move_in_duration = 250;
|
||||||
base.Dispose(isDisposing);
|
const double total = 300;
|
||||||
|
|
||||||
if (drawableRepeat.IsNotNull())
|
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
if (loopCurrentTime < move_out_duration)
|
||||||
|
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||||
|
else
|
||||||
|
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
@ -13,8 +14,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
public abstract partial class FollowCircle : CompositeDrawable
|
public abstract partial class FollowCircle : CompositeDrawable
|
||||||
{
|
{
|
||||||
[Resolved]
|
protected DrawableSlider? DrawableObject { get; private set; }
|
||||||
protected DrawableHitObject? ParentObject { get; private set; }
|
|
||||||
|
private readonly IBindable<bool> tracking = new Bindable<bool>();
|
||||||
|
|
||||||
protected FollowCircle()
|
protected FollowCircle()
|
||||||
{
|
{
|
||||||
@ -22,65 +24,73 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(DrawableHitObject? hitObject)
|
||||||
{
|
{
|
||||||
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking =>
|
DrawableObject = hitObject as DrawableSlider;
|
||||||
|
|
||||||
|
if (DrawableObject != null)
|
||||||
{
|
{
|
||||||
Debug.Assert(ParentObject != null);
|
tracking.BindTo(DrawableObject.Tracking);
|
||||||
|
tracking.BindValueChanged(tracking =>
|
||||||
if (ParentObject.Judged)
|
|
||||||
return;
|
|
||||||
|
|
||||||
using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0)))
|
|
||||||
{
|
{
|
||||||
if (tracking.NewValue)
|
if (DrawableObject.Judged)
|
||||||
OnSliderPress();
|
return;
|
||||||
else
|
|
||||||
OnSliderRelease();
|
using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0)))
|
||||||
}
|
{
|
||||||
}, true);
|
if (tracking.NewValue)
|
||||||
|
OnSliderPress();
|
||||||
|
else
|
||||||
|
OnSliderRelease();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
if (ParentObject != null)
|
if (DrawableObject != null)
|
||||||
{
|
{
|
||||||
ParentObject.HitObjectApplied += onHitObjectApplied;
|
DrawableObject.HitObjectApplied += onHitObjectApplied;
|
||||||
onHitObjectApplied(ParentObject);
|
onHitObjectApplied(DrawableObject);
|
||||||
|
|
||||||
ParentObject.ApplyCustomUpdateState += updateStateTransforms;
|
DrawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
updateStateTransforms(ParentObject, ParentObject.State.Value);
|
updateStateTransforms(DrawableObject, DrawableObject.State.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||||
{
|
{
|
||||||
|
// Sane defaults when a new hitobject is applied to the drawable slider.
|
||||||
this.ScaleTo(1f)
|
this.ScaleTo(1f)
|
||||||
.FadeOut();
|
.FadeOut();
|
||||||
|
|
||||||
|
// Immediately play out any pending transforms from press/release
|
||||||
|
FinishTransforms(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
|
private void updateStateTransforms(DrawableHitObject d, ArmedState state)
|
||||||
{
|
{
|
||||||
Debug.Assert(ParentObject != null);
|
Debug.Assert(DrawableObject != null);
|
||||||
|
|
||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case ArmedState.Hit:
|
case ArmedState.Hit:
|
||||||
switch (drawableObject)
|
switch (d)
|
||||||
{
|
{
|
||||||
case DrawableSliderTail:
|
case DrawableSliderTail:
|
||||||
// Use ParentObject instead of drawableObject because slider tail's
|
// Use DrawableObject instead of local object because slider tail's
|
||||||
// HitStateUpdateTime is ~36ms before the actual slider end (aka slider
|
// HitStateUpdateTime is ~36ms before the actual slider end (aka slider
|
||||||
// tail leniency)
|
// tail leniency)
|
||||||
using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime))
|
using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime))
|
||||||
OnSliderEnd();
|
OnSliderEnd();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableSliderTick:
|
case DrawableSliderTick:
|
||||||
case DrawableSliderRepeat:
|
case DrawableSliderRepeat:
|
||||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
|
||||||
OnSliderTick();
|
OnSliderTick();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -88,15 +98,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ArmedState.Miss:
|
case ArmedState.Miss:
|
||||||
switch (drawableObject)
|
switch (d)
|
||||||
{
|
{
|
||||||
case DrawableSliderTail:
|
case DrawableSliderTail:
|
||||||
case DrawableSliderTick:
|
case DrawableSliderTick:
|
||||||
case DrawableSliderRepeat:
|
case DrawableSliderRepeat:
|
||||||
// Despite above comment, ok to use drawableObject.HitStateUpdateTime
|
// Despite above comment, ok to use d.HitStateUpdateTime
|
||||||
// here, since on stable, the break anim plays right when the tail is
|
// here, since on stable, the break anim plays right when the tail is
|
||||||
// missed, not when the slider ends
|
// missed, not when the slider ends
|
||||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
|
||||||
OnSliderBreak();
|
OnSliderBreak();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -109,10 +119,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (ParentObject != null)
|
if (DrawableObject != null)
|
||||||
{
|
{
|
||||||
ParentObject.HitObjectApplied -= onHitObjectApplied;
|
DrawableObject.HitObjectApplied -= onHitObjectApplied;
|
||||||
ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
DrawableObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyCursor : SkinnableCursor
|
public partial class LegacyCursor : SkinnableCursor
|
||||||
{
|
{
|
||||||
|
public static readonly int REVOLUTION_DURATION = 10000;
|
||||||
|
|
||||||
private const float pressed_scale = 1.3f;
|
private const float pressed_scale = 1.3f;
|
||||||
private const float released_scale = 1f;
|
private const float released_scale = 1f;
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
if (spin)
|
if (spin)
|
||||||
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
|
ExpandTarget.Spin(REVOLUTION_DURATION, RotationDirection.Clockwise);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Expand()
|
public override void Expand()
|
||||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private void load(OsuConfigManager config, ISkinSource skinSource)
|
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||||
{
|
{
|
||||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||||
|
AllowPartRotation = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true;
|
||||||
|
|
||||||
Texture = skin.GetTexture("cursortrail");
|
Texture = skin.GetTexture("cursortrail");
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
protected override void OnSliderPress()
|
protected override void OnSliderPress()
|
||||||
{
|
{
|
||||||
Debug.Assert(ParentObject != null);
|
Debug.Assert(DrawableObject != null);
|
||||||
|
|
||||||
double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
|
double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current);
|
||||||
|
|
||||||
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
|
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
|
||||||
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
|
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
|
||||||
|
@ -61,13 +61,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
|
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
|
||||||
|
|
||||||
// As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle".
|
// As a precondition, prefer that any *prefix* lookups are run against the skin which is providing "hitcircle".
|
||||||
// This is to correctly handle a case such as:
|
// This is to correctly handle a case such as:
|
||||||
//
|
//
|
||||||
// - Beatmap provides `hitcircle`
|
// - Beatmap provides `hitcircle`
|
||||||
// - User skin provides `sliderstartcircle`
|
// - User skin provides `sliderstartcircle`
|
||||||
//
|
//
|
||||||
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
|
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
|
||||||
|
//
|
||||||
|
// Of note, this consideration should only be used to decide whether to continue looking up the prefixed name or not.
|
||||||
|
// The final lookups must still run on the full skin hierarchy as per usual in order to correctly handle fallback cases.
|
||||||
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
|
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
|
||||||
|
|
||||||
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
|
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
|
||||||
@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
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.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
@ -51,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
||||||
|
|
||||||
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
|
||||||
|
|
||||||
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
|
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||||
accentColour.BindValueChanged(c =>
|
accentColour.BindValueChanged(c =>
|
||||||
{
|
{
|
||||||
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
|
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > 600 / 255f ? Color4.Black : Color4.White;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,36 +80,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
|
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
protected override void Update()
|
||||||
{
|
{
|
||||||
const double duration = 300;
|
base.Update();
|
||||||
const float rotation = 5.625f;
|
|
||||||
|
|
||||||
switch (state)
|
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||||
{
|
{
|
||||||
case ArmedState.Idle:
|
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||||
if (shouldRotate)
|
arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||||
{
|
}
|
||||||
InternalChild.ScaleTo(1.3f)
|
else
|
||||||
.RotateTo(rotation)
|
{
|
||||||
.Then()
|
const double duration = 300;
|
||||||
.ScaleTo(1f, duration)
|
const float rotation = 5.625f;
|
||||||
.RotateTo(-rotation, duration)
|
|
||||||
.Loop();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
InternalChild.ScaleTo(1.3f).Then()
|
|
||||||
.ScaleTo(1f, duration, Easing.Out)
|
|
||||||
.Loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration;
|
||||||
|
|
||||||
case ArmedState.Hit:
|
// Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96
|
||||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
if (shouldRotate)
|
||||||
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out);
|
{
|
||||||
break;
|
arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration);
|
||||||
|
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration, Easing.Out));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +116,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
if (drawableRepeat.IsNotNull())
|
if (drawableRepeat.IsNotNull())
|
||||||
{
|
{
|
||||||
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
|
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
|
||||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
}
|
}
|
||||||
|
|
||||||
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
|
Vector2 pos = new Vector2();
|
||||||
|
|
||||||
if (combo != null)
|
if (combo != null)
|
||||||
{
|
{
|
||||||
combo.Anchor = Anchor.BottomLeft;
|
combo.Anchor = Anchor.BottomLeft;
|
||||||
combo.Origin = Anchor.BottomLeft;
|
combo.Origin = Anchor.BottomLeft;
|
||||||
combo.Scale = new Vector2(1.28f);
|
combo.Scale = new Vector2(1.28f);
|
||||||
|
|
||||||
|
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spectatorList != null)
|
||||||
|
{
|
||||||
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
|
spectatorList.Origin = Anchor.BottomLeft;
|
||||||
|
spectatorList.Position = pos;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
new LegacyDefaultComboCounter(),
|
new LegacyDefaultComboCounter(),
|
||||||
new LegacyKeyCounterDisplay(),
|
new LegacyKeyCounterDisplay(),
|
||||||
|
new SpectatorList(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user