mirror of
https://github.com/osukey/osukey.git
synced 2025-05-19 12:37:38 +09:00
Merge branch 'master' into editor-background
This commit is contained in:
commit
c3eb0c6330
66
Gemfile.lock
66
Gemfile.lock
@ -3,25 +3,25 @@ GEM
|
|||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.5)
|
CFPropertyList (3.0.5)
|
||||||
rexml
|
rexml
|
||||||
addressable (2.8.0)
|
addressable (2.8.1)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
artifactory (3.0.15)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.601.0)
|
aws-partitions (1.653.0)
|
||||||
aws-sdk-core (3.131.2)
|
aws-sdk-core (3.166.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.525.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.5)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.57.0)
|
aws-sdk-kms (1.59.0)
|
||||||
aws-sdk-core (~> 3, >= 3.127.0)
|
aws-sdk-core (~> 3, >= 3.165.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.114.0)
|
aws-sdk-s3 (1.117.1)
|
||||||
aws-sdk-core (~> 3, >= 3.127.0)
|
aws-sdk-core (~> 3, >= 3.165.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.5.0)
|
aws-sigv4 (1.5.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
@ -34,10 +34,10 @@ GEM
|
|||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.6)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.92.3)
|
excon (0.93.1)
|
||||||
faraday (1.10.0)
|
faraday (1.10.2)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@ -66,7 +66,7 @@ GEM
|
|||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.6)
|
fastimage (2.2.6)
|
||||||
fastlane (2.206.2)
|
fastlane (2.210.1)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
@ -110,9 +110,9 @@ GEM
|
|||||||
souyuz (= 0.11.1)
|
souyuz (= 0.11.1)
|
||||||
fastlane-plugin-xamarin (0.6.3)
|
fastlane-plugin-xamarin (0.6.3)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.23.0)
|
google-apis-androidpublisher_v3 (0.29.0)
|
||||||
google-apis-core (>= 0.6, < 2.a)
|
google-apis-core (>= 0.9.0, < 2.a)
|
||||||
google-apis-core (0.6.0)
|
google-apis-core (0.9.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@ -121,27 +121,27 @@ GEM
|
|||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.12.0)
|
google-apis-iamcredentials_v1 (0.15.0)
|
||||||
google-apis-core (>= 0.6, < 2.a)
|
google-apis-core (>= 0.9.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.9.0)
|
google-apis-playcustomapp_v1 (0.12.0)
|
||||||
google-apis-core (>= 0.6, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-storage_v1 (0.16.0)
|
google-apis-storage_v1 (0.19.0)
|
||||||
google-apis-core (>= 0.6, < 2.a)
|
google-apis-core (>= 0.9.0, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.2.0)
|
google-cloud-errors (1.3.0)
|
||||||
google-cloud-storage (1.36.2)
|
google-cloud-storage (1.43.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.1)
|
google-apis-storage_v1 (~> 0.19.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.2.0)
|
googleauth (1.3.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
@ -154,22 +154,22 @@ GEM
|
|||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
jmespath (1.6.1)
|
jmespath (1.6.1)
|
||||||
json (2.6.2)
|
json (2.6.2)
|
||||||
jwt (2.4.1)
|
jwt (2.5.0)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.7.1)
|
mini_portile2 (2.8.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
nanaimo (0.3.0)
|
nanaimo (0.3.0)
|
||||||
naturally (2.2.1)
|
naturally (2.2.1)
|
||||||
nokogiri (1.13.1)
|
nokogiri (1.13.9)
|
||||||
mini_portile2 (~> 2.7.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
optparse (0.1.1)
|
optparse (0.1.1)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.6.0)
|
plist (3.6.0)
|
||||||
public_suffix (4.0.7)
|
public_suffix (5.0.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
|
@ -138,10 +138,10 @@ platform :ios do
|
|||||||
end
|
end
|
||||||
|
|
||||||
lane :testflight_prune_dry do
|
lane :testflight_prune_dry do
|
||||||
clean_testflight_testers(days_of_inactivity:45, dry_run: true)
|
clean_testflight_testers(days_of_inactivity:30, dry_run: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
lane :testflight_prune do
|
lane :testflight_prune do
|
||||||
clean_testflight_testers(days_of_inactivity: 45)
|
clean_testflight_testers(days_of_inactivity: 30)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1022.1" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1101.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -18,17 +18,9 @@ using osu.Framework;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
using osu.Desktop.Windows;
|
using osu.Desktop.Windows;
|
||||||
using osu.Framework.Input.Handlers;
|
|
||||||
using osu.Framework.Input.Handlers.Joystick;
|
|
||||||
using osu.Framework.Input.Handlers.Mouse;
|
|
||||||
using osu.Framework.Input.Handlers.Tablet;
|
|
||||||
using osu.Framework.Input.Handlers.Touch;
|
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.IPC;
|
using osu.Game.IPC;
|
||||||
using osu.Game.Overlays.Settings;
|
|
||||||
using osu.Game.Overlays.Settings.Sections;
|
|
||||||
using osu.Game.Overlays.Settings.Sections.Input;
|
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using SDL2;
|
using SDL2;
|
||||||
|
|
||||||
@ -148,27 +140,6 @@ namespace osu.Desktop
|
|||||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
|
||||||
{
|
|
||||||
switch (handler)
|
|
||||||
{
|
|
||||||
case ITabletHandler th:
|
|
||||||
return new TabletSettings(th);
|
|
||||||
|
|
||||||
case MouseHandler mh:
|
|
||||||
return new MouseSettings(mh);
|
|
||||||
|
|
||||||
case JoystickHandler jh:
|
|
||||||
return new JoystickSettings(jh);
|
|
||||||
|
|
||||||
case TouchHandler th:
|
|
||||||
return new InputSection.HandlerSection(th);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return base.CreateSettingsSubsectionFor(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
||||||
|
|
||||||
private readonly List<string> importableFiles = new List<string>();
|
private readonly List<string> importableFiles = new List<string>();
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Catch.Edit;
|
|||||||
using osu.Game.Rulesets.Catch.Mods;
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
using osu.Game.Rulesets.Catch.Replays;
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
using osu.Game.Rulesets.Catch.Scoring;
|
using osu.Game.Rulesets.Catch.Scoring;
|
||||||
|
using osu.Game.Rulesets.Catch.Skinning.Argon;
|
||||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
@ -188,6 +189,9 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
{
|
{
|
||||||
case LegacySkin:
|
case LegacySkin:
|
||||||
return new CatchLegacySkinTransformer(skin);
|
return new CatchLegacySkinTransformer(skin);
|
||||||
|
|
||||||
|
case ArgonSkin:
|
||||||
|
return new CatchArgonSkinTransformer(skin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@ -10,7 +11,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.EnumExtensions;
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -23,7 +23,6 @@ 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;
|
||||||
|
|
||||||
@ -35,8 +34,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
private CatchDistanceSnapGrid distanceSnapGrid;
|
private CatchDistanceSnapGrid distanceSnapGrid;
|
||||||
|
|
||||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
|
||||||
|
|
||||||
private InputManager inputManager;
|
private InputManager inputManager;
|
||||||
|
|
||||||
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
|
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
|
||||||
@ -81,6 +78,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
inputManager = GetContainingInputManager();
|
inputManager = GetContainingInputManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||||
|
{
|
||||||
|
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
|
||||||
|
// Therefore this functionality is not currently used.
|
||||||
|
//
|
||||||
|
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||||
|
|
||||||
|
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||||
|
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
|
||||||
|
|
||||||
|
return actualDistance / expectedDistance;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
@ -120,11 +130,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
new BananaShowerCompositionTool()
|
new BananaShowerCompositionTool()
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
|
||||||
{
|
|
||||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
|
||||||
});
|
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||||
@ -196,7 +201,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
private void updateDistanceSnapGrid()
|
private void updateDistanceSnapGrid()
|
||||||
{
|
{
|
||||||
if (distanceSnapToggle.Value != TernaryState.True)
|
if (DistanceSnapToggle.Value != TernaryState.True)
|
||||||
{
|
{
|
||||||
distanceSnapGrid.Hide();
|
distanceSnapGrid.Hide();
|
||||||
return;
|
return;
|
||||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
|
|
||||||
public float DisplayRotation => Rotation;
|
public float DisplayRotation => Rotation;
|
||||||
|
|
||||||
|
public double DisplayStartTime => HitObject.StartTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
|
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
{
|
{
|
||||||
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
|
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
|
||||||
|
|
||||||
|
public double DisplayStartTime => LifetimeStart;
|
||||||
|
|
||||||
Bindable<Color4> IHasCatchObjectState.AccentColour => AccentColour;
|
Bindable<Color4> IHasCatchObjectState.AccentColour => AccentColour;
|
||||||
|
|
||||||
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
|
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
|
||||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
{
|
{
|
||||||
PalpableCatchHitObject HitObject { get; }
|
PalpableCatchHitObject HitObject { get; }
|
||||||
|
|
||||||
|
double DisplayStartTime { get; }
|
||||||
|
|
||||||
Bindable<Color4> AccentColour { get; }
|
Bindable<Color4> AccentColour { get; }
|
||||||
|
|
||||||
Bindable<bool> HyperDash { get; }
|
Bindable<bool> HyperDash { get; }
|
||||||
|
122
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs
Normal file
122
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// 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.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Colour;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Effects;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
internal class ArgonBananaPiece : ArgonFruitPiece
|
||||||
|
{
|
||||||
|
private Container stabilisedPieceContainer = null!;
|
||||||
|
|
||||||
|
private Drawable fadeContent = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AddInternal(fadeContent = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
stabilisedPieceContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Colour = Color4.White.Opacity(0.4f),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Size = new Vector2(8),
|
||||||
|
Scale = new Vector2(25, 1),
|
||||||
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.8f)),
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Width = 1.6f,
|
||||||
|
Height = 2,
|
||||||
|
},
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White.Opacity(0)),
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Width = 1.6f,
|
||||||
|
Height = 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(1.2f),
|
||||||
|
EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Type = EdgeEffectType.Glow,
|
||||||
|
Hollow = false,
|
||||||
|
Colour = Color4.White.Opacity(0.1f),
|
||||||
|
Radius = 50,
|
||||||
|
},
|
||||||
|
Child =
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
},
|
||||||
|
BorderColour = Color4.White.Opacity(0.1f),
|
||||||
|
BorderThickness = 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
const float parent_scale_application = 0.4f;
|
||||||
|
|
||||||
|
// relative to time on screen
|
||||||
|
const float lens_flare_start = 0.3f;
|
||||||
|
const float lens_flare_end = 0.8f;
|
||||||
|
|
||||||
|
// Undo some of the parent scale being applied to make the lens flare feel a bit better..
|
||||||
|
float scale = parent_scale_application + (1 - parent_scale_application) * (1 / (ObjectState.DisplaySize.X / (CatchHitObject.OBJECT_RADIUS * 2)));
|
||||||
|
|
||||||
|
stabilisedPieceContainer.Rotation = -ObjectState.DisplayRotation;
|
||||||
|
stabilisedPieceContainer.Scale = new Vector2(scale, 1);
|
||||||
|
|
||||||
|
double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime;
|
||||||
|
|
||||||
|
fadeContent.Alpha = MathHelper.Clamp(
|
||||||
|
Interpolation.ValueAt(
|
||||||
|
Time.Current, 1f, 0f,
|
||||||
|
ObjectState.DisplayStartTime + duration * lens_flare_start,
|
||||||
|
ObjectState.DisplayStartTime + duration * lens_flare_end,
|
||||||
|
Easing.OutQuint
|
||||||
|
), 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs
Normal file
85
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// 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.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
public class ArgonCatcher : CompositeDrawable
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = 10,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = Color4.White,
|
||||||
|
Width = Catcher.ALLOWED_CATCH_RANGE,
|
||||||
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Name = "long line left",
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Colour = Color4.White,
|
||||||
|
Alpha = 0.25f,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 20,
|
||||||
|
Height = 1.8f,
|
||||||
|
},
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Name = "bumper left",
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Colour = Color4.White,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2,
|
||||||
|
Height = 4,
|
||||||
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Name = "long line right",
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Colour = Color4.White,
|
||||||
|
Alpha = 0.25f,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 20,
|
||||||
|
Height = 1.8f,
|
||||||
|
},
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Name = "bumper right",
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Colour = Color4.White,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2,
|
||||||
|
Height = 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonDropletPiece.cs
Normal file
121
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonDropletPiece.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
internal class ArgonDropletPiece : CatchHitObjectPiece
|
||||||
|
{
|
||||||
|
protected override Drawable HyperBorderPiece => hyperBorderPiece;
|
||||||
|
|
||||||
|
private Drawable hyperBorderPiece = null!;
|
||||||
|
|
||||||
|
private Container layers = null!;
|
||||||
|
|
||||||
|
private float rotationRandomness;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
const float droplet_scale_down = 0.7f;
|
||||||
|
|
||||||
|
int largeBlobSeed = RNG.Next();
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(20),
|
||||||
|
},
|
||||||
|
layers = new Container
|
||||||
|
{
|
||||||
|
Scale = new Vector2(droplet_scale_down),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new CircularBlob
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
InnerRadius = 0.5f,
|
||||||
|
Alpha = 0.15f,
|
||||||
|
Seed = largeBlobSeed
|
||||||
|
},
|
||||||
|
new CircularBlob
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
InnerRadius = 0.4f,
|
||||||
|
Alpha = 0.5f,
|
||||||
|
Scale = new Vector2(0.7f),
|
||||||
|
Seed = RNG.Next()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hyperBorderPiece = new CircularBlob
|
||||||
|
{
|
||||||
|
Scale = new Vector2(droplet_scale_down),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
InnerRadius = 0.5f,
|
||||||
|
Alpha = 0.15f,
|
||||||
|
Seed = largeBlobSeed
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AccentColour.BindValueChanged(colour =>
|
||||||
|
{
|
||||||
|
foreach (var sprite in layers)
|
||||||
|
sprite.Colour = colour.NewValue;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
rotationRandomness = RNG.NextSingle(0.2f, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
// Note that droplets are rotated at a higher level, so this is mostly just to create more
|
||||||
|
// random arrangements of the multiple layers than actually rotate.
|
||||||
|
//
|
||||||
|
// Because underlying rotation is always clockwise, we apply anti-clockwise resistance to avoid
|
||||||
|
// making things spin too fast.
|
||||||
|
for (int i = 0; i < layers.Count; i++)
|
||||||
|
{
|
||||||
|
layers[i].Rotation -=
|
||||||
|
(float)Clock.ElapsedFrameTime
|
||||||
|
* 0.4f * rotationRandomness
|
||||||
|
// Each layer should alternate rotation speed.
|
||||||
|
* (i % 2 == 1 ? 0.5f : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonFruitPiece.cs
Normal file
121
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonFruitPiece.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
internal class ArgonFruitPiece : CatchHitObjectPiece
|
||||||
|
{
|
||||||
|
protected override Drawable HyperBorderPiece => hyperBorderPiece;
|
||||||
|
|
||||||
|
private Drawable hyperBorderPiece = null!;
|
||||||
|
|
||||||
|
private Container layers = null!;
|
||||||
|
|
||||||
|
private float rotationRandomness;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
int largeBlobSeed = RNG.Next();
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(20),
|
||||||
|
},
|
||||||
|
layers = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new CircularBlob
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Alpha = 0.15f,
|
||||||
|
InnerRadius = 0.5f,
|
||||||
|
Size = new Vector2(1.1f),
|
||||||
|
Seed = largeBlobSeed,
|
||||||
|
},
|
||||||
|
new CircularBlob
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
InnerRadius = 0.2f,
|
||||||
|
Alpha = 0.5f,
|
||||||
|
Seed = RNG.Next(),
|
||||||
|
},
|
||||||
|
new CircularBlob
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
InnerRadius = 0.05f,
|
||||||
|
Seed = RNG.Next(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hyperBorderPiece = new CircularBlob
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
InnerRadius = 0.08f,
|
||||||
|
Size = new Vector2(1.15f),
|
||||||
|
Seed = largeBlobSeed
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AccentColour.BindValueChanged(colour =>
|
||||||
|
{
|
||||||
|
foreach (var sprite in layers)
|
||||||
|
sprite.Colour = colour.NewValue;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
rotationRandomness = RNG.NextSingle(0.2f, 1) * (RNG.NextBool() ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
for (int i = 0; i < layers.Count; i++)
|
||||||
|
{
|
||||||
|
layers[i].Rotation +=
|
||||||
|
// Layers are ordered from largest to smallest. Smaller layers should rotate more.
|
||||||
|
(i * 2)
|
||||||
|
* (float)Clock.ElapsedFrameTime
|
||||||
|
* 0.02f * rotationRandomness
|
||||||
|
// Each layer should alternate rotation direction.
|
||||||
|
* (i % 2 == 1 ? 1 : -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonHitExplosion.cs
Normal file
112
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonHitExplosion.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Effects;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
public class ArgonHitExplosion : CompositeDrawable, IHitExplosion
|
||||||
|
{
|
||||||
|
public override bool RemoveWhenNotAlive => true;
|
||||||
|
|
||||||
|
private Container tallExplosion = null!;
|
||||||
|
private Container largeFaint = null!;
|
||||||
|
|
||||||
|
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
|
|
||||||
|
public ArgonHitExplosion()
|
||||||
|
{
|
||||||
|
Size = new Vector2(20);
|
||||||
|
Anchor = Anchor.BottomCentre;
|
||||||
|
Origin = Anchor.BottomCentre;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
tallExplosion = new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
Width = 0.1f,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Alpha = 0,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
largeFaint = new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Alpha = 0,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
accentColour.BindValueChanged(colour =>
|
||||||
|
{
|
||||||
|
tallExplosion.EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Type = EdgeEffectType.Glow,
|
||||||
|
Colour = colour.NewValue,
|
||||||
|
Hollow = false,
|
||||||
|
Roundness = 15,
|
||||||
|
Radius = 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
largeFaint.EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Type = EdgeEffectType.Glow,
|
||||||
|
Colour = Interpolation.ValueAt(0.2f, colour.NewValue, Color4.White, 0, 1),
|
||||||
|
Hollow = false,
|
||||||
|
Radius = 50,
|
||||||
|
};
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Animate(HitExplosionEntry entry)
|
||||||
|
{
|
||||||
|
X = entry.Position;
|
||||||
|
Scale = new Vector2(entry.HitObject.Scale);
|
||||||
|
accentColour.Value = entry.ObjectColour;
|
||||||
|
|
||||||
|
using (BeginAbsoluteSequence(entry.LifetimeStart))
|
||||||
|
{
|
||||||
|
this.FadeOutFromOne(400);
|
||||||
|
|
||||||
|
if (!(entry.HitObject is Droplet))
|
||||||
|
{
|
||||||
|
float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
|
||||||
|
|
||||||
|
tallExplosion
|
||||||
|
.ScaleTo(new Vector2(1.1f, 20 * scale), 200, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.ScaleTo(new Vector2(1.1f, 1), 600, Easing.In);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
193
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
193
osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
|
||||||
|
{
|
||||||
|
protected readonly HitResult Result;
|
||||||
|
|
||||||
|
protected SpriteText JudgementText { get; private set; } = null!;
|
||||||
|
|
||||||
|
private RingExplosion? ringExplosion;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
public ArgonJudgementPiece(HitResult result)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
Y = 160;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
JudgementText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Text = Result.GetDescription().ToUpperInvariant(),
|
||||||
|
Colour = colours.ForHitResult(Result),
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Spacing = new Vector2(10, 0),
|
||||||
|
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Result.IsHit())
|
||||||
|
{
|
||||||
|
AddInternal(ringExplosion = new RingExplosion(Result)
|
||||||
|
{
|
||||||
|
Colour = colours.ForHitResult(Result),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plays the default animation for this judgement piece.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The base implementation only handles fade (for all result types) and misses.
|
||||||
|
/// Individual rulesets are recommended to implement their appropriate hit animations.
|
||||||
|
/// </remarks>
|
||||||
|
public virtual void PlayAnimation()
|
||||||
|
{
|
||||||
|
switch (Result)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
JudgementText
|
||||||
|
.ScaleTo(Vector2.One)
|
||||||
|
.ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Miss:
|
||||||
|
this.ScaleTo(1.6f);
|
||||||
|
this.ScaleTo(1, 100, Easing.In);
|
||||||
|
|
||||||
|
this.MoveTo(Vector2.Zero);
|
||||||
|
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||||
|
|
||||||
|
this.RotateTo(0);
|
||||||
|
this.RotateTo(40, 800, Easing.InQuint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.FadeOutFromOne(800);
|
||||||
|
|
||||||
|
ringExplosion?.PlayAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Drawable? GetAboveHitObjectsProxiedContent() => null;
|
||||||
|
|
||||||
|
private class RingExplosion : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly float travel = 52;
|
||||||
|
|
||||||
|
public RingExplosion(HitResult result)
|
||||||
|
{
|
||||||
|
const float thickness = 4;
|
||||||
|
|
||||||
|
const float small_size = 9;
|
||||||
|
const float large_size = 14;
|
||||||
|
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
Blending = BlendingParameters.Additive;
|
||||||
|
|
||||||
|
int countSmall = 0;
|
||||||
|
int countLarge = 0;
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.Meh:
|
||||||
|
countSmall = 3;
|
||||||
|
travel *= 0.3f;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Ok:
|
||||||
|
case HitResult.Good:
|
||||||
|
countSmall = 4;
|
||||||
|
travel *= 0.6f;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Great:
|
||||||
|
case HitResult.Perfect:
|
||||||
|
countSmall = 4;
|
||||||
|
countLarge = 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < countSmall; i++)
|
||||||
|
AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
|
||||||
|
|
||||||
|
for (int i = 0; i < countLarge; i++)
|
||||||
|
AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlayAnimation()
|
||||||
|
{
|
||||||
|
foreach (var c in InternalChildren)
|
||||||
|
{
|
||||||
|
const float start_position_ratio = 0.3f;
|
||||||
|
|
||||||
|
float direction = RNG.NextSingle(0, 360);
|
||||||
|
float distance = RNG.NextSingle(travel / 2, travel);
|
||||||
|
|
||||||
|
c.MoveTo(new Vector2(
|
||||||
|
MathF.Cos(direction) * distance * start_position_ratio,
|
||||||
|
MathF.Sin(direction) * distance * start_position_ratio
|
||||||
|
));
|
||||||
|
|
||||||
|
c.MoveTo(new Vector2(
|
||||||
|
MathF.Cos(direction) * distance,
|
||||||
|
MathF.Sin(direction) * distance
|
||||||
|
), 600, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.FadeOutFromOne(1000, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RingPiece : CircularContainer
|
||||||
|
{
|
||||||
|
public RingPiece(float thickness = 9)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
BorderThickness = thickness;
|
||||||
|
BorderColour = Color4.White;
|
||||||
|
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Alpha = 0,
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||||
|
{
|
||||||
|
public class CatchArgonSkinTransformer : SkinTransformer
|
||||||
|
{
|
||||||
|
public CatchArgonSkinTransformer(ISkin skin)
|
||||||
|
: base(skin)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Drawable? GetDrawableComponent(ISkinComponent component)
|
||||||
|
{
|
||||||
|
switch (component)
|
||||||
|
{
|
||||||
|
case CatchSkinComponent catchComponent:
|
||||||
|
// TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
|
||||||
|
switch (catchComponent.Component)
|
||||||
|
{
|
||||||
|
case CatchSkinComponents.HitExplosion:
|
||||||
|
return new ArgonHitExplosion();
|
||||||
|
|
||||||
|
case CatchSkinComponents.Catcher:
|
||||||
|
return new ArgonCatcher();
|
||||||
|
|
||||||
|
case CatchSkinComponents.Fruit:
|
||||||
|
return new ArgonFruitPiece();
|
||||||
|
|
||||||
|
case CatchSkinComponents.Banana:
|
||||||
|
return new ArgonBananaPiece();
|
||||||
|
|
||||||
|
case CatchSkinComponents.Droplet:
|
||||||
|
return new ArgonDropletPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.GetDrawableComponent(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,19 @@
|
|||||||
// 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.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Skinning.Default
|
namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||||
{
|
{
|
||||||
public class BananaPiece : CatchHitObjectPiece
|
public class BananaPiece : CatchHitObjectPiece
|
||||||
{
|
{
|
||||||
protected override BorderPiece BorderPiece { get; }
|
protected override Drawable BorderPiece { get; }
|
||||||
|
|
||||||
public BananaPiece()
|
public BananaPiece()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
new BananaPulpFormation
|
new BananaPulpFormation
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ using System;
|
|||||||
using JetBrains.Annotations;
|
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.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -26,13 +27,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
|||||||
/// A part of this piece that will be faded out while falling in the playfield.
|
/// A part of this piece that will be faded out while falling in the playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
protected virtual BorderPiece BorderPiece => null;
|
protected virtual Drawable BorderPiece => null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A part of this piece that will be only visible when <see cref="HyperDash"/> is true.
|
/// A part of this piece that will be only visible when <see cref="HyperDash"/> is true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
protected virtual HyperBorderPiece HyperBorderPiece => null;
|
protected virtual Drawable HyperBorderPiece => null;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
|
@ -11,13 +11,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
|||||||
{
|
{
|
||||||
public class DropletPiece : CatchHitObjectPiece
|
public class DropletPiece : CatchHitObjectPiece
|
||||||
{
|
{
|
||||||
protected override HyperBorderPiece HyperBorderPiece { get; }
|
protected override Drawable HyperBorderPiece { get; }
|
||||||
|
|
||||||
public DropletPiece()
|
public DropletPiece()
|
||||||
{
|
{
|
||||||
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
|
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
new Pulp
|
new Pulp
|
||||||
{
|
{
|
||||||
|
@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
|||||||
|
|
||||||
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
|
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
|
||||||
|
|
||||||
protected override BorderPiece BorderPiece { get; }
|
protected override Drawable BorderPiece { get; }
|
||||||
protected override HyperBorderPiece HyperBorderPiece { get; }
|
protected override Drawable HyperBorderPiece { get; }
|
||||||
|
|
||||||
public FruitPiece()
|
public FruitPiece()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
new FruitPulpFormation
|
new FruitPulpFormation
|
||||||
{
|
{
|
||||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
assertHeadJudgement(HitResult.Miss);
|
assertHeadJudgement(HitResult.Miss);
|
||||||
assertTickJudgement(HitResult.LargeTickMiss);
|
assertTickJudgement(HitResult.LargeTickMiss);
|
||||||
assertTailJudgement(HitResult.Miss);
|
assertTailJudgement(HitResult.Miss);
|
||||||
assertNoteJudgement(HitResult.IgnoreHit);
|
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
|
|
||||||
performTest(objects, new List<ReplayFrame>());
|
performTest(objects, new List<ReplayFrame>());
|
||||||
|
|
||||||
addJudgementAssert(objects[0], HitResult.IgnoreHit);
|
addJudgementAssert(objects[0], HitResult.IgnoreMiss);
|
||||||
addJudgementAssert(objects[1], HitResult.IgnoreHit);
|
addJudgementAssert(objects[1], HitResult.IgnoreMiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
public override string Acronym => "HO";
|
public override string Acronym => "HO";
|
||||||
|
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 0.9;
|
||||||
|
|
||||||
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
||||||
|
|
||||||
|
@ -69,6 +69,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private double? releaseTime;
|
private double? releaseTime;
|
||||||
|
|
||||||
|
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||||
|
|
||||||
public DrawableHoldNote()
|
public DrawableHoldNote()
|
||||||
: this(null)
|
: this(null)
|
||||||
{
|
{
|
||||||
@ -260,7 +262,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
tick.MissForcefully();
|
tick.MissForcefully();
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
endHold();
|
endHold();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,12 +4,15 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -52,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
};
|
};
|
||||||
|
|
||||||
private OsuDistanceSnapGrid grid;
|
private OsuDistanceSnapGrid grid;
|
||||||
|
private SnappingCursorContainer cursor;
|
||||||
|
|
||||||
public TestSceneOsuDistanceSnapGrid()
|
public TestSceneOsuDistanceSnapGrid()
|
||||||
{
|
{
|
||||||
@ -88,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.SlateGray
|
Colour = Color4.SlateGray
|
||||||
},
|
},
|
||||||
|
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
|
||||||
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
|
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
|
||||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,6 +158,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
assertSnappedDistance(expectedDistance);
|
assertSnappedDistance(expectedDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReferenceObjectNotOnSnapGrid()
|
||||||
|
{
|
||||||
|
AddStep("create grid", () =>
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Color4.SlateGray
|
||||||
|
},
|
||||||
|
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
|
||||||
|
grid = new OsuDistanceSnapGrid(new HitCircle
|
||||||
|
{
|
||||||
|
Position = grid_position,
|
||||||
|
// This is important. It sets the reference object to a point in time that isn't on the current snap divisor's grid.
|
||||||
|
// We are testing that the grid's display is offset correctly.
|
||||||
|
StartTime = 40,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2)));
|
||||||
|
|
||||||
|
AddAssert("Ensure cursor is on a grid line", () =>
|
||||||
|
{
|
||||||
|
return grid.ChildrenOfType<CircularProgress>().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLimitedDistance()
|
public void TestLimitedDistance()
|
||||||
{
|
{
|
||||||
@ -166,8 +201,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.SlateGray
|
Colour = Color4.SlateGray
|
||||||
},
|
},
|
||||||
|
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
|
||||||
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
|
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
|
||||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -186,6 +221,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
public Func<Vector2, Vector2> GetSnapPosition;
|
public Func<Vector2, Vector2> GetSnapPosition;
|
||||||
|
|
||||||
|
public Vector2 LastSnappedPosition { get; private set; }
|
||||||
|
|
||||||
private readonly Drawable cursor;
|
private readonly Drawable cursor;
|
||||||
|
|
||||||
private InputManager inputManager;
|
private InputManager inputManager;
|
||||||
@ -214,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
cursor.Position = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
|
cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,20 +20,49 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestGridExclusivity()
|
public void TestGridToggles()
|
||||||
{
|
{
|
||||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
|
||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
rectangularGridActive(false);
|
rectangularGridActive(false);
|
||||||
|
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
|
||||||
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
rectangularGridActive(true);
|
rectangularGridActive(true);
|
||||||
|
|
||||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
rectangularGridActive(true);
|
||||||
|
|
||||||
|
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
|
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
rectangularGridActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDistanceSnapMomentaryToggle()
|
||||||
|
{
|
||||||
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
|
||||||
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||||
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridSnapMomentaryToggle()
|
||||||
|
{
|
||||||
|
rectangularGridActive(false);
|
||||||
|
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
rectangularGridActive(true);
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
rectangularGridActive(false);
|
rectangularGridActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +79,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
|
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
|
||||||
else
|
else
|
||||||
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
|
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
|
||||||
|
|
||||||
AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||||
|
{
|
||||||
|
public class TestSceneOsuModFreezeFrame : OsuModTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestFreezeFrame()
|
||||||
|
{
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModFreezeFrame(),
|
||||||
|
PassCondition = () => true,
|
||||||
|
Autoplay = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
|
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
|
||||||
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
||||||
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
||||||
new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
|
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
|
||||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
|
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -377,7 +377,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||||
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
|
() => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
|
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
|
||||||
|
@ -40,16 +40,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
body.BorderColour = colours.Yellow;
|
body.BorderColour = colours.Yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int? lastVersion;
|
||||||
|
|
||||||
public override void UpdateFrom(Slider hitObject)
|
public override void UpdateFrom(Slider hitObject)
|
||||||
{
|
{
|
||||||
base.UpdateFrom(hitObject);
|
base.UpdateFrom(hitObject);
|
||||||
|
|
||||||
body.PathRadius = hitObject.Scale * OsuHitObject.OBJECT_RADIUS;
|
body.PathRadius = hitObject.Scale * OsuHitObject.OBJECT_RADIUS;
|
||||||
|
|
||||||
|
if (lastVersion != hitObject.Path.Version.Value)
|
||||||
|
{
|
||||||
|
lastVersion = hitObject.Path.Version.Value;
|
||||||
|
|
||||||
var vertices = new List<Vector2>();
|
var vertices = new List<Vector2>();
|
||||||
hitObject.Path.GetPathToProgress(vertices, 0, 1);
|
hitObject.Path.GetPathToProgress(vertices, 0, 1);
|
||||||
|
|
||||||
body.SetVertices(vertices);
|
body.SetVertices(vertices);
|
||||||
|
}
|
||||||
|
|
||||||
Size = body.Size;
|
Size = body.Size;
|
||||||
OriginPosition = body.PathOffset;
|
OriginPosition = body.PathOffset;
|
||||||
|
@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
||||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||||
|
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
||||||
|
|
||||||
public SliderSelectionBlueprint(Slider slider)
|
public SliderSelectionBlueprint(Slider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
@ -86,6 +87,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
||||||
|
|
||||||
BodyPiece.UpdateFrom(HitObject);
|
BodyPiece.UpdateFrom(HitObject);
|
||||||
|
|
||||||
|
if (editorBeatmap != null)
|
||||||
|
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
|
selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleQuickDeletion()
|
public override bool HandleQuickDeletion()
|
||||||
@ -100,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool hasSingleObjectSelected => selectedObjects.Count == 1;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
@ -108,14 +115,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
BodyPiece.UpdateFrom(HitObject);
|
BodyPiece.UpdateFrom(HitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
updateVisualDefinition();
|
||||||
|
|
||||||
|
// In the case more than a single object is selected, block hover from arriving at sliders behind this one.
|
||||||
|
// Without doing this, the path visualisers of potentially hundreds of sliders will render, which is not only
|
||||||
|
// visually noisy but also functionally useless.
|
||||||
|
return !hasSingleObjectSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
updateVisualDefinition();
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnSelected()
|
protected override void OnSelected()
|
||||||
{
|
{
|
||||||
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
updateVisualDefinition();
|
||||||
{
|
|
||||||
RemoveControlPointsRequested = removeControlPoints,
|
|
||||||
SplitControlPointsRequested = splitControlPoints
|
|
||||||
});
|
|
||||||
|
|
||||||
base.OnSelected();
|
base.OnSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,11 +141,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
base.OnDeselected();
|
base.OnDeselected();
|
||||||
|
|
||||||
// throw away frame buffers on deselection.
|
updateVisualDefinition();
|
||||||
|
BodyPiece.RecyclePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVisualDefinition()
|
||||||
|
{
|
||||||
|
// To reduce overhead of drawing these blueprints, only add extra detail when hovered or when only this slider is selected.
|
||||||
|
if (IsSelected && (hasSingleObjectSelected || IsHovered))
|
||||||
|
{
|
||||||
|
if (ControlPointVisualiser == null)
|
||||||
|
{
|
||||||
|
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
||||||
|
{
|
||||||
|
RemoveControlPointsRequested = removeControlPoints,
|
||||||
|
SplitControlPointsRequested = splitControlPoints
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
ControlPointVisualiser?.Expire();
|
ControlPointVisualiser?.Expire();
|
||||||
ControlPointVisualiser = null;
|
ControlPointVisualiser = null;
|
||||||
|
}
|
||||||
BodyPiece.RecyclePath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 rightClickPosition;
|
private Vector2 rightClickPosition;
|
||||||
|
@ -5,19 +5,17 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
|
||||||
{
|
{
|
||||||
public class SpinnerPiece : BlueprintPiece<Spinner>
|
public class SpinnerPiece : BlueprintPiece<Spinner>
|
||||||
{
|
{
|
||||||
private readonly CircularContainer circle;
|
private readonly Circle circle;
|
||||||
private readonly RingPiece ring;
|
private readonly Circle ring;
|
||||||
|
|
||||||
public SpinnerPiece()
|
public SpinnerPiece()
|
||||||
{
|
{
|
||||||
@ -25,18 +23,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
|
|||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
FillMode = FillMode.Fit;
|
FillMode = FillMode.Fit;
|
||||||
Size = new Vector2(1.3f);
|
Size = new Vector2(1);
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
circle = new CircularContainer
|
circle = new Circle
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
|
||||||
Alpha = 0.5f,
|
Alpha = 0.5f,
|
||||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
|
||||||
},
|
},
|
||||||
ring = new RingPiece()
|
ring = new Circle
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(OsuHitObject.OBJECT_RADIUS),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +13,11 @@ using osu.Framework.Extensions.EnumExtensions;
|
|||||||
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.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Edit;
|
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;
|
||||||
@ -44,12 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
new SpinnerCompositionTool()
|
new SpinnerCompositionTool()
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
|
||||||
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
|
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||||
{
|
{
|
||||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
|
|
||||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
|
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -80,19 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
||||||
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||||
distanceSnapToggle.ValueChanged += _ =>
|
DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
|
||||||
{
|
|
||||||
updateDistanceSnapGrid();
|
|
||||||
|
|
||||||
if (distanceSnapToggle.Value == TernaryState.True)
|
|
||||||
rectangularGridSnapToggle.Value = TernaryState.False;
|
|
||||||
};
|
|
||||||
|
|
||||||
rectangularGridSnapToggle.ValueChanged += _ =>
|
|
||||||
{
|
|
||||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
|
||||||
distanceSnapToggle.Value = TernaryState.False;
|
|
||||||
};
|
|
||||||
|
|
||||||
// we may be entering the screen with a selection already active
|
// we may be entering the screen with a selection already active
|
||||||
updateDistanceSnapGrid();
|
updateDistanceSnapGrid();
|
||||||
@ -112,6 +101,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
|
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
|
||||||
|
|
||||||
|
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||||
|
{
|
||||||
|
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||||
|
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||||
|
|
||||||
|
return actualDistance / expectedDistance;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
@ -132,24 +129,46 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||||
|
{
|
||||||
|
// 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.HasFlagFast(SnapType.Grids) && 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;
|
||||||
|
}
|
||||||
|
|
||||||
return snapResult;
|
return snapResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||||
|
|
||||||
if (snapType.HasFlagFast(SnapType.Grids))
|
if (snapType.HasFlagFast(SnapType.Grids))
|
||||||
{
|
{
|
||||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||||
{
|
{
|
||||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
|
||||||
|
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
|
||||||
|
result.Time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||||
{
|
{
|
||||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
||||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
|
||||||
|
result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||||
@ -202,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
distanceSnapGridCache.Invalidate();
|
distanceSnapGridCache.Invalidate();
|
||||||
distanceSnapGrid = null;
|
distanceSnapGrid = null;
|
||||||
|
|
||||||
if (distanceSnapToggle.Value != TernaryState.True)
|
if (DistanceSnapToggle.Value != TernaryState.True)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (BlueprintContainer.CurrentTool)
|
switch (BlueprintContainer.CurrentTool)
|
||||||
@ -229,6 +248,42 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
if (e.Repeat)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
handleToggleViaKey(e);
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyUp(KeyUpEvent e)
|
||||||
|
{
|
||||||
|
handleToggleViaKey(e);
|
||||||
|
base.OnKeyUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool AdjustDistanceSpacing(GlobalAction action, float amount)
|
||||||
|
{
|
||||||
|
// To allow better visualisation, ensure that the spacing grid is visible before adjusting.
|
||||||
|
DistanceSnapToggle.Value = TernaryState.True;
|
||||||
|
|
||||||
|
return base.AdjustDistanceSpacing(action, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool gridSnapMomentary;
|
||||||
|
|
||||||
|
private void handleToggleViaKey(KeyboardEvent key)
|
||||||
|
{
|
||||||
|
bool shiftPressed = key.ShiftPressed;
|
||||||
|
|
||||||
|
if (shiftPressed != gridSnapMomentary)
|
||||||
|
{
|
||||||
|
gridSnapMomentary = shiftPressed;
|
||||||
|
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
||||||
{
|
{
|
||||||
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
|
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
|
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
|
||||||
|
|
||||||
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
|
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
|
||||||
public BindableFloat Scale { get; } = new BindableFloat(4)
|
public BindableFloat Scale { get; } = new BindableFloat(4)
|
||||||
|
89
osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
Normal file
89
osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
|
{
|
||||||
|
public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicableToBeatmap
|
||||||
|
{
|
||||||
|
public override string Name => "Freeze Frame";
|
||||||
|
|
||||||
|
public override string Acronym => "FR";
|
||||||
|
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
|
||||||
|
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||||
|
|
||||||
|
//Alters the transforms of the approach circles, breaking the effects of these mods.
|
||||||
|
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
|
||||||
|
|
||||||
|
public override ModType Type => ModType.Fun;
|
||||||
|
|
||||||
|
//mod breaks normal approach circle preempt
|
||||||
|
private double originalPreempt;
|
||||||
|
|
||||||
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var firstHitObject = beatmap.HitObjects.OfType<OsuHitObject>().FirstOrDefault();
|
||||||
|
if (firstHitObject == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double lastNewComboTime = 0;
|
||||||
|
|
||||||
|
originalPreempt = firstHitObject.TimePreempt;
|
||||||
|
|
||||||
|
foreach (var obj in beatmap.HitObjects.OfType<OsuHitObject>())
|
||||||
|
{
|
||||||
|
if (obj.NewCombo) { lastNewComboTime = obj.StartTime; }
|
||||||
|
|
||||||
|
applyFadeInAdjustment(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyFadeInAdjustment(OsuHitObject osuObject)
|
||||||
|
{
|
||||||
|
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
|
||||||
|
|
||||||
|
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
|
||||||
|
{
|
||||||
|
switch (nested)
|
||||||
|
{
|
||||||
|
//SliderRepeat wont layer correctly if preempt is changed.
|
||||||
|
case SliderRepeat:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
applyFadeInAdjustment(nested);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
|
||||||
|
{
|
||||||
|
drawableObject.ApplyCustomUpdateState += (drawableHitObject, _) =>
|
||||||
|
{
|
||||||
|
if (drawableHitObject is not DrawableHitCircle drawableHitCircle) return;
|
||||||
|
|
||||||
|
var hitCircle = drawableHitCircle.HitObject;
|
||||||
|
var approachCircle = drawableHitCircle.ApproachCircle;
|
||||||
|
|
||||||
|
// Reapply scale, ensuring the AR isn't changed due to the new preempt.
|
||||||
|
approachCircle.ClearTransforms(targetMember: nameof(approachCircle.Scale));
|
||||||
|
approachCircle.ScaleTo(4 * (float)(hitCircle.TimePreempt / originalPreempt));
|
||||||
|
|
||||||
|
using (drawableHitCircle.ApproachCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
|
||||||
|
approachCircle.ScaleTo(1, hitCircle.TimePreempt).Then().Expire();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public override LocalisableString Description => "It never gets boring!";
|
public override LocalisableString Description => "It never gets boring!";
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTargetPractice)).ToArray();
|
||||||
|
|
||||||
[SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider<float>))]
|
[SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider<float>))]
|
||||||
public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
|
public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Automation;
|
public override ModType Type => ModType.Automation;
|
||||||
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
||||||
public override double ScoreMultiplier => 0.9;
|
public override double ScoreMultiplier => 0.9;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
|
||||||
|
|
||||||
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
|
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.DifficultyIncrease;
|
public override ModType Type => ModType.DifficultyIncrease;
|
||||||
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
|
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
|
||||||
public override double ScoreMultiplier => 1.0;
|
public override double ScoreMultiplier => 1.0;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTargetPractice) };
|
||||||
|
|
||||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||||
{
|
{
|
||||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||||
{
|
{
|
||||||
typeof(OsuModAutopilot),
|
typeof(OsuModAutopilot),
|
||||||
typeof(OsuModTarget),
|
typeof(OsuModTargetPractice),
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,16 +32,15 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>,
|
public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>,
|
||||||
IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride,
|
IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, IHasSeed, IHidesApproachCircles
|
||||||
IHasSeed, IHidesApproachCircles
|
|
||||||
{
|
{
|
||||||
public override string Name => "Target";
|
public override string Name => "Target Practice";
|
||||||
public override string Acronym => "TP";
|
public override string Acronym => "TP";
|
||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
public override IconUsage? Icon => OsuIcon.ModTarget;
|
public override IconUsage? Icon => OsuIcon.ModTarget;
|
||||||
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
|
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 0.1;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||||
{
|
{
|
@ -102,8 +102,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
Size = HitArea.DrawSize;
|
Size = HitArea.DrawSize;
|
||||||
|
|
||||||
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
PositionBindable.BindValueChanged(_ => UpdatePosition());
|
||||||
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
StackHeightBindable.BindValueChanged(_ => UpdatePosition());
|
||||||
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
|
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +134,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdatePosition()
|
||||||
|
{
|
||||||
|
Position = HitObject.StackedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
public override void Shake() => shakeContainer.Shake();
|
public override void Shake() => shakeContainer.Shake();
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
|
@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
|
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
|
||||||
{
|
{
|
||||||
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
|
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
|
||||||
// ReSharper disable once RedundantArgumentDefaultValue - removing the "redundant" default value triggers BaseMethodCallWithDefaultParameter
|
|
||||||
|
// ReSharper disable once RedundantArgumentDefaultValue
|
||||||
base.ApplyTransformsAt(time, false);
|
base.ApplyTransformsAt(time, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -43,13 +42,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
PositionBindable.BindValueChanged(_ => updatePosition());
|
|
||||||
pathVersion.BindValueChanged(_ => updatePosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnFree()
|
protected override void OnFree()
|
||||||
{
|
{
|
||||||
base.OnFree();
|
base.OnFree();
|
||||||
@ -57,6 +49,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
pathVersion.UnbindFrom(DrawableSlider.PathVersion);
|
pathVersion.UnbindFrom(DrawableSlider.PathVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void UpdatePosition()
|
||||||
|
{
|
||||||
|
// Slider head is always drawn at (0,0).
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnApply()
|
protected override void OnApply()
|
||||||
{
|
{
|
||||||
base.OnApply();
|
base.OnApply();
|
||||||
@ -100,11 +97,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
base.Shake();
|
base.Shake();
|
||||||
DrawableSlider.Shake();
|
DrawableSlider.Shake();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePosition()
|
|
||||||
{
|
|
||||||
if (Slider != null)
|
|
||||||
Position = HitObject.Position - Slider.Position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
yield return new OsuModSpunOut();
|
yield return new OsuModSpunOut();
|
||||||
|
|
||||||
if (mods.HasFlagFast(LegacyMods.Target))
|
if (mods.HasFlagFast(LegacyMods.Target))
|
||||||
yield return new OsuModTarget();
|
yield return new OsuModTargetPractice();
|
||||||
|
|
||||||
if (mods.HasFlagFast(LegacyMods.TouchDevice))
|
if (mods.HasFlagFast(LegacyMods.TouchDevice))
|
||||||
yield return new OsuModTouchDevice();
|
yield return new OsuModTouchDevice();
|
||||||
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
value |= LegacyMods.SpunOut;
|
value |= LegacyMods.SpunOut;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OsuModTarget:
|
case OsuModTargetPractice:
|
||||||
value |= LegacyMods.Target;
|
value |= LegacyMods.Target;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
case ModType.Conversion:
|
case ModType.Conversion:
|
||||||
return new Mod[]
|
return new Mod[]
|
||||||
{
|
{
|
||||||
new OsuModTarget(),
|
new OsuModTargetPractice(),
|
||||||
new OsuModDifficultyAdjust(),
|
new OsuModDifficultyAdjust(),
|
||||||
new OsuModClassic(),
|
new OsuModClassic(),
|
||||||
new OsuModRandom(),
|
new OsuModRandom(),
|
||||||
@ -201,7 +201,8 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
new OsuModMuted(),
|
new OsuModMuted(),
|
||||||
new OsuModNoScope(),
|
new OsuModNoScope(),
|
||||||
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
|
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
|
||||||
new ModAdaptiveSpeed()
|
new ModAdaptiveSpeed(),
|
||||||
|
new OsuModFreezeFrame()
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.System:
|
case ModType.System:
|
||||||
|
@ -121,12 +121,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
|
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
|
||||||
flash.Colour = colour.NewValue;
|
flash.Colour = colour.NewValue;
|
||||||
|
|
||||||
updateStateTransforms(drawableObject, drawableObject.State.Value);
|
// Accent colour may be changed many times during a paused gameplay state.
|
||||||
|
// Schedule the change to avoid transforms piling up.
|
||||||
|
Scheduler.AddOnce(updateStateTransforms);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value);
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||||
{
|
{
|
||||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||||
@ -171,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
|
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
|
||||||
using (BeginDelayedSequence(flash_in_duration / 12))
|
using (BeginDelayedSequence(flash_in_duration / 12))
|
||||||
{
|
{
|
||||||
outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
|
outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf);
|
||||||
outerGradient
|
outerGradient
|
||||||
.FadeColour(Color4.White, 80)
|
.FadeColour(Color4.White, 80)
|
||||||
.Then()
|
.Then()
|
||||||
|
@ -1,8 +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.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets.Taiko.Mods;
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||||
{
|
{
|
||||||
@ -16,5 +20,37 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
|||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFlashlightAlwaysHasNonZeroSize()
|
||||||
|
{
|
||||||
|
bool failed = false;
|
||||||
|
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TestTaikoModFlashlight { ComboBasedSize = { Value = true } },
|
||||||
|
Autoplay = false,
|
||||||
|
PassCondition = () =>
|
||||||
|
{
|
||||||
|
failed |= this.ChildrenOfType<TestTaikoModFlashlight.TestTaikoFlashlight>().SingleOrDefault()?.FlashlightSize.Y == 0;
|
||||||
|
return !failed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestTaikoModFlashlight : TaikoModFlashlight
|
||||||
|
{
|
||||||
|
protected override Flashlight CreateFlashlight() => new TestTaikoFlashlight(this, Playfield);
|
||||||
|
|
||||||
|
public class TestTaikoFlashlight : TaikoFlashlight
|
||||||
|
{
|
||||||
|
public TestTaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield)
|
||||||
|
: base(modFlashlight, taikoPlayfield)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public new Vector2 FlashlightSize => base.FlashlightSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,28 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
|
|
||||||
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
|
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
|
||||||
|
|
||||||
|
if (original.BeatmapInfo.Ruleset.OnlineID == 0)
|
||||||
|
{
|
||||||
|
// Post processing step to transform standard slider velocity changes into scroll speed changes
|
||||||
|
double lastScrollSpeed = 1;
|
||||||
|
|
||||||
|
foreach (HitObject hitObject in original.HitObjects)
|
||||||
|
{
|
||||||
|
double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity;
|
||||||
|
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||||
|
|
||||||
|
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||||
|
{
|
||||||
|
converted.ControlPointInfo.Add(hitObject.StartTime, new EffectControlPoint
|
||||||
|
{
|
||||||
|
KiaiMode = currentEffectPoint.KiaiMode,
|
||||||
|
OmitFirstBarLine = currentEffectPoint.OmitFirstBarLine,
|
||||||
|
ScrollSpeed = lastScrollSpeed = nextScrollSpeed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (original.BeatmapInfo.Ruleset.OnlineID == 3)
|
if (original.BeatmapInfo.Ruleset.OnlineID == 3)
|
||||||
{
|
{
|
||||||
// Post processing step to transform mania hit objects with the same start time into strong hits
|
// Post processing step to transform mania hit objects with the same start time into strong hits
|
||||||
|
@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
|
|
||||||
public override float DefaultFlashlightSize => 200;
|
public override float DefaultFlashlightSize => 200;
|
||||||
|
|
||||||
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield);
|
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, Playfield);
|
||||||
|
|
||||||
private TaikoPlayfield playfield = null!;
|
protected TaikoPlayfield Playfield { get; private set; } = null!;
|
||||||
|
|
||||||
public override void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
public override void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
Playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||||
base.ApplyToDrawableRuleset(drawableRuleset);
|
base.ApplyToDrawableRuleset(drawableRuleset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TaikoFlashlight : Flashlight
|
public class TaikoFlashlight : Flashlight
|
||||||
{
|
{
|
||||||
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
|
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
|
||||||
private readonly TaikoPlayfield taikoPlayfield;
|
private readonly TaikoPlayfield taikoPlayfield;
|
||||||
@ -47,21 +47,28 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
{
|
{
|
||||||
this.taikoPlayfield = taikoPlayfield;
|
this.taikoPlayfield = taikoPlayfield;
|
||||||
|
|
||||||
FlashlightSize = adjustSize(GetSize());
|
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
|
||||||
FlashlightSmoothness = 1.4f;
|
FlashlightSmoothness = 1.4f;
|
||||||
|
|
||||||
AddLayout(flashlightProperties);
|
AddLayout(flashlightProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 adjustSize(float size)
|
/// <summary>
|
||||||
|
/// Returns the aspect ratio-adjusted size of the flashlight.
|
||||||
|
/// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="size">
|
||||||
|
/// The size of the flashlight.
|
||||||
|
/// The value provided here should always come from <see cref="ModFlashlight{T}.Flashlight.GetSize"/>.
|
||||||
|
/// </param>
|
||||||
|
private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
|
||||||
{
|
{
|
||||||
// Preserve flashlight size through the playfield's aspect adjustment.
|
|
||||||
return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
|
return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateFlashlightSize(float size)
|
protected override void UpdateFlashlightSize(float size)
|
||||||
{
|
{
|
||||||
this.TransformTo(nameof(FlashlightSize), adjustSize(size), FLASHLIGHT_FADE_DURATION);
|
this.TransformTo(nameof(FlashlightSize), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string FragmentShader => "CircularFlashlight";
|
protected override string FragmentShader => "CircularFlashlight";
|
||||||
@ -75,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
|
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
|
||||||
|
|
||||||
ClearTransforms(targetMember: nameof(FlashlightSize));
|
ClearTransforms(targetMember: nameof(FlashlightSize));
|
||||||
FlashlightSize = adjustSize(Combo.Value);
|
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
|
||||||
|
|
||||||
flashlightProperties.Validate();
|
flashlightProperties.Validate();
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location.
|
// Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location.
|
||||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName, null))
|
using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -66,7 +67,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
|
AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
|
||||||
|
|
||||||
assertSnapDistance(100 * multiplier);
|
assertSnapDistance(100 * multiplier, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
@ -79,7 +80,20 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
SliderVelocity = multiplier
|
SliderVelocity = multiplier
|
||||||
}
|
}
|
||||||
});
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier)
|
||||||
|
{
|
||||||
|
assertSnapDistance(100 * multiplier, new HitObject
|
||||||
|
{
|
||||||
|
DifficultyControlPoint = new DifficultyControlPoint
|
||||||
|
{
|
||||||
|
SliderVelocity = multiplier
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
@ -88,7 +102,32 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
||||||
|
|
||||||
assertSnapDistance(100f / divisor);
|
assertSnapDistance(100f / divisor, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The basic distance-duration functions should always include slider velocity of the reference object.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestConversionsWithSliderVelocity()
|
||||||
|
{
|
||||||
|
const float base_distance = 100;
|
||||||
|
const float slider_velocity = 1.2f;
|
||||||
|
|
||||||
|
var referenceObject = new HitObject
|
||||||
|
{
|
||||||
|
DifficultyControlPoint = new DifficultyControlPoint
|
||||||
|
{
|
||||||
|
SliderVelocity = slider_velocity
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||||
|
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
|
||||||
|
assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject);
|
||||||
|
|
||||||
|
assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject);
|
||||||
|
assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -197,20 +236,20 @@ namespace osu.Game.Tests.Editing
|
|||||||
assertSnappedDistance(400, 400);
|
assertSnappedDistance(400, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSnapDistance(float expectedDistance, HitObject? hitObject = null)
|
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||||
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance));
|
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private void assertDurationToDistance(double duration, float expectedDistance)
|
private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
|
||||||
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);
|
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private void assertDistanceToDuration(float distance, double expectedDuration)
|
private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
|
||||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private void assertSnappedDuration(float distance, double expectedDuration)
|
private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
|
||||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(new HitObject(), distance) == expectedDuration);
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private void assertSnappedDistance(float distance, float expectedDistance)
|
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
|
||||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(new HitObject(), distance) == expectedDistance);
|
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private class TestHitObjectComposer : OsuHitObjectComposer
|
private class TestHitObjectComposer : OsuHitObjectComposer
|
||||||
{
|
{
|
||||||
|
@ -106,6 +106,49 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
assertBeatSnap(16);
|
assertBeatSnap(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKeyboardNavigation()
|
||||||
|
{
|
||||||
|
pressKey(1);
|
||||||
|
assertBeatSnap(1);
|
||||||
|
assertPreset(BeatDivisorType.Common);
|
||||||
|
|
||||||
|
pressKey(2);
|
||||||
|
assertBeatSnap(2);
|
||||||
|
assertPreset(BeatDivisorType.Common);
|
||||||
|
|
||||||
|
pressKey(3);
|
||||||
|
assertBeatSnap(3);
|
||||||
|
assertPreset(BeatDivisorType.Triplets);
|
||||||
|
|
||||||
|
pressKey(4);
|
||||||
|
assertBeatSnap(4);
|
||||||
|
assertPreset(BeatDivisorType.Common);
|
||||||
|
|
||||||
|
pressKey(5);
|
||||||
|
assertBeatSnap(5);
|
||||||
|
assertPreset(BeatDivisorType.Custom, 5);
|
||||||
|
|
||||||
|
pressKey(6);
|
||||||
|
assertBeatSnap(6);
|
||||||
|
assertPreset(BeatDivisorType.Triplets);
|
||||||
|
|
||||||
|
pressKey(7);
|
||||||
|
assertBeatSnap(7);
|
||||||
|
assertPreset(BeatDivisorType.Custom, 7);
|
||||||
|
|
||||||
|
pressKey(8);
|
||||||
|
assertBeatSnap(8);
|
||||||
|
assertPreset(BeatDivisorType.Common);
|
||||||
|
|
||||||
|
void pressKey(int key) => AddStep($"press shift+{key}", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
|
InputManager.Key(Key.Number0 + key);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBeatPresetNavigation()
|
public void TestBeatPresetNavigation()
|
||||||
{
|
{
|
||||||
|
@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
IBindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
|
IBindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
|
||||||
|
|
||||||
public float GetBeatSnapDistanceAt(HitObject referenceObject) => beat_snap_distance;
|
public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance;
|
||||||
|
|
||||||
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
|
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
|
||||||
|
|
||||||
|
32
osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs
Normal file
32
osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test editor hotkeys at a high level to ensure they all work well together.
|
||||||
|
/// </summary>
|
||||||
|
public class TestSceneEditorBindings : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBeatDivisorChangeHotkeys()
|
||||||
|
{
|
||||||
|
AddStep("hold shift", () => InputManager.PressKey(Key.LShift));
|
||||||
|
|
||||||
|
AddStep("press 4", () => InputManager.Key(Key.Number4));
|
||||||
|
AddAssert("snap updated to 4", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(4));
|
||||||
|
|
||||||
|
AddStep("press 6", () => InputManager.Key(Key.Number6));
|
||||||
|
AddAssert("snap updated to 6", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(6));
|
||||||
|
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.LShift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -155,6 +155,20 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
|
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClone()
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle { StartTime = 1000 };
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
AddStep("clone", () => Editor.Clone());
|
||||||
|
AddAssert("is two objects", () => EditorBeatmap.HitObjects.Count == 2);
|
||||||
|
AddStep("clone", () => Editor.Clone());
|
||||||
|
AddAssert("is three objects", () => EditorBeatmap.HitObjects.Count == 3);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCutNothing()
|
public void TestCutNothing()
|
||||||
{
|
{
|
||||||
@ -175,5 +189,22 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("paste hitobject", () => Editor.Paste());
|
AddStep("paste hitobject", () => Editor.Paste());
|
||||||
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
|
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCloneNothing()
|
||||||
|
{
|
||||||
|
// Add arbitrary object and copy to clipboard.
|
||||||
|
// This is tested to ensure that clone doesn't incorrectly read from the clipboard when no selection is made.
|
||||||
|
var addedObject = new HitCircle { StartTime = 1000 };
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
AddStep("copy hitobject", () => Editor.Copy());
|
||||||
|
|
||||||
|
AddStep("deselect all objects", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||||
|
|
||||||
|
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
AddStep("clone", () => Editor.Clone());
|
||||||
|
AddAssert("still one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,10 +161,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
|
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
|
||||||
|
|
||||||
AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5));
|
AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5));
|
||||||
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
|
|
||||||
|
|
||||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
|
||||||
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
|
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
|
||||||
|
|
||||||
|
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EditorBeatmapContainer : Container
|
public class EditorBeatmapContainer : Container
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
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.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -26,9 +26,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
|
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private OsuConfigManager localConfig;
|
private OsuConfigManager localConfig = null!;
|
||||||
|
|
||||||
private HUDOverlay hudOverlay;
|
private HUDOverlay hudOverlay = null!;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||||
@ -149,6 +149,41 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
|
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHoldForMenuDoesWorkWhenHidden()
|
||||||
|
{
|
||||||
|
bool activated = false;
|
||||||
|
|
||||||
|
HoldForMenuButton getHoldForMenu() => hudOverlay.ChildrenOfType<HoldForMenuButton>().Single();
|
||||||
|
|
||||||
|
createNew();
|
||||||
|
|
||||||
|
AddStep("bind action", () =>
|
||||||
|
{
|
||||||
|
activated = false;
|
||||||
|
|
||||||
|
var holdForMenu = getHoldForMenu();
|
||||||
|
|
||||||
|
holdForMenu.Action += () => activated = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||||
|
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
||||||
|
|
||||||
|
AddStep("attempt activate", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(getHoldForMenu().OfType<HoldToConfirmContainer>().Single());
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("activated", () => activated);
|
||||||
|
|
||||||
|
AddStep("release mouse button", () =>
|
||||||
|
{
|
||||||
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestInputDoesntWorkWhenHUDHidden()
|
public void TestInputDoesntWorkWhenHUDHidden()
|
||||||
{
|
{
|
||||||
@ -220,7 +255,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
|
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createNew(Action<HUDOverlay> action = null)
|
private void createNew(Action<HUDOverlay>? action = null)
|
||||||
{
|
{
|
||||||
AddStep("create overlay", () =>
|
AddStep("create overlay", () =>
|
||||||
{
|
{
|
||||||
@ -239,7 +274,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
localConfig?.Dispose();
|
if (localConfig.IsNotNull())
|
||||||
|
localConfig.Dispose();
|
||||||
|
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
498
osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
Normal file
498
osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
public class TestSceneScoring : OsuTestScene
|
||||||
|
{
|
||||||
|
private GraphContainer graphs = null!;
|
||||||
|
private SettingsSlider<int> sliderMaxCombo = null!;
|
||||||
|
|
||||||
|
private FillFlowContainer legend = null!;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasic()
|
||||||
|
{
|
||||||
|
AddStep("setup tests", () =>
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
RowDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
graphs = new GraphContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
legend = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding(20),
|
||||||
|
Direction = FillDirection.Full,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding(20),
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Full,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
sliderMaxCombo = new SettingsSlider<int>
|
||||||
|
{
|
||||||
|
Width = 0.5f,
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
Current = new BindableInt(1024)
|
||||||
|
{
|
||||||
|
MinValue = 96,
|
||||||
|
MaxValue = 8192,
|
||||||
|
},
|
||||||
|
LabelText = "max combo",
|
||||||
|
},
|
||||||
|
new OsuTextFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.5f,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
|
||||||
|
|
||||||
|
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
|
||||||
|
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
|
||||||
|
|
||||||
|
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
|
||||||
|
|
||||||
|
rerun();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int base_great = 300;
|
||||||
|
private const int base_ok = 100;
|
||||||
|
|
||||||
|
private void rerun()
|
||||||
|
{
|
||||||
|
graphs.Clear();
|
||||||
|
legend.Clear();
|
||||||
|
|
||||||
|
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
|
||||||
|
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
|
||||||
|
|
||||||
|
runScoreV1();
|
||||||
|
runScoreV2();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runScoreV1()
|
||||||
|
{
|
||||||
|
int totalScore = 0;
|
||||||
|
int currentCombo = 0;
|
||||||
|
|
||||||
|
void applyHitV1(int baseScore)
|
||||||
|
{
|
||||||
|
if (baseScore == 0)
|
||||||
|
{
|
||||||
|
currentCombo = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float score_multiplier = 1;
|
||||||
|
|
||||||
|
totalScore += baseScore;
|
||||||
|
|
||||||
|
// combo multiplier
|
||||||
|
// ReSharper disable once PossibleLossOfFraction
|
||||||
|
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
|
||||||
|
|
||||||
|
currentCombo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
|
||||||
|
() => applyHitV1(base_great),
|
||||||
|
() => applyHitV1(base_ok),
|
||||||
|
() => applyHitV1(0),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
// Arbitrary value chosen towards the upper range.
|
||||||
|
const double score_multiplier = 4;
|
||||||
|
|
||||||
|
return (int)(totalScore * score_multiplier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runScoreV2()
|
||||||
|
{
|
||||||
|
int maxCombo = sliderMaxCombo.Current.Value;
|
||||||
|
|
||||||
|
int currentCombo = 0;
|
||||||
|
double comboPortion = 0;
|
||||||
|
double currentBaseScore = 0;
|
||||||
|
double maxBaseScore = 0;
|
||||||
|
int currentHits = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < maxCombo; i++)
|
||||||
|
applyHitV2(base_great);
|
||||||
|
|
||||||
|
double comboPortionMax = comboPortion;
|
||||||
|
|
||||||
|
currentCombo = 0;
|
||||||
|
comboPortion = 0;
|
||||||
|
currentBaseScore = 0;
|
||||||
|
maxBaseScore = 0;
|
||||||
|
currentHits = 0;
|
||||||
|
|
||||||
|
void applyHitV2(int baseScore)
|
||||||
|
{
|
||||||
|
maxBaseScore += base_great;
|
||||||
|
currentBaseScore += baseScore;
|
||||||
|
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
||||||
|
|
||||||
|
currentHits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
runForAlgorithm("ScoreV2", Color4.OrangeRed,
|
||||||
|
() => applyHitV2(base_great),
|
||||||
|
() => applyHitV2(base_ok),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
currentHits++;
|
||||||
|
maxBaseScore += base_great;
|
||||||
|
currentCombo = 0;
|
||||||
|
}, () =>
|
||||||
|
{
|
||||||
|
double accuracy = currentBaseScore / maxBaseScore;
|
||||||
|
|
||||||
|
return (int)Math.Round
|
||||||
|
(
|
||||||
|
700000 * comboPortion / comboPortionMax +
|
||||||
|
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
|
||||||
|
{
|
||||||
|
int maxCombo = sliderMaxCombo.Current.Value;
|
||||||
|
|
||||||
|
var beatmap = new OsuBeatmap();
|
||||||
|
for (int i = 0; i < maxCombo; i++)
|
||||||
|
beatmap.HitObjects.Add(new HitCircle());
|
||||||
|
|
||||||
|
processor.ApplyBeatmap(beatmap);
|
||||||
|
|
||||||
|
runForAlgorithm(name, colour,
|
||||||
|
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
|
||||||
|
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
|
||||||
|
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
|
||||||
|
() => (int)processor.TotalScore.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<int> getTotalScore)
|
||||||
|
{
|
||||||
|
int maxCombo = sliderMaxCombo.Current.Value;
|
||||||
|
|
||||||
|
List<float> results = new List<float>();
|
||||||
|
|
||||||
|
for (int i = 0; i < maxCombo; i++)
|
||||||
|
{
|
||||||
|
if (graphs.MissLocations.Contains(i))
|
||||||
|
applyMiss();
|
||||||
|
else if (graphs.NonPerfectLocations.Contains(i))
|
||||||
|
applyNonPerfect();
|
||||||
|
else
|
||||||
|
applyHit();
|
||||||
|
|
||||||
|
results.Add(getTotalScore());
|
||||||
|
}
|
||||||
|
|
||||||
|
graphs.Add(new LineGraph
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
LineColour = colour,
|
||||||
|
Values = results
|
||||||
|
});
|
||||||
|
|
||||||
|
legend.Add(new OsuSpriteText
|
||||||
|
{
|
||||||
|
Colour = colour,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.5f,
|
||||||
|
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
|
||||||
|
});
|
||||||
|
|
||||||
|
legend.Add(new OsuSpriteText
|
||||||
|
{
|
||||||
|
Colour = colour,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.5f,
|
||||||
|
Text = $"final score {getTotalScore():#,0}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
|
||||||
|
{
|
||||||
|
public readonly BindableList<double> MissLocations = new BindableList<double>();
|
||||||
|
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
|
||||||
|
|
||||||
|
public Bindable<int> MaxCombo = new Bindable<int>();
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
|
private readonly Box hoverLine;
|
||||||
|
|
||||||
|
private readonly Container missLines;
|
||||||
|
private readonly Container verticalGridLines;
|
||||||
|
|
||||||
|
public int CurrentHoverCombo { get; private set; }
|
||||||
|
|
||||||
|
public GraphContainer()
|
||||||
|
{
|
||||||
|
InternalChild = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = OsuColour.Gray(0.1f),
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
verticalGridLines = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
hoverLine = new Box
|
||||||
|
{
|
||||||
|
Colour = Color4.Yellow,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Alpha = 0,
|
||||||
|
Width = 1,
|
||||||
|
},
|
||||||
|
missLines = new Container
|
||||||
|
{
|
||||||
|
Alpha = 0.6f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
Content,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
||||||
|
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
||||||
|
|
||||||
|
MaxCombo.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
updateMissLocations();
|
||||||
|
updateVerticalGridLines();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVerticalGridLines()
|
||||||
|
{
|
||||||
|
verticalGridLines.Clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < MaxCombo.Value; i++)
|
||||||
|
{
|
||||||
|
if (i % 100 == 0)
|
||||||
|
{
|
||||||
|
verticalGridLines.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = OsuColour.Gray(0.2f),
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Width = 1,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
RelativePositionAxes = Axes.X,
|
||||||
|
X = (float)i / MaxCombo.Value,
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
RelativePositionAxes = Axes.X,
|
||||||
|
X = (float)i / MaxCombo.Value,
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Text = $"{i:#,0}",
|
||||||
|
Rotation = -30,
|
||||||
|
Y = -20,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMissLocations()
|
||||||
|
{
|
||||||
|
missLines.Clear();
|
||||||
|
|
||||||
|
foreach (int miss in MissLocations)
|
||||||
|
{
|
||||||
|
missLines.Add(new Box
|
||||||
|
{
|
||||||
|
Colour = Color4.Red,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Width = 1,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
RelativePositionAxes = Axes.X,
|
||||||
|
X = (float)miss / MaxCombo.Value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int miss in NonPerfectLocations)
|
||||||
|
{
|
||||||
|
missLines.Add(new Box
|
||||||
|
{
|
||||||
|
Colour = Color4.Orange,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Width = 1,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
RelativePositionAxes = Axes.X,
|
||||||
|
X = (float)miss / MaxCombo.Value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
hoverLine.Show();
|
||||||
|
return base.OnHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
hoverLine.Hide();
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
{
|
||||||
|
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
|
||||||
|
|
||||||
|
hoverLine.X = e.MousePosition.X;
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButton.Left)
|
||||||
|
MissLocations.Add(CurrentHoverCombo);
|
||||||
|
else
|
||||||
|
NonPerfectLocations.Add(CurrentHoverCombo);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GraphTooltip? tooltip;
|
||||||
|
|
||||||
|
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
|
||||||
|
|
||||||
|
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
|
||||||
|
|
||||||
|
public class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
|
||||||
|
{
|
||||||
|
private readonly GraphContainer graphContainer;
|
||||||
|
|
||||||
|
private readonly OsuTextFlowContainer textFlow;
|
||||||
|
|
||||||
|
public GraphTooltip(GraphContainer graphContainer)
|
||||||
|
{
|
||||||
|
this.graphContainer = graphContainer;
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
CornerRadius = 10;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = OsuColour.Gray(0.15f),
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
textFlow = new OsuTextFlowContainer
|
||||||
|
{
|
||||||
|
Colour = Color4.White,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding(10),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int? lastContentCombo;
|
||||||
|
|
||||||
|
public void SetContent(IEnumerable<LineGraph> content)
|
||||||
|
{
|
||||||
|
int relevantCombo = graphContainer.CurrentHoverCombo;
|
||||||
|
|
||||||
|
if (lastContentCombo == relevantCombo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastContentCombo = relevantCombo;
|
||||||
|
textFlow.Clear();
|
||||||
|
|
||||||
|
textFlow.AddParagraph($"At combo {relevantCombo}:");
|
||||||
|
|
||||||
|
foreach (var graph in content)
|
||||||
|
{
|
||||||
|
float valueAtHover = graph.Values.ElementAt(relevantCombo);
|
||||||
|
float ofTotal = valueAtHover / graph.Values.Last();
|
||||||
|
|
||||||
|
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Move(Vector2 pos) => this.MoveTo(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -862,52 +862,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestRandomFallbackOnNonMatchingPrevious()
|
|
||||||
{
|
|
||||||
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
|
||||||
|
|
||||||
AddStep("populate maps", () =>
|
|
||||||
{
|
|
||||||
manySets.Clear();
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3, new[]
|
|
||||||
{
|
|
||||||
// all taiko except for first
|
|
||||||
rulesets.GetRuleset(i > 0 ? 1 : 0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadBeatmaps(manySets);
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false));
|
|
||||||
|
|
||||||
AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First()));
|
|
||||||
|
|
||||||
AddStep("Toggle non-matching filter", () =>
|
|
||||||
{
|
|
||||||
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null);
|
|
||||||
|
|
||||||
AddStep("Restore different ruleset filter", () =>
|
|
||||||
{
|
|
||||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false);
|
|
||||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
|
||||||
});
|
|
||||||
|
|
||||||
AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo!.Equals(manySets.First().Beatmaps.First()));
|
|
||||||
}
|
|
||||||
|
|
||||||
AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFilteringByUserStarDifficulty()
|
public void TestFilteringByUserStarDifficulty()
|
||||||
{
|
{
|
||||||
@ -955,6 +909,63 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
checkVisibleItemCount(true, 15);
|
checkVisibleItemCount(true, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselSelectsNextWhenPreviousIsFiltered()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
// 10 sets that go osu! -> taiko -> catch -> osu! -> ...
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
|
||||||
|
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort mode is important to keep the ruleset order
|
||||||
|
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
|
||||||
|
setSelected(1, 1);
|
||||||
|
|
||||||
|
for (int i = 1; i < 10; i++)
|
||||||
|
{
|
||||||
|
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
|
||||||
|
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
|
||||||
|
});
|
||||||
|
waitForSelection(i + 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselSelectsBackwardsWhenDistanceIsShorter()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
// 10 sets that go taiko, osu!, osu!, osu!, taiko, osu!, osu!, osu!, ...
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 4 == 0 ? 1 : 0);
|
||||||
|
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort mode is important to keep the ruleset order
|
||||||
|
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
|
||||||
|
|
||||||
|
for (int i = 2; i < 10; i += 4)
|
||||||
|
{
|
||||||
|
setSelected(i, 1);
|
||||||
|
AddStep("Set ruleset to taiko", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
|
||||||
|
});
|
||||||
|
waitForSelection(i - 1, 1);
|
||||||
|
AddStep("Remove ruleset filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
|
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
|
||||||
bool randomDifficulties = false)
|
bool randomDifficulties = false)
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -24,6 +25,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private readonly Bindable<float> safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
private readonly Bindable<float> safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
||||||
private readonly Bindable<float> safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
private readonly Bindable<float> safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
||||||
|
|
||||||
|
private readonly Bindable<bool> applySafeAreaConsiderations = new Bindable<bool>(true);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -84,6 +87,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Current = safeAreaPaddingRight,
|
Current = safeAreaPaddingRight,
|
||||||
LabelText = "Right"
|
LabelText = "Right"
|
||||||
},
|
},
|
||||||
|
new SettingsCheckbox
|
||||||
|
{
|
||||||
|
LabelText = "Apply",
|
||||||
|
Current = applySafeAreaConsiderations,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea());
|
safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea());
|
||||||
safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea());
|
safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea());
|
||||||
safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea());
|
safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea());
|
||||||
|
applySafeAreaConsiderations.BindValueChanged(_ => updateSafeArea());
|
||||||
});
|
});
|
||||||
|
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
@ -107,6 +116,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Left = safeAreaPaddingLeft.Value,
|
Left = safeAreaPaddingLeft.Value,
|
||||||
Right = safeAreaPaddingRight.Value,
|
Right = safeAreaPaddingRight.Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Game.LocalConfig.SetValue(OsuSetting.SafeAreaConsiderations, applySafeAreaConsiderations.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCustomDirectory()
|
public void TestCustomDirectory()
|
||||||
{
|
{
|
||||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory), null)) // don't use clean run as we are writing a config file.
|
using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
|
||||||
{
|
{
|
||||||
string osuDesktopStorage = Path.Combine(host.UserStoragePaths.First(), nameof(TestCustomDirectory));
|
string osuDesktopStorage = Path.Combine(host.UserStoragePaths.First(), nameof(TestCustomDirectory));
|
||||||
const string custom_tournament = "custom";
|
const string custom_tournament = "custom";
|
||||||
@ -49,7 +49,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
// manual cleaning so we can prepare a config file.
|
// manual cleaning so we can prepare a config file.
|
||||||
storage.DeleteDirectory(string.Empty);
|
storage.DeleteDirectory(string.Empty);
|
||||||
|
|
||||||
using (var storageConfig = new TournamentStorageManager(storage))
|
using (var storageConfig = new TournamentConfigManager(storage))
|
||||||
storageConfig.SetValue(StorageConfig.CurrentTournament, custom_tournament);
|
storageConfig.SetValue(StorageConfig.CurrentTournament, custom_tournament);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -66,82 +66,5 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestMigration()
|
|
||||||
{
|
|
||||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestMigration), null)) // don't use clean run as we are writing test files for migration.
|
|
||||||
{
|
|
||||||
string osuRoot = Path.Combine(host.UserStoragePaths.First(), nameof(TestMigration));
|
|
||||||
string configFile = Path.Combine(osuRoot, "tournament.ini");
|
|
||||||
|
|
||||||
if (File.Exists(configFile))
|
|
||||||
File.Delete(configFile);
|
|
||||||
|
|
||||||
// Recreate the old setup that uses "tournament" as the base path.
|
|
||||||
string oldPath = Path.Combine(osuRoot, "tournament");
|
|
||||||
|
|
||||||
string videosPath = Path.Combine(oldPath, "Videos");
|
|
||||||
string modsPath = Path.Combine(oldPath, "Mods");
|
|
||||||
string flagsPath = Path.Combine(oldPath, "Flags");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(videosPath);
|
|
||||||
Directory.CreateDirectory(modsPath);
|
|
||||||
Directory.CreateDirectory(flagsPath);
|
|
||||||
|
|
||||||
// Define testing files corresponding to the specific file migrations that are needed
|
|
||||||
string bracketFile = Path.Combine(osuRoot, TournamentGameBase.BRACKET_FILENAME);
|
|
||||||
|
|
||||||
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
|
|
||||||
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
|
|
||||||
string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
|
|
||||||
|
|
||||||
// Define sample files to test recursive copying
|
|
||||||
string videoFile = Path.Combine(videosPath, "video.mp4");
|
|
||||||
string modFile = Path.Combine(modsPath, "mod.png");
|
|
||||||
string flagFile = Path.Combine(flagsPath, "flag.png");
|
|
||||||
|
|
||||||
File.WriteAllText(bracketFile, "{}");
|
|
||||||
File.WriteAllText(drawingsConfig, "test");
|
|
||||||
File.WriteAllText(drawingsFile, "test");
|
|
||||||
File.WriteAllText(drawingsResult, "test");
|
|
||||||
File.WriteAllText(videoFile, "test");
|
|
||||||
File.WriteAllText(modFile, "test");
|
|
||||||
File.WriteAllText(flagFile, "test");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var osu = LoadTournament(host);
|
|
||||||
|
|
||||||
var storage = osu.Dependencies.Get<Storage>();
|
|
||||||
|
|
||||||
string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
|
|
||||||
|
|
||||||
videosPath = Path.Combine(migratedPath, "Videos");
|
|
||||||
modsPath = Path.Combine(migratedPath, "Mods");
|
|
||||||
flagsPath = Path.Combine(migratedPath, "Flags");
|
|
||||||
|
|
||||||
videoFile = Path.Combine(videosPath, "video.mp4");
|
|
||||||
modFile = Path.Combine(modsPath, "mod.png");
|
|
||||||
flagFile = Path.Combine(flagsPath, "flag.png");
|
|
||||||
|
|
||||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
|
|
||||||
|
|
||||||
Assert.True(storage.Exists(TournamentGameBase.BRACKET_FILENAME));
|
|
||||||
Assert.True(storage.Exists("drawings.txt"));
|
|
||||||
Assert.True(storage.Exists("drawings_results.txt"));
|
|
||||||
|
|
||||||
Assert.True(storage.Exists("drawings.ini"));
|
|
||||||
|
|
||||||
Assert.True(storage.Exists(videoFile));
|
|
||||||
Assert.True(storage.Exists(modFile));
|
|
||||||
Assert.True(storage.Exists(flagFile));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
host.Exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
public void CheckIPCLocation()
|
public void CheckIPCLocation()
|
||||||
{
|
{
|
||||||
// don't use clean run because files are being written before osu! launches.
|
// don't use clean run because files are being written before osu! launches.
|
||||||
using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation), null))
|
using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation)))
|
||||||
{
|
{
|
||||||
string basePath = Path.Combine(host.UserStoragePaths.First(), nameof(CheckIPCLocation));
|
string basePath = Path.Combine(host.UserStoragePaths.First(), nameof(CheckIPCLocation));
|
||||||
|
|
||||||
|
@ -8,14 +8,23 @@ using osu.Framework.Platform;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Configuration
|
namespace osu.Game.Tournament.Configuration
|
||||||
{
|
{
|
||||||
public class TournamentStorageManager : IniConfigManager<StorageConfig>
|
public class TournamentConfigManager : IniConfigManager<StorageConfig>
|
||||||
{
|
{
|
||||||
protected override string Filename => "tournament.ini";
|
protected override string Filename => "tournament.ini";
|
||||||
|
|
||||||
public TournamentStorageManager(Storage storage)
|
private const string default_tournament = "default";
|
||||||
|
|
||||||
|
public TournamentConfigManager(Storage storage)
|
||||||
: base(storage)
|
: base(storage)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseDefaults()
|
||||||
|
{
|
||||||
|
base.InitialiseDefaults();
|
||||||
|
|
||||||
|
SetDefault(StorageConfig.CurrentTournament, default_tournament);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StorageConfig
|
public enum StorageConfig
|
@ -1,10 +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.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -13,35 +10,28 @@ using osu.Game.Tournament.Configuration;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.IO
|
namespace osu.Game.Tournament.IO
|
||||||
{
|
{
|
||||||
public class TournamentStorage : MigratableStorage
|
public class TournamentStorage : WrappedStorage
|
||||||
{
|
{
|
||||||
private const string default_tournament = "default";
|
|
||||||
private readonly Storage storage;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The storage where all tournaments are located.
|
/// The storage where all tournaments are located.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Storage AllTournaments;
|
public readonly Storage AllTournaments;
|
||||||
|
|
||||||
private readonly TournamentStorageManager storageConfig;
|
|
||||||
public readonly Bindable<string> CurrentTournament;
|
public readonly Bindable<string> CurrentTournament;
|
||||||
|
|
||||||
|
protected TournamentConfigManager TournamentConfigManager { get; }
|
||||||
|
|
||||||
public TournamentStorage(Storage storage)
|
public TournamentStorage(Storage storage)
|
||||||
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
|
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
|
||||||
{
|
{
|
||||||
this.storage = storage;
|
|
||||||
AllTournaments = UnderlyingStorage;
|
AllTournaments = UnderlyingStorage;
|
||||||
|
|
||||||
storageConfig = new TournamentStorageManager(storage);
|
TournamentConfigManager = new TournamentConfigManager(storage);
|
||||||
|
|
||||||
if (storage.Exists("tournament.ini"))
|
CurrentTournament = TournamentConfigManager.GetBindable<string>(StorageConfig.CurrentTournament);
|
||||||
{
|
|
||||||
ChangeTargetStorage(AllTournaments.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
|
ChangeTargetStorage(AllTournaments.GetStorageForDirectory(CurrentTournament.Value));
|
||||||
}
|
|
||||||
else
|
|
||||||
Migrate(AllTournaments.GetStorageForDirectory(default_tournament));
|
|
||||||
|
|
||||||
CurrentTournament = storageConfig.GetBindable<string>(StorageConfig.CurrentTournament);
|
|
||||||
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
|
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
|
||||||
|
|
||||||
CurrentTournament.BindValueChanged(updateTournament);
|
CurrentTournament.BindValueChanged(updateTournament);
|
||||||
@ -53,62 +43,6 @@ namespace osu.Game.Tournament.IO
|
|||||||
Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
|
Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ChangeTargetStorage(Storage newStorage)
|
|
||||||
{
|
|
||||||
// due to an unfortunate oversight, on OSes that are sensitive to pathname casing
|
|
||||||
// the custom flags directory needed to be named `Flags` (uppercase),
|
|
||||||
// while custom mods and videos directories needed to be named `mods` and `videos` respectively (lowercase).
|
|
||||||
// to unify handling to uppercase, move any non-compliant directories automatically for the user to migrate.
|
|
||||||
// can be removed 20220528
|
|
||||||
if (newStorage.ExistsDirectory("flags"))
|
|
||||||
AttemptOperation(() => Directory.Move(newStorage.GetFullPath("flags"), newStorage.GetFullPath("Flags")));
|
|
||||||
if (newStorage.ExistsDirectory("mods"))
|
|
||||||
AttemptOperation(() => Directory.Move(newStorage.GetFullPath("mods"), newStorage.GetFullPath("Mods")));
|
|
||||||
if (newStorage.ExistsDirectory("videos"))
|
|
||||||
AttemptOperation(() => Directory.Move(newStorage.GetFullPath("videos"), newStorage.GetFullPath("Videos")));
|
|
||||||
|
|
||||||
base.ChangeTargetStorage(newStorage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty);
|
public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty);
|
||||||
|
|
||||||
public override bool Migrate(Storage newStorage)
|
|
||||||
{
|
|
||||||
// this migration only happens once on moving to the per-tournament storage system.
|
|
||||||
// listed files are those known at that point in time.
|
|
||||||
// this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
|
|
||||||
|
|
||||||
var source = new DirectoryInfo(storage.GetFullPath("tournament"));
|
|
||||||
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
|
|
||||||
|
|
||||||
if (source.Exists)
|
|
||||||
{
|
|
||||||
Logger.Log("Migrating tournament assets to default tournament storage.");
|
|
||||||
CopyRecursive(source, destination);
|
|
||||||
DeleteRecursive(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveFileIfExists(TournamentGameBase.BRACKET_FILENAME, destination);
|
|
||||||
moveFileIfExists("drawings.txt", destination);
|
|
||||||
moveFileIfExists("drawings_results.txt", destination);
|
|
||||||
moveFileIfExists("drawings.ini", destination);
|
|
||||||
|
|
||||||
ChangeTargetStorage(newStorage);
|
|
||||||
storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament);
|
|
||||||
storageConfig.Save();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void moveFileIfExists(string file, DirectoryInfo destination)
|
|
||||||
{
|
|
||||||
if (!storage.Exists(file))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Logger.Log($"Migrating {file} to default tournament storage.");
|
|
||||||
var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
|
|
||||||
AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
|
|
||||||
fileInfo.Delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
|
|||||||
|
|
||||||
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
|
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
|
||||||
|
|
||||||
req.Success += res =>
|
req.Success += res => Schedule(() =>
|
||||||
{
|
{
|
||||||
Model.Beatmap = new TournamentBeatmap(res);
|
Model.Beatmap = new TournamentBeatmap(res);
|
||||||
updatePanel();
|
updatePanel();
|
||||||
};
|
});
|
||||||
|
|
||||||
req.Failure += _ =>
|
req.Failure += _ => Schedule(() =>
|
||||||
{
|
{
|
||||||
Model.Beatmap = null;
|
Model.Beatmap = null;
|
||||||
updatePanel();
|
updatePanel();
|
||||||
};
|
});
|
||||||
|
|
||||||
API.Queue(req);
|
API.Queue(req);
|
||||||
}, true);
|
}, true);
|
||||||
|
@ -47,6 +47,7 @@ namespace osu.Game.Beatmaps
|
|||||||
// Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
|
// Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
|
||||||
// Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
|
// Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
|
||||||
original.BeatmapInfo = original.BeatmapInfo.Clone();
|
original.BeatmapInfo = original.BeatmapInfo.Clone();
|
||||||
|
original.ControlPointInfo = original.ControlPointInfo.DeepClone();
|
||||||
|
|
||||||
return ConvertBeatmap(original, cancellationToken);
|
return ConvertBeatmap(original, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -238,14 +238,6 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
#region Compatibility properties
|
#region Compatibility properties
|
||||||
|
|
||||||
[Ignored]
|
|
||||||
[Obsolete("Use BeatmapInfo.Difficulty instead.")] // can be removed 20220719
|
|
||||||
public BeatmapDifficulty BaseDifficulty
|
|
||||||
{
|
|
||||||
get => Difficulty;
|
|
||||||
set => Difficulty = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Ignored]
|
[Ignored]
|
||||||
public string? Path => File?.Filename;
|
public string? Path => File?.Filename;
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ namespace osu.Game.Beatmaps
|
|||||||
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
|
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
|
||||||
{
|
{
|
||||||
var metadata = beatmapInfo.Metadata;
|
var metadata = beatmapInfo.Metadata;
|
||||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -22,11 +21,5 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
/// if empty, <see cref="ComboColours"/> will fall back to default combo colours.
|
/// if empty, <see cref="ComboColours"/> will fall back to default combo colours.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
List<Color4> CustomComboColours { get; }
|
List<Color4> CustomComboColours { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds combo colours to the list.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use CustomComboColours directly.")] // can be removed 20220215
|
|
||||||
void AddComboColours(params Color4[] colours);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -435,8 +435,10 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
addControlPoint(time, controlPoint, true);
|
addControlPoint(time, controlPoint, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
|
||||||
|
|
||||||
#pragma warning disable 618
|
#pragma warning disable 618
|
||||||
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
|
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
|
||||||
#pragma warning restore 618
|
#pragma warning restore 618
|
||||||
{
|
{
|
||||||
SliderVelocity = speedMultiplier,
|
SliderVelocity = speedMultiplier,
|
||||||
@ -448,8 +450,6 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
OmitFirstBarLine = omitFirstBarSignature,
|
OmitFirstBarLine = omitFirstBarSignature,
|
||||||
};
|
};
|
||||||
|
|
||||||
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
|
|
||||||
|
|
||||||
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
|
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
|
||||||
if (onlineRulesetID == 1 || onlineRulesetID == 3)
|
if (onlineRulesetID == 1 || onlineRulesetID == 3)
|
||||||
effectPoint.ScrollSpeed = speedMultiplier;
|
effectPoint.ScrollSpeed = speedMultiplier;
|
||||||
|
@ -174,11 +174,15 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool GenerateTicks { get; private set; } = true;
|
public bool GenerateTicks { get; private set; } = true;
|
||||||
|
|
||||||
public LegacyDifficultyControlPoint(double beatLength)
|
public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
|
||||||
: this()
|
: this()
|
||||||
{
|
{
|
||||||
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
|
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
|
||||||
|
if (rulesetId == 1 || rulesetId == 3)
|
||||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
|
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
|
||||||
|
else
|
||||||
|
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
|
||||||
|
|
||||||
GenerateTicks = !double.IsNaN(beatLength);
|
GenerateTicks = !double.IsNaN(beatLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.Timing
|
|
||||||
{
|
|
||||||
[Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")]
|
|
||||||
public enum TimeSignatures // can be removed 20220722
|
|
||||||
{
|
|
||||||
[Description("4/4")]
|
|
||||||
SimpleQuadruple = 4,
|
|
||||||
|
|
||||||
[Description("3/4")]
|
|
||||||
SimpleTriple = 3
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using osu.Game.Database;
|
|
||||||
|
|
||||||
namespace osu.Game.Configuration
|
|
||||||
{
|
|
||||||
[Table("Settings")]
|
|
||||||
public class DatabasedSetting : IHasPrimaryKey // can be removed 20220315.
|
|
||||||
{
|
|
||||||
public int ID { get; set; }
|
|
||||||
|
|
||||||
public bool IsManaged => ID > 0;
|
|
||||||
|
|
||||||
public int? RulesetID { get; set; }
|
|
||||||
|
|
||||||
public int? Variant { get; set; }
|
|
||||||
|
|
||||||
public int? SkinInfoID { get; set; }
|
|
||||||
|
|
||||||
[Column("Key")]
|
|
||||||
public string Key { get; set; }
|
|
||||||
|
|
||||||
[Column("Value")]
|
|
||||||
public string StringValue
|
|
||||||
{
|
|
||||||
get => Value.ToString();
|
|
||||||
set => Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Value;
|
|
||||||
|
|
||||||
public DatabasedSetting(string key, object value)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor for derived classes that may require serialisation.
|
|
||||||
/// </summary>
|
|
||||||
public DatabasedSetting()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{Key}=>{Value}";
|
|
||||||
}
|
|
||||||
}
|
|
@ -118,7 +118,6 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.Prefer24HourTime, CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.Contains(@"tt"));
|
SetDefault(OsuSetting.Prefer24HourTime, CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.Contains(@"tt"));
|
||||||
|
|
||||||
// Gameplay
|
// Gameplay
|
||||||
SetDefault(OsuSetting.PositionalHitsounds, true); // replaced by level setting below, can be removed 20220703.
|
|
||||||
SetDefault(OsuSetting.PositionalHitsoundsLevel, 0.2f, 0, 1);
|
SetDefault(OsuSetting.PositionalHitsoundsLevel, 0.2f, 0, 1);
|
||||||
SetDefault(OsuSetting.DimLevel, 0.7, 0, 1, 0.01);
|
SetDefault(OsuSetting.DimLevel, 0.7, 0, 1, 0.01);
|
||||||
SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
|
SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
|
||||||
@ -128,7 +127,6 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.HitLighting, true);
|
SetDefault(OsuSetting.HitLighting, true);
|
||||||
|
|
||||||
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
|
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
|
||||||
SetDefault(OsuSetting.ShowProgressGraph, true);
|
|
||||||
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
|
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
|
||||||
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
||||||
SetDefault(OsuSetting.KeyOverlay, false);
|
SetDefault(OsuSetting.KeyOverlay, false);
|
||||||
@ -155,6 +153,7 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.SongSelectRightMouseScroll, false);
|
SetDefault(OsuSetting.SongSelectRightMouseScroll, false);
|
||||||
|
|
||||||
SetDefault(OsuSetting.Scaling, ScalingMode.Off);
|
SetDefault(OsuSetting.Scaling, ScalingMode.Off);
|
||||||
|
SetDefault(OsuSetting.SafeAreaConsiderations, true);
|
||||||
|
|
||||||
SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
|
SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
|
||||||
SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
|
SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
|
||||||
@ -204,14 +203,11 @@ namespace osu.Game.Configuration
|
|||||||
if (!int.TryParse(pieces[0], out int year)) return;
|
if (!int.TryParse(pieces[0], out int year)) return;
|
||||||
if (!int.TryParse(pieces[1], out int monthDay)) return;
|
if (!int.TryParse(pieces[1], out int monthDay)) return;
|
||||||
|
|
||||||
|
// ReSharper disable once UnusedVariable
|
||||||
int combined = (year * 10000) + monthDay;
|
int combined = (year * 10000) + monthDay;
|
||||||
|
|
||||||
if (combined < 20220103)
|
// migrations can be added here using a condition like:
|
||||||
{
|
// if (combined < 20220103) { performMigration() }
|
||||||
var positionalHitsoundsEnabled = GetBindable<bool>(OsuSetting.PositionalHitsounds);
|
|
||||||
if (!positionalHitsoundsEnabled.Value)
|
|
||||||
SetValue(OsuSetting.PositionalHitsoundsLevel, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TrackedSettings CreateTrackedSettings()
|
public override TrackedSettings CreateTrackedSettings()
|
||||||
@ -298,14 +294,11 @@ namespace osu.Game.Configuration
|
|||||||
ShowStoryboard,
|
ShowStoryboard,
|
||||||
KeyOverlay,
|
KeyOverlay,
|
||||||
GameplayLeaderboard,
|
GameplayLeaderboard,
|
||||||
PositionalHitsounds,
|
|
||||||
PositionalHitsoundsLevel,
|
PositionalHitsoundsLevel,
|
||||||
AlwaysPlayFirstComboBreak,
|
AlwaysPlayFirstComboBreak,
|
||||||
FloatingComments,
|
FloatingComments,
|
||||||
HUDVisibilityMode,
|
HUDVisibilityMode,
|
||||||
|
|
||||||
// This has been migrated to the component itself. can be removed 20221027.
|
|
||||||
ShowProgressGraph,
|
|
||||||
ShowHealthDisplayWhenCantFail,
|
ShowHealthDisplayWhenCantFail,
|
||||||
FadePlayfieldWhenHealthLow,
|
FadePlayfieldWhenHealthLow,
|
||||||
MouseDisableButtons,
|
MouseDisableButtons,
|
||||||
@ -372,6 +365,7 @@ namespace osu.Game.Configuration
|
|||||||
DiscordRichPresence,
|
DiscordRichPresence,
|
||||||
AutomaticallyDownloadWhenSpectating,
|
AutomaticallyDownloadWhenSpectating,
|
||||||
ShowOnlineExplicitContent,
|
ShowOnlineExplicitContent,
|
||||||
LastProcessedMetadataId
|
LastProcessedMetadataId,
|
||||||
|
SafeAreaConsiderations,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Database
|
|||||||
/// <param name="item">The item to export.</param>
|
/// <param name="item">The item to export.</param>
|
||||||
public void Export(TModel item)
|
public void Export(TModel item)
|
||||||
{
|
{
|
||||||
string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
|
string filename = $"{item.GetDisplayString().GetValidFilename()}{FileExtension}";
|
||||||
|
|
||||||
using (var stream = exportStorage.CreateFileSafely(filename))
|
using (var stream = exportStorage.CreateFileSafely(filename))
|
||||||
ExportModelTo(item, stream);
|
ExportModelTo(item, stream);
|
||||||
|
@ -857,17 +857,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
if (legacyCollectionImporter.GetAvailableCount(storage).GetResultSafely() > 0)
|
if (legacyCollectionImporter.GetAvailableCount(storage).GetResultSafely() > 0)
|
||||||
{
|
{
|
||||||
legacyCollectionImporter.ImportFromStorage(storage).ContinueWith(task =>
|
legacyCollectionImporter.ImportFromStorage(storage).ContinueWith(_ => storage.Move("collection.db", "collection.db.migrated"));
|
||||||
{
|
|
||||||
if (task.Exception != null)
|
|
||||||
{
|
|
||||||
// can be removed 20221027 (just for initial safety).
|
|
||||||
Logger.Error(task.Exception.InnerException, "Collections could not be migrated to realm. Please provide your \"collection.db\" to the dev team.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.Move("collection.db", "collection.db.migrated");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Text.RegularExpressions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
@ -15,6 +15,8 @@ namespace osu.Game.Extensions
|
|||||||
{
|
{
|
||||||
public static class ModelExtensions
|
public static class ModelExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly Regex invalid_filename_chars = new Regex(@"(?!$)[^A-Za-z0-9_()[\]. \-]", RegexOptions.Compiled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the relative path in osu! storage for this file.
|
/// Get the relative path in osu! storage for this file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -137,20 +139,14 @@ namespace osu.Game.Extensions
|
|||||||
return instance.OnlineID.Equals(other.OnlineID);
|
return instance.OnlineID.Equals(other.OnlineID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly char[] invalid_filename_characters = Path.GetInvalidFileNameChars()
|
|
||||||
// Backslash is added to avoid issues when exporting to zip.
|
|
||||||
// See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143.
|
|
||||||
.Append('\\')
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a valid filename for use inside a zip file. Avoids backslashes being incorrectly converted to directories.
|
/// Create a valid filename which should work across all platforms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetValidArchiveContentFilename(this string filename)
|
/// <remarks>
|
||||||
{
|
/// This function replaces all characters not included in a very pessimistic list which should be compatible
|
||||||
foreach (char c in invalid_filename_characters)
|
/// across all operating systems. We are using this in place of <see cref="Path.GetInvalidFileNameChars"/> as
|
||||||
filename = filename.Replace(c, '_');
|
/// that function does not have per-platform considerations (and is only made to work on windows).
|
||||||
return filename;
|
/// </remarks>
|
||||||
}
|
public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
private Bindable<float> sizeY;
|
private Bindable<float> sizeY;
|
||||||
private Bindable<float> posX;
|
private Bindable<float> posX;
|
||||||
private Bindable<float> posY;
|
private Bindable<float> posY;
|
||||||
|
private Bindable<bool> applySafeAreaPadding;
|
||||||
|
|
||||||
private Bindable<MarginPadding> safeAreaPadding;
|
private Bindable<MarginPadding> safeAreaPadding;
|
||||||
|
|
||||||
@ -132,6 +133,9 @@ namespace osu.Game.Graphics.Containers
|
|||||||
posY = config.GetBindable<float>(OsuSetting.ScalingPositionY);
|
posY = config.GetBindable<float>(OsuSetting.ScalingPositionY);
|
||||||
posY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
posY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
||||||
|
|
||||||
|
applySafeAreaPadding = config.GetBindable<bool>(OsuSetting.SafeAreaConsiderations);
|
||||||
|
applySafeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
|
||||||
|
|
||||||
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
|
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
|
||||||
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
|
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
|
||||||
}
|
}
|
||||||
@ -192,7 +196,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
bool requiresMasking = targetRect.Size != Vector2.One
|
bool requiresMasking = targetRect.Size != Vector2.One
|
||||||
// For the top level scaling container, for now we apply masking if safe areas are in use.
|
// For the top level scaling container, for now we apply masking if safe areas are in use.
|
||||||
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|
||||||
|| (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
|
|| (targetMode == ScalingMode.Everything && (applySafeAreaPadding.Value && safeAreaPadding.Value.Total != Vector2.Zero));
|
||||||
|
|
||||||
if (requiresMasking)
|
if (requiresMasking)
|
||||||
sizableContainer.Masking = true;
|
sizableContainer.Masking = true;
|
||||||
@ -225,6 +229,9 @@ namespace osu.Game.Graphics.Containers
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ISafeArea safeArea { get; set; }
|
private ISafeArea safeArea { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; }
|
||||||
|
|
||||||
private readonly bool confineHostCursor;
|
private readonly bool confineHostCursor;
|
||||||
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
|
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
|
||||||
|
|
||||||
@ -259,7 +266,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
{
|
{
|
||||||
if (host.Window == null) return;
|
if (host.Window == null) return;
|
||||||
|
|
||||||
bool coversWholeScreen = Size == Vector2.One && safeArea.SafeAreaPadding.Value.Total == Vector2.Zero;
|
bool coversWholeScreen = Size == Vector2.One && (!config.Get<bool>(OsuSetting.SafeAreaConsiderations) || safeArea.SafeAreaPadding.Value.Total == Vector2.Zero);
|
||||||
host.Window.CursorConfineRect = coversWholeScreen ? null : ToScreenSpace(DrawRectangle).AABBFloat;
|
host.Window.CursorConfineRect = coversWholeScreen ? null : ToScreenSpace(DrawRectangle).AABBFloat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
|
|
||||||
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
|
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
public OsuDirectorySelectorBreadcrumbDisplay()
|
||||||
private void load()
|
|
||||||
{
|
{
|
||||||
Height = 50;
|
Padding = new MarginPadding(15);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
|
private class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
|
||||||
|
@ -31,14 +31,17 @@ namespace osu.Game.Input.Bindings
|
|||||||
parentInputManager = GetContainingInputManager();
|
parentInputManager = GetContainingInputManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: Do not change the order of key bindings in this list.
|
// IMPORTANT: Take care when changing order of the items in the enumerable.
|
||||||
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
|
// It is used to decide the order of precedence, with the earlier items having higher precedence.
|
||||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
|
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
|
||||||
.Concat(OverlayKeyBindings)
|
|
||||||
.Concat(EditorKeyBindings)
|
.Concat(EditorKeyBindings)
|
||||||
.Concat(InGameKeyBindings)
|
.Concat(InGameKeyBindings)
|
||||||
.Concat(SongSelectKeyBindings)
|
.Concat(SongSelectKeyBindings)
|
||||||
.Concat(AudioControlKeyBindings);
|
.Concat(AudioControlKeyBindings)
|
||||||
|
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
|
||||||
|
// It has generally been agreed on that local screens like the editor should have priority,
|
||||||
|
// based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
|
||||||
|
.Concat(OverlayKeyBindings);
|
||||||
|
|
||||||
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
|
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
|
||||||
{
|
{
|
||||||
@ -87,6 +90,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
|
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
|
||||||
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
|
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
|
||||||
|
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
|
||||||
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
||||||
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
||||||
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
|
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
|
||||||
@ -343,5 +347,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
|
||||||
ToggleProfile,
|
ToggleProfile,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
|
||||||
|
EditorCloneSelection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
|
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Clone selection"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Cycle grid display mode"
|
/// "Cycle grid display mode"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -44,7 +44,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
public int MaxCombo { get; set; }
|
public int MaxCombo { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
[JsonProperty("rank")]
|
// ScoreRank is aligned to make 0 equal D. We still want to serialise this (even when DefaultValueHandling.Ignore is used).
|
||||||
|
[JsonProperty("rank", DefaultValueHandling = DefaultValueHandling.Include)]
|
||||||
public ScoreRank Rank { get; set; }
|
public ScoreRank Rank { get; set; }
|
||||||
|
|
||||||
[JsonProperty("started_at")]
|
[JsonProperty("started_at")]
|
||||||
@ -153,10 +154,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
|
|
||||||
var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray();
|
var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray();
|
||||||
|
|
||||||
var scoreInfo = ToScoreInfo(mods);
|
var scoreInfo = ToScoreInfo(mods, beatmap);
|
||||||
|
|
||||||
scoreInfo.Ruleset = ruleset;
|
scoreInfo.Ruleset = ruleset;
|
||||||
if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
|
|
||||||
|
|
||||||
return scoreInfo;
|
return scoreInfo;
|
||||||
}
|
}
|
||||||
@ -165,8 +164,18 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
/// Create a <see cref="ScoreInfo"/> from an API score instance.
|
/// Create a <see cref="ScoreInfo"/> from an API score instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mods">The mod instances, resolved from a ruleset.</param>
|
/// <param name="mods">The mod instances, resolved from a ruleset.</param>
|
||||||
/// <returns></returns>
|
/// <param name="beatmap">The object to populate the scores' beatmap with.
|
||||||
public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
|
///<list type="bullet">
|
||||||
|
/// <item>If this is a <see cref="BeatmapInfo"/> type, then the score will be fully populated with the given object.</item>
|
||||||
|
/// <item>Otherwise, if this is an <see cref="IBeatmapInfo"/> type (e.g. <see cref="APIBeatmap"/>), then only the beatmap ruleset will be populated.</item>
|
||||||
|
/// <item>Otherwise, if this is <c>null</c>, then the beatmap ruleset will not be populated.</item>
|
||||||
|
/// <item>The online beatmap ID is populated in all cases.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The populated <see cref="ScoreInfo"/>.</returns>
|
||||||
|
public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
|
||||||
|
{
|
||||||
|
var score = new ScoreInfo
|
||||||
{
|
{
|
||||||
OnlineID = OnlineID,
|
OnlineID = OnlineID,
|
||||||
User = User ?? new APIUser { Id = UserID },
|
User = User ?? new APIUser { Id = UserID },
|
||||||
@ -185,6 +194,18 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
PP = PP,
|
PP = PP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (beatmap is BeatmapInfo realmBeatmap)
|
||||||
|
score.BeatmapInfo = realmBeatmap;
|
||||||
|
else if (beatmap != null)
|
||||||
|
{
|
||||||
|
score.BeatmapInfo.Ruleset.OnlineID = beatmap.Ruleset.OnlineID;
|
||||||
|
score.BeatmapInfo.Ruleset.Name = beatmap.Ruleset.Name;
|
||||||
|
score.BeatmapInfo.Ruleset.ShortName = beatmap.Ruleset.ShortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="SoloScoreInfo"/> from a local score for score submission.
|
/// Creates a <see cref="SoloScoreInfo"/> from a local score for score submission.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
28
osu.Game/Online/HubClient.cs
Normal file
28
osu.Game/Online/HubClient.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
||||||
|
namespace osu.Game.Online
|
||||||
|
{
|
||||||
|
public class HubClient : PersistentEndpointClient
|
||||||
|
{
|
||||||
|
public readonly HubConnection Connection;
|
||||||
|
|
||||||
|
public HubClient(HubConnection connection)
|
||||||
|
{
|
||||||
|
Connection = connection;
|
||||||
|
Connection.Closed += InvokeClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConnectAsync(CancellationToken cancellationToken) => Connection.StartAsync(cancellationToken);
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await base.DisposeAsync().ConfigureAwait(false);
|
||||||
|
await Connection.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,13 +10,11 @@ using Microsoft.AspNetCore.SignalR.Client;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online
|
namespace osu.Game.Online
|
||||||
{
|
{
|
||||||
public class HubClientConnector : IHubClientConnector
|
public class HubClientConnector : PersistentEndpointClientConnector, IHubClientConnector
|
||||||
{
|
{
|
||||||
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
|
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
|
||||||
|
|
||||||
@ -25,7 +23,6 @@ namespace osu.Game.Online
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<HubConnection>? ConfigureConnection { get; set; }
|
public Action<HubConnection>? ConfigureConnection { get; set; }
|
||||||
|
|
||||||
private readonly string clientName;
|
|
||||||
private readonly string endpoint;
|
private readonly string endpoint;
|
||||||
private readonly string versionHash;
|
private readonly string versionHash;
|
||||||
private readonly bool preferMessagePack;
|
private readonly bool preferMessagePack;
|
||||||
@ -34,18 +31,7 @@ namespace osu.Game.Online
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current connection opened by this connector.
|
/// The current connection opened by this connector.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HubConnection? CurrentConnection { get; private set; }
|
public new HubConnection? CurrentConnection => ((HubClient?)base.CurrentConnection)?.Connection;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this is connected to the hub, use <see cref="CurrentConnection"/> to access the connection, if this is <c>true</c>.
|
|
||||||
/// </summary>
|
|
||||||
public IBindable<bool> IsConnected => isConnected;
|
|
||||||
|
|
||||||
private readonly Bindable<bool> isConnected = new Bindable<bool>();
|
|
||||||
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
|
|
||||||
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new <see cref="HubClientConnector"/>.
|
/// Constructs a new <see cref="HubClientConnector"/>.
|
||||||
@ -56,99 +42,16 @@ namespace osu.Game.Online
|
|||||||
/// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param>
|
/// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param>
|
||||||
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
|
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
|
||||||
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
|
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
|
||||||
|
: base(api)
|
||||||
{
|
{
|
||||||
this.clientName = clientName;
|
ClientName = clientName;
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.versionHash = versionHash;
|
this.versionHash = versionHash;
|
||||||
this.preferMessagePack = preferMessagePack;
|
this.preferMessagePack = preferMessagePack;
|
||||||
|
|
||||||
apiState.BindTo(api.State);
|
|
||||||
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Reconnect()
|
protected override Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
|
|
||||||
return Task.Run(connectIfPossible);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task connectIfPossible()
|
|
||||||
{
|
|
||||||
switch (apiState.Value)
|
|
||||||
{
|
|
||||||
case APIState.Failing:
|
|
||||||
case APIState.Offline:
|
|
||||||
await disconnect(true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case APIState.Online:
|
|
||||||
await connect();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task connect()
|
|
||||||
{
|
|
||||||
cancelExistingConnect();
|
|
||||||
|
|
||||||
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
|
||||||
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (apiState.Value == APIState.Online)
|
|
||||||
{
|
|
||||||
// ensure any previous connection was disposed.
|
|
||||||
// this will also create a new cancellation token source.
|
|
||||||
await disconnect(false).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// this token will be valid for the scope of this connection.
|
|
||||||
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
|
|
||||||
var cancellationToken = connectCancelSource.Token;
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
Logger.Log($"{clientName} connecting...", LoggingTarget.Network);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// importantly, rebuild the connection each attempt to get an updated access token.
|
|
||||||
CurrentConnection = buildConnection(cancellationToken);
|
|
||||||
|
|
||||||
await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Logger.Log($"{clientName} connected!", LoggingTarget.Network);
|
|
||||||
isConnected.Value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
//connection process was cancelled.
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
connectionLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles an exception and delays an async flow.
|
|
||||||
/// </summary>
|
|
||||||
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Logger.Log($"{clientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
|
|
||||||
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HubConnection buildConnection(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var builder = new HubConnectionBuilder()
|
var builder = new HubConnectionBuilder()
|
||||||
.WithUrl(endpoint, options =>
|
.WithUrl(endpoint, options =>
|
||||||
@ -188,59 +91,9 @@ namespace osu.Game.Online
|
|||||||
|
|
||||||
ConfigureConnection?.Invoke(newConnection);
|
ConfigureConnection?.Invoke(newConnection);
|
||||||
|
|
||||||
newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
|
return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection));
|
||||||
return newConnection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
|
protected override string ClientName { get; }
|
||||||
{
|
|
||||||
isConnected.Value = false;
|
|
||||||
|
|
||||||
if (ex != null)
|
|
||||||
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
Logger.Log($"{clientName} disconnected", LoggingTarget.Network);
|
|
||||||
|
|
||||||
// make sure a disconnect wasn't triggered (and this is still the active connection).
|
|
||||||
if (!cancellationToken.IsCancellationRequested)
|
|
||||||
await Task.Run(connect, default).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task disconnect(bool takeLock)
|
|
||||||
{
|
|
||||||
cancelExistingConnect();
|
|
||||||
|
|
||||||
if (takeLock)
|
|
||||||
{
|
|
||||||
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
|
||||||
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (CurrentConnection != null)
|
|
||||||
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
CurrentConnection = null;
|
|
||||||
if (takeLock)
|
|
||||||
connectionLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelExistingConnect()
|
|
||||||
{
|
|
||||||
connectCancelSource.Cancel();
|
|
||||||
connectCancelSource = new CancellationTokenSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}";
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
apiState.UnbindAll();
|
|
||||||
cancelExistingConnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
osu.Game/Online/PersistentEndpointClient.cs
Normal file
35
osu.Game/Online/PersistentEndpointClient.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace osu.Game.Online
|
||||||
|
{
|
||||||
|
public abstract class PersistentEndpointClient : IAsyncDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event notifying the <see cref="PersistentEndpointClientConnector"/> that the connection has been closed
|
||||||
|
/// </summary>
|
||||||
|
public event Func<Exception?, Task>? Closed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notifies the <see cref="PersistentEndpointClientConnector"/> that the connection has been closed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">The exception that the connection closed with.</param>
|
||||||
|
protected Task InvokeClosed(Exception? exception) => Closed?.Invoke(exception) ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects the client to the remote service to begin processing messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to stop processing messages.</param>
|
||||||
|
public abstract Task ConnectAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public virtual ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Closed = null;
|
||||||
|
return new ValueTask(Task.CompletedTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
198
osu.Game/Online/PersistentEndpointClientConnector.cs
Normal file
198
osu.Game/Online/PersistentEndpointClientConnector.cs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
|
namespace osu.Game.Online
|
||||||
|
{
|
||||||
|
public abstract class PersistentEndpointClientConnector : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the managed connection is currently connected. When <c>true</c> use <see cref="CurrentConnection"/> to access the connection.
|
||||||
|
/// </summary>
|
||||||
|
public IBindable<bool> IsConnected => isConnected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current connection opened by this connector.
|
||||||
|
/// </summary>
|
||||||
|
public PersistentEndpointClient? CurrentConnection { get; private set; }
|
||||||
|
|
||||||
|
private readonly Bindable<bool> isConnected = new Bindable<bool>();
|
||||||
|
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
|
||||||
|
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api"> An API provider used to react to connection state changes.</param>
|
||||||
|
protected PersistentEndpointClientConnector(IAPIProvider api)
|
||||||
|
{
|
||||||
|
apiState.BindTo(api.State);
|
||||||
|
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Reconnect()
|
||||||
|
{
|
||||||
|
Logger.Log($"{ClientName} reconnecting...", LoggingTarget.Network);
|
||||||
|
return Task.Run(connectIfPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task connectIfPossible()
|
||||||
|
{
|
||||||
|
switch (apiState.Value)
|
||||||
|
{
|
||||||
|
case APIState.Failing:
|
||||||
|
case APIState.Offline:
|
||||||
|
await disconnect(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case APIState.Online:
|
||||||
|
await connect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task connect()
|
||||||
|
{
|
||||||
|
cancelExistingConnect();
|
||||||
|
|
||||||
|
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
||||||
|
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (apiState.Value == APIState.Online)
|
||||||
|
{
|
||||||
|
// ensure any previous connection was disposed.
|
||||||
|
// this will also create a new cancellation token source.
|
||||||
|
await disconnect(false).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// this token will be valid for the scope of this connection.
|
||||||
|
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
|
||||||
|
var cancellationToken = connectCancelSource.Token;
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Logger.Log($"{ClientName} connecting...", LoggingTarget.Network);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// importantly, rebuild the connection each attempt to get an updated access token.
|
||||||
|
CurrentConnection = await BuildConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
CurrentConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
await CurrentConnection.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Logger.Log($"{ClientName} connected!", LoggingTarget.Network);
|
||||||
|
isConnected.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
//connection process was cancelled.
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
connectionLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles an exception and delays an async flow.
|
||||||
|
/// </summary>
|
||||||
|
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
|
||||||
|
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="PersistentEndpointClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to stop the process.</param>
|
||||||
|
protected abstract Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
isConnected.Value = false;
|
||||||
|
|
||||||
|
if (ex != null)
|
||||||
|
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
|
||||||
|
|
||||||
|
// make sure a disconnect wasn't triggered (and this is still the active connection).
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
await Task.Run(connect, default).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task disconnect(bool takeLock)
|
||||||
|
{
|
||||||
|
cancelExistingConnect();
|
||||||
|
|
||||||
|
if (takeLock)
|
||||||
|
{
|
||||||
|
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
||||||
|
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (CurrentConnection != null)
|
||||||
|
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CurrentConnection = null;
|
||||||
|
if (takeLock)
|
||||||
|
connectionLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelExistingConnect()
|
||||||
|
{
|
||||||
|
connectCancelSource.Cancel();
|
||||||
|
connectCancelSource = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual string ClientName => GetType().ReadableName();
|
||||||
|
|
||||||
|
public override string ToString() => $"{ClientName} ({(IsConnected.Value ? "connected" : "not connected")})";
|
||||||
|
|
||||||
|
private bool isDisposed;
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
if (isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
apiState.UnbindAll();
|
||||||
|
cancelExistingConnect();
|
||||||
|
|
||||||
|
isDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -179,6 +179,8 @@ namespace osu.Game
|
|||||||
|
|
||||||
private Bindable<string> configRuleset;
|
private Bindable<string> configRuleset;
|
||||||
|
|
||||||
|
private Bindable<bool> applySafeAreaConsiderations;
|
||||||
|
|
||||||
private Bindable<float> uiScale;
|
private Bindable<float> uiScale;
|
||||||
|
|
||||||
private Bindable<string> configSkin;
|
private Bindable<string> configSkin;
|
||||||
@ -280,10 +282,7 @@ namespace osu.Game
|
|||||||
configRuleset = LocalConfig.GetBindable<string>(OsuSetting.Ruleset);
|
configRuleset = LocalConfig.GetBindable<string>(OsuSetting.Ruleset);
|
||||||
uiScale = LocalConfig.GetBindable<float>(OsuSetting.UIScale);
|
uiScale = LocalConfig.GetBindable<float>(OsuSetting.UIScale);
|
||||||
|
|
||||||
var preferredRuleset = int.TryParse(configRuleset.Value, out int rulesetId)
|
var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value);
|
||||||
// int parsing can be removed 20220522
|
|
||||||
? RulesetStore.GetRuleset(rulesetId)
|
|
||||||
: RulesetStore.GetRuleset(configRuleset.Value);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -312,6 +311,9 @@ namespace osu.Game
|
|||||||
|
|
||||||
SelectedMods.BindValueChanged(modsChanged);
|
SelectedMods.BindValueChanged(modsChanged);
|
||||||
Beatmap.BindValueChanged(beatmapChanged, true);
|
Beatmap.BindValueChanged(beatmapChanged, true);
|
||||||
|
|
||||||
|
applySafeAreaConsiderations = LocalConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations);
|
||||||
|
applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalLinkOpener externalLinkOpener;
|
private ExternalLinkOpener externalLinkOpener;
|
||||||
|
@ -21,7 +21,11 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Handlers;
|
using osu.Framework.Input.Handlers;
|
||||||
|
using osu.Framework.Input.Handlers.Joystick;
|
||||||
using osu.Framework.Input.Handlers.Midi;
|
using osu.Framework.Input.Handlers.Midi;
|
||||||
|
using osu.Framework.Input.Handlers.Mouse;
|
||||||
|
using osu.Framework.Input.Handlers.Tablet;
|
||||||
|
using osu.Framework.Input.Handlers.Touch;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -46,6 +50,7 @@ using osu.Game.Online.Spectator;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Overlays.Settings.Sections;
|
using osu.Game.Overlays.Settings.Sections;
|
||||||
|
using osu.Game.Overlays.Settings.Sections.Input;
|
||||||
using osu.Game.Resources;
|
using osu.Game.Resources;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -189,6 +194,8 @@ namespace osu.Game
|
|||||||
|
|
||||||
private RealmAccess realm;
|
private RealmAccess realm;
|
||||||
|
|
||||||
|
protected SafeAreaContainer SafeAreaContainer { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For now, this is used as a source specifically for beat synced components.
|
/// For now, this is used as a source specifically for beat synced components.
|
||||||
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
|
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
|
||||||
@ -341,7 +348,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
GlobalActionContainer globalBindings;
|
GlobalActionContainer globalBindings;
|
||||||
|
|
||||||
base.Content.Add(new SafeAreaContainer
|
base.Content.Add(SafeAreaContainer = new SafeAreaContainer
|
||||||
{
|
{
|
||||||
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
|
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -521,6 +528,29 @@ namespace osu.Game
|
|||||||
/// <remarks>Should be overriden per-platform to provide settings for platform-specific handlers.</remarks>
|
/// <remarks>Should be overriden per-platform to provide settings for platform-specific handlers.</remarks>
|
||||||
public virtual SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
public virtual SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
||||||
{
|
{
|
||||||
|
// One would think that this could be moved to the `OsuGameDesktop` class, but doing so means that
|
||||||
|
// OsuGameTestScenes will not show any input options (as they are based on OsuGame not OsuGameDesktop).
|
||||||
|
//
|
||||||
|
// This in turn makes it hard for ruleset creators to adjust input settings while testing their ruleset
|
||||||
|
// within the test browser interface.
|
||||||
|
if (RuntimeInfo.IsDesktop)
|
||||||
|
{
|
||||||
|
switch (handler)
|
||||||
|
{
|
||||||
|
case ITabletHandler th:
|
||||||
|
return new TabletSettings(th);
|
||||||
|
|
||||||
|
case MouseHandler mh:
|
||||||
|
return new MouseSettings(mh);
|
||||||
|
|
||||||
|
case JoystickHandler jh:
|
||||||
|
return new JoystickSettings(jh);
|
||||||
|
|
||||||
|
case TouchHandler:
|
||||||
|
return new InputSection.HandlerSection(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (handler)
|
switch (handler)
|
||||||
{
|
{
|
||||||
case MidiHandler:
|
case MidiHandler:
|
||||||
|
@ -115,6 +115,7 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
filterControl.Search(query);
|
filterControl.Search(query);
|
||||||
Show();
|
Show();
|
||||||
|
ScrollFlow.ScrollToStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
|
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
|
||||||
|
@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool completionSent;
|
private int completionSent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempt to post a completion notification.
|
/// Attempt to post a completion notification.
|
||||||
@ -162,11 +162,11 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
if (CompletionTarget == null)
|
if (CompletionTarget == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (completionSent)
|
// Thread-safe barrier, as this may be called by a web request and also scheduled to the update thread at the same time.
|
||||||
|
if (Interlocked.Exchange(ref completionSent, 1) == 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CompletionTarget.Invoke(CreateCompletionNotification());
|
CompletionTarget.Invoke(CreateCompletionNotification());
|
||||||
completionSent = true;
|
|
||||||
|
|
||||||
Close(false);
|
Close(false);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings.Sections.Graphics
|
namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||||
@ -50,6 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
private SettingsDropdown<Size> resolutionDropdown = null!;
|
private SettingsDropdown<Size> resolutionDropdown = null!;
|
||||||
private SettingsDropdown<Display> displayDropdown = null!;
|
private SettingsDropdown<Display> displayDropdown = null!;
|
||||||
private SettingsDropdown<WindowMode> windowModeDropdown = null!;
|
private SettingsDropdown<WindowMode> windowModeDropdown = null!;
|
||||||
|
private SettingsCheckbox safeAreaConsiderationsCheckbox = null!;
|
||||||
|
|
||||||
private Bindable<float> scalingPositionX = null!;
|
private Bindable<float> scalingPositionX = null!;
|
||||||
private Bindable<float> scalingPositionY = null!;
|
private Bindable<float> scalingPositionY = null!;
|
||||||
@ -101,6 +103,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
ItemSource = resolutions,
|
ItemSource = resolutions,
|
||||||
Current = sizeFullscreen
|
Current = sizeFullscreen
|
||||||
},
|
},
|
||||||
|
safeAreaConsiderationsCheckbox = new SettingsCheckbox
|
||||||
|
{
|
||||||
|
LabelText = "Shrink game to avoid cameras and notches",
|
||||||
|
Current = osuConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations),
|
||||||
|
},
|
||||||
new SettingsSlider<float, UIScaleSlider>
|
new SettingsSlider<float, UIScaleSlider>
|
||||||
{
|
{
|
||||||
LabelText = GraphicsSettingsStrings.UIScaling,
|
LabelText = GraphicsSettingsStrings.UIScaling,
|
||||||
@ -166,7 +173,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
|
|
||||||
windowModeDropdown.Current.BindValueChanged(_ =>
|
windowModeDropdown.Current.BindValueChanged(_ =>
|
||||||
{
|
{
|
||||||
updateDisplayModeDropdowns();
|
updateDisplaySettingsVisibility();
|
||||||
updateScreenModeWarning();
|
updateScreenModeWarning();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@ -191,7 +198,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
.Distinct());
|
.Distinct());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDisplayModeDropdowns();
|
updateDisplaySettingsVisibility();
|
||||||
}), true);
|
}), true);
|
||||||
|
|
||||||
scalingMode.BindValueChanged(_ =>
|
scalingMode.BindValueChanged(_ =>
|
||||||
@ -221,11 +228,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
Scheduler.AddOnce(d =>
|
Scheduler.AddOnce(d =>
|
||||||
{
|
{
|
||||||
displayDropdown.Items = d;
|
displayDropdown.Items = d;
|
||||||
updateDisplayModeDropdowns();
|
updateDisplaySettingsVisibility();
|
||||||
}, displays);
|
}, displays);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDisplayModeDropdowns()
|
private void updateDisplaySettingsVisibility()
|
||||||
{
|
{
|
||||||
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
|
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
|
||||||
resolutionDropdown.Show();
|
resolutionDropdown.Show();
|
||||||
@ -236,6 +243,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
displayDropdown.Show();
|
displayDropdown.Show();
|
||||||
else
|
else
|
||||||
displayDropdown.Hide();
|
displayDropdown.Hide();
|
||||||
|
|
||||||
|
if (host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero)
|
||||||
|
safeAreaConsiderationsCheckbox.Show();
|
||||||
|
else
|
||||||
|
safeAreaConsiderationsCheckbox.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateScreenModeWarning()
|
private void updateScreenModeWarning()
|
||||||
|
@ -141,6 +141,8 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
Name = "Right buttons",
|
Name = "Right buttons",
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
AutoSizeAxes = Axes.X,
|
AutoSizeAxes = Axes.X,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Box
|
new Box
|
||||||
|
@ -23,9 +23,13 @@ namespace osu.Game.Overlays.Volume
|
|||||||
{
|
{
|
||||||
case GlobalAction.DecreaseVolume:
|
case GlobalAction.DecreaseVolume:
|
||||||
case GlobalAction.IncreaseVolume:
|
case GlobalAction.IncreaseVolume:
|
||||||
|
ActionRequested?.Invoke(e.Action);
|
||||||
|
return true;
|
||||||
|
|
||||||
case GlobalAction.ToggleMute:
|
case GlobalAction.ToggleMute:
|
||||||
case GlobalAction.NextVolumeMeter:
|
case GlobalAction.NextVolumeMeter:
|
||||||
case GlobalAction.PreviousVolumeMeter:
|
case GlobalAction.PreviousVolumeMeter:
|
||||||
|
if (!e.Repeat)
|
||||||
ActionRequested?.Invoke(e.Action);
|
ActionRequested?.Invoke(e.Action);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
@ -10,9 +13,11 @@ using osu.Framework.Extensions.LocalisationExtensions;
|
|||||||
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.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
@ -20,6 +25,7 @@ using osu.Game.Overlays;
|
|||||||
using osu.Game.Overlays.OSD;
|
using osu.Game.Overlays.OSD;
|
||||||
using osu.Game.Overlays.Settings.Sections;
|
using osu.Game.Overlays.Settings.Sections;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Edit
|
namespace osu.Game.Rulesets.Edit
|
||||||
{
|
{
|
||||||
@ -32,7 +38,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
{
|
{
|
||||||
private const float adjust_step = 0.1f;
|
private const float adjust_step = 0.1f;
|
||||||
|
|
||||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
|
public BindableDouble DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
|
||||||
{
|
{
|
||||||
MinValue = 0.1,
|
MinValue = 0.1,
|
||||||
MaxValue = 6.0,
|
MaxValue = 6.0,
|
||||||
@ -44,10 +50,15 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
|
protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
|
||||||
|
|
||||||
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider;
|
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider;
|
||||||
|
private ExpandableButton currentDistanceSpacingButton;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private OnScreenDisplay onScreenDisplay { get; set; }
|
private OnScreenDisplay onScreenDisplay { get; set; }
|
||||||
|
|
||||||
|
protected readonly Bindable<TernaryState> DistanceSnapToggle = new Bindable<TernaryState>();
|
||||||
|
|
||||||
|
private bool distanceSnapMomentary;
|
||||||
|
|
||||||
protected DistancedHitObjectComposer(Ruleset ruleset)
|
protected DistancedHitObjectComposer(Ruleset ruleset)
|
||||||
: base(ruleset)
|
: base(ruleset)
|
||||||
{
|
{
|
||||||
@ -74,10 +85,27 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
||||||
Child = new EditorToolboxGroup("snapping")
|
Child = new EditorToolboxGroup("snapping")
|
||||||
{
|
{
|
||||||
Child = distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
|
||||||
{
|
{
|
||||||
Current = { BindTarget = DistanceSpacingMultiplier },
|
|
||||||
KeyboardStep = adjust_step,
|
KeyboardStep = adjust_step,
|
||||||
|
// Manual binding in LoadComplete to handle one-way event flow.
|
||||||
|
Current = DistanceSpacingMultiplier.GetUnboundCopy(),
|
||||||
|
},
|
||||||
|
currentDistanceSpacingButton = new ExpandableButton
|
||||||
|
{
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
(HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
|
||||||
|
|
||||||
|
Debug.Assert(objects != null);
|
||||||
|
|
||||||
|
DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
|
||||||
|
DistanceSnapToggle.Value = TernaryState.True;
|
||||||
|
},
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,6 +113,51 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
|
||||||
|
{
|
||||||
|
HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject;
|
||||||
|
|
||||||
|
if (lastBefore == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
HitObject firstAfter = Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= EditorClock.CurrentTime)?.HitObject;
|
||||||
|
|
||||||
|
if (firstAfter == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (lastBefore == firstAfter)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (lastBefore, firstAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract double ReadCurrentDistanceSnap(HitObject before, HitObject after);
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
(HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
|
||||||
|
|
||||||
|
double currentSnap = objects == null
|
||||||
|
? 0
|
||||||
|
: ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
|
||||||
|
|
||||||
|
if (currentSnap > DistanceSpacingMultiplier.MinValue)
|
||||||
|
{
|
||||||
|
currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
|
||||||
|
&& !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
|
||||||
|
currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
|
||||||
|
currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentDistanceSpacingButton.Enabled.Value = false;
|
||||||
|
currentDistanceSpacingButton.ContractedLabelText = string.Empty;
|
||||||
|
currentDistanceSpacingButton.ExpandedLabelText = "Use current (unavailable)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -102,6 +175,45 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
|
|
||||||
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
|
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
// Manual binding to handle enabling distance spacing when the slider is interacted with.
|
||||||
|
distanceSpacingSlider.Current.BindValueChanged(spacing =>
|
||||||
|
{
|
||||||
|
DistanceSpacingMultiplier.Value = spacing.NewValue;
|
||||||
|
DistanceSnapToggle.Value = TernaryState.True;
|
||||||
|
});
|
||||||
|
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||||
|
{
|
||||||
|
new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
if (e.Repeat)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
handleToggleViaKey(e);
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyUp(KeyUpEvent e)
|
||||||
|
{
|
||||||
|
handleToggleViaKey(e);
|
||||||
|
base.OnKeyUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleToggleViaKey(KeyboardEvent key)
|
||||||
|
{
|
||||||
|
bool altPressed = key.AltPressed;
|
||||||
|
|
||||||
|
if (altPressed != distanceSnapMomentary)
|
||||||
|
{
|
||||||
|
distanceSnapMomentary = altPressed;
|
||||||
|
DistanceSnapToggle.Value = DistanceSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +223,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
{
|
{
|
||||||
case GlobalAction.EditorIncreaseDistanceSpacing:
|
case GlobalAction.EditorIncreaseDistanceSpacing:
|
||||||
case GlobalAction.EditorDecreaseDistanceSpacing:
|
case GlobalAction.EditorDecreaseDistanceSpacing:
|
||||||
return adjustDistanceSpacing(e.Action, adjust_step);
|
return AdjustDistanceSpacing(e.Action, adjust_step);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -127,13 +239,13 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
{
|
{
|
||||||
case GlobalAction.EditorIncreaseDistanceSpacing:
|
case GlobalAction.EditorIncreaseDistanceSpacing:
|
||||||
case GlobalAction.EditorDecreaseDistanceSpacing:
|
case GlobalAction.EditorDecreaseDistanceSpacing:
|
||||||
return adjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
|
return AdjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool adjustDistanceSpacing(GlobalAction action, float amount)
|
protected virtual bool AdjustDistanceSpacing(GlobalAction action, float amount)
|
||||||
{
|
{
|
||||||
if (DistanceSpacingMultiplier.Disabled)
|
if (DistanceSpacingMultiplier.Disabled)
|
||||||
return false;
|
return false;
|
||||||
@ -143,12 +255,13 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
else if (action == GlobalAction.EditorDecreaseDistanceSpacing)
|
else if (action == GlobalAction.EditorDecreaseDistanceSpacing)
|
||||||
DistanceSpacingMultiplier.Value -= amount;
|
DistanceSpacingMultiplier.Value -= amount;
|
||||||
|
|
||||||
|
DistanceSnapToggle.Value = TernaryState.True;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
|
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
|
||||||
{
|
{
|
||||||
return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
|
return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual float DurationToDistance(HitObject referenceObject, double duration)
|
public virtual float DurationToDistance(HitObject referenceObject, double duration)
|
||||||
|
101
osu.Game/Rulesets/Edit/ExpandableButton.cs
Normal file
101
osu.Game/Rulesets/Edit/ExpandableButton.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// 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.Framework.Localisation;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Edit
|
||||||
|
{
|
||||||
|
internal class ExpandableButton : RoundedButton, IExpandable
|
||||||
|
{
|
||||||
|
private float actualHeight;
|
||||||
|
|
||||||
|
public override float Height
|
||||||
|
{
|
||||||
|
get => base.Height;
|
||||||
|
set => base.Height = actualHeight = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalisableString contractedLabelText;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The label text to display when this button is in a contracted state.
|
||||||
|
/// </summary>
|
||||||
|
public LocalisableString ContractedLabelText
|
||||||
|
{
|
||||||
|
get => contractedLabelText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == contractedLabelText)
|
||||||
|
return;
|
||||||
|
|
||||||
|
contractedLabelText = value;
|
||||||
|
|
||||||
|
if (!Expanded.Value)
|
||||||
|
Text = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalisableString expandedLabelText;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The label text to display when this button is in an expanded state.
|
||||||
|
/// </summary>
|
||||||
|
public LocalisableString ExpandedLabelText
|
||||||
|
{
|
||||||
|
get => expandedLabelText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == expandedLabelText)
|
||||||
|
return;
|
||||||
|
|
||||||
|
expandedLabelText = value;
|
||||||
|
|
||||||
|
if (Expanded.Value)
|
||||||
|
Text = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BindableBool Expanded { get; } = new BindableBool();
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private IExpandingContainer? expandingContainer { get; set; }
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
expandingContainer?.Expanded.BindValueChanged(containerExpanded =>
|
||||||
|
{
|
||||||
|
Expanded.Value = containerExpanded.NewValue;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
Expanded.BindValueChanged(expanded =>
|
||||||
|
{
|
||||||
|
Text = expanded.NewValue ? expandedLabelText : contractedLabelText;
|
||||||
|
|
||||||
|
if (expanded.NewValue)
|
||||||
|
{
|
||||||
|
SpriteText.Anchor = Anchor.Centre;
|
||||||
|
SpriteText.Origin = Anchor.Centre;
|
||||||
|
SpriteText.Font = OsuFont.GetFont(weight: FontWeight.Bold);
|
||||||
|
base.Height = actualHeight;
|
||||||
|
Background.Show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SpriteText.Anchor = Anchor.CentreLeft;
|
||||||
|
SpriteText.Origin = Anchor.CentreLeft;
|
||||||
|
SpriteText.Font = OsuFont.GetFont(weight: FontWeight.Regular);
|
||||||
|
base.Height = actualHeight / 2;
|
||||||
|
Background.Hide();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
|
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (checkLeftToggleFromKey(e.Key, out int leftIndex))
|
if (checkLeftToggleFromKey(e.Key, out int leftIndex))
|
||||||
|
@ -27,8 +27,9 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// Retrieves the distance between two points within a timing point that are one beat length apart.
|
/// Retrieves the distance between two points within a timing point that are one beat length apart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
||||||
|
/// <param name="useReferenceSliderVelocity">Whether the <paramref name="referenceObject"/>'s slider velocity should be factored into the returned distance.</param>
|
||||||
/// <returns>The distance between two points residing in the timing point that are one beat length apart.</returns>
|
/// <returns>The distance between two points residing in the timing point that are one beat length apart.</returns>
|
||||||
float GetBeatSnapDistanceAt(HitObject referenceObject);
|
float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a duration to a distance without applying any snapping.
|
/// Converts a duration to a distance without applying any snapping.
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mods
|
|
||||||
{
|
|
||||||
[Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
|
|
||||||
public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
|
|
||||||
{
|
|
||||||
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
|
|
||||||
|
|
||||||
void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mods
|
|
||||||
{
|
|
||||||
[Obsolete("Use ICreateReplayData instead")] // Can be removed 20220929
|
|
||||||
public interface ICreateReplay : ICreateReplayData
|
|
||||||
{
|
|
||||||
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
|
|
||||||
|
|
||||||
ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
|
||||||
{
|
|
||||||
var replayScore = CreateReplayScore(beatmap, mods);
|
|
||||||
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -101,9 +101,6 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual bool ValidForMultiplayerAsFreeMod => true;
|
public virtual bool ValidForMultiplayerAsFreeMod => true;
|
||||||
|
|
||||||
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
|
|
||||||
public virtual bool Ranked => false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this mod requires configuration to apply changes to the game.
|
/// Whether this mod requires configuration to apply changes to the game.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -8,7 +8,6 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mods
|
namespace osu.Game.Rulesets.Mods
|
||||||
{
|
{
|
||||||
@ -33,16 +32,6 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
|
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
|
||||||
|
|
||||||
[Obsolete("Override CreateReplayData(IBeatmap, IReadOnlyList<Mod>) instead")] // Can be removed 20220929
|
public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new ModReplayData(new Replay(), new ModCreatedUser { Username = @"autoplay" });
|
||||||
public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score { Replay = new Replay() };
|
|
||||||
|
|
||||||
public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
|
||||||
{
|
|
||||||
#pragma warning disable CS0618
|
|
||||||
var replayScore = CreateReplayScore(beatmap, mods);
|
|
||||||
#pragma warning restore CS0618
|
|
||||||
|
|
||||||
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user