1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 13:37:25 +08:00

Merge branch 'master' into bezier-convert

This commit is contained in:
Dean Herbert 2022-10-31 15:41:08 +09:00
commit 31ba77e378
74 changed files with 1536 additions and 693 deletions

View File

@ -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)

View File

@ -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

View File

@ -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.1028.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. -->

View File

@ -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>();

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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>();

View File

@ -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; }

View 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);
}
}
}

View 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,
},
}
},
};
}
}
}

View 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);
}
}
}
}

View 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);
}
}
}
}

View 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);
}
}
}
}
}

View 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
};
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
{ {

View File

@ -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()
{ {

View File

@ -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
{ {

View File

@ -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
{ {

View File

@ -44,6 +44,28 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
rectangularGridActive(false); 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());
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);
}
private void rectangularGridActive(bool active) private void rectangularGridActive(bool active)
{ {
AddStep("choose placement tool", () => InputManager.Key(Key.Number2)); AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
@ -57,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]

View File

@ -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)

View File

@ -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;
var vertices = new List<Vector2>(); if (lastVersion != hitObject.Path.Version.Value)
hitObject.Path.GetPathToProgress(vertices, 0, 1); {
lastVersion = hitObject.Path.Version.Value;
body.SetVertices(vertices); var vertices = new List<Vector2>();
hitObject.Path.GetPathToProgress(vertices, 0, 1);
body.SetVertices(vertices);
}
Size = body.Size; Size = body.Size;
OriginPosition = body.PathOffset; OriginPosition = body.PathOffset;

View File

@ -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,13 +141,31 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
base.OnDeselected(); base.OnDeselected();
// throw away frame buffers on deselection. updateVisualDefinition();
ControlPointVisualiser?.Expire();
ControlPointVisualiser = null;
BodyPiece.RecyclePath(); 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 = null;
}
}
private Vector2 rightClickPosition; private Vector2 rightClickPosition;
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -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),
},
}; };
} }

View File

@ -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,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid(); placementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
// we may be entering the screen with a selection already active // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
@ -100,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();
@ -120,13 +129,30 @@ 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); 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));
@ -195,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)
@ -222,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)

View File

@ -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)

View File

@ -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);
} }

View File

@ -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;
}
} }
} }

View File

@ -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

View File

@ -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
{ {

View File

@ -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

View File

@ -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);
} }
} }

View File

@ -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]

View File

@ -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();
}
}
}
} }
} }

View File

@ -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));

View File

@ -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

View File

@ -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();
}
} }
} }

View File

@ -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;

View File

@ -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();
} }
} }

View File

@ -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);
} }
} }

View File

@ -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;

View File

@ -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?).
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; if (rulesetId == 1 || rulesetId == 3)
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);
} }

View File

@ -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
}
}

View File

@ -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}";
}
}

View File

@ -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);
@ -127,7 +126,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);
@ -154,6 +152,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);
@ -203,14 +202,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()
@ -296,14 +292,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,
@ -370,6 +363,7 @@ namespace osu.Game.Configuration
DiscordRichPresence, DiscordRichPresence,
AutomaticallyDownloadWhenSpectating, AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent, ShowOnlineExplicitContent,
LastProcessedMetadataId LastProcessedMetadataId,
SafeAreaConsiderations,
} }
} }

View File

@ -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);

View File

@ -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;

View File

@ -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, "_");
} }
} }

View File

@ -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;
} }
} }

View File

@ -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,25 +164,47 @@ 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)
{ {
OnlineID = OnlineID, var score = new ScoreInfo
User = User ?? new APIUser { Id = UserID }, {
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, OnlineID = OnlineID,
Ruleset = new RulesetInfo { OnlineID = RulesetID }, User = User ?? new APIUser { Id = UserID },
Passed = Passed, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
TotalScore = TotalScore, Ruleset = new RulesetInfo { OnlineID = RulesetID },
Accuracy = Accuracy, Passed = Passed,
MaxCombo = MaxCombo, TotalScore = TotalScore,
Rank = Rank, Accuracy = Accuracy,
Statistics = Statistics, MaxCombo = MaxCombo,
MaximumStatistics = MaximumStatistics, Rank = Rank,
Date = EndedAt, Statistics = Statistics,
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary? MaximumStatistics = MaximumStatistics,
Mods = mods, Date = EndedAt,
PP = PP, Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
}; Mods = mods,
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.

View File

@ -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);
} }
private ExternalLinkOpener externalLinkOpener; private ExternalLinkOpener externalLinkOpener;

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -23,10 +23,14 @@ 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:
ActionRequested?.Invoke(e.Action); if (!e.Repeat)
ActionRequested?.Invoke(e.Action);
return true; return true;
} }

View File

@ -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[]
{ {
Current = { BindTarget = DistanceSpacingMultiplier }, distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
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,6 +255,7 @@ 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;
} }

View 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);
}
}
}

View File

@ -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());
}
}

View File

@ -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 });
}
}
}

View File

@ -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>

View File

@ -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 });
}
} }
} }

View File

@ -196,18 +196,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true); updateState(State.Value, true);
} }
/// <summary>
/// Applies a hit object to be represented by this <see cref="DrawableHitObject"/>.
/// </summary>
[Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] // Can be removed 20211021.
public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
{
if (lifetimeEntry != null)
Apply(lifetimeEntry);
else
Apply(hitObject);
}
/// <summary> /// <summary>
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>. /// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
/// A new <see cref="HitObjectLifetimeEntry"/> is automatically created and applied to this <see cref="DrawableHitObject"/>. /// A new <see cref="HitObjectLifetimeEntry"/> is automatically created and applied to this <see cref="DrawableHitObject"/>.

View File

@ -1,28 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Edit.Components
{
public class CircularButton : OsuButton
{
private const float width = 125;
private const float height = 30;
public CircularButton()
{
Size = new Vector2(width, height);
}
protected override void Update()
{
base.Update();
Content.CornerRadius = DrawHeight / 2f;
Content.CornerExponent = 2;
}
}
}

View File

@ -250,7 +250,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void seekTrackToCurrent() private void seekTrackToCurrent()
{ {
double target = Current / Content.DrawWidth * editorClock.TrackLength; double target = TimeAtPosition(Current);
editorClock.Seek(Math.Min(editorClock.TrackLength, target)); editorClock.Seek(Math.Min(editorClock.TrackLength, target));
} }
@ -264,7 +264,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (handlingDragInput) if (handlingDragInput)
editorClock.Stop(); editorClock.Stop();
ScrollTo((float)(editorClock.CurrentTime / editorClock.TrackLength) * Content.DrawWidth, false); float position = PositionAtTime(editorClock.CurrentTime);
ScrollTo(position, false);
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -113,7 +113,7 @@ namespace osu.Game.Screens.Menu
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
logoBounceContainer = new DragContainer logoBounceContainer = new Container
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
@ -407,27 +407,24 @@ namespace osu.Game.Screens.Menu
impactContainer.ScaleTo(1.12f, 250); impactContainer.ScaleTo(1.12f, 250);
} }
private class DragContainer : Container public override bool DragBlocksClick => false;
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{ {
public override bool DragBlocksClick => false; Vector2 change = e.MousePosition - e.MouseDownPosition;
protected override bool OnDragStart(DragStartEvent e) => true; // Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length;
protected override void OnDrag(DragEvent e) logoBounceContainer.MoveTo(change);
{ }
Vector2 change = e.MousePosition - e.MouseDownPosition;
// Diminish the drag distance as we go further to simulate "rubber band" feeling. protected override void OnDragEnd(DragEndEvent e)
change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length; {
logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
this.MoveTo(change); base.OnDragEnd(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
this.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
base.OnDragEnd(e);
}
} }
} }
} }

View File

@ -9,7 +9,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
@ -45,12 +44,6 @@ namespace osu.Game.Screens.Play.HUD
[Resolved] [Resolved]
private DrawableRuleset? drawableRuleset { get; set; } private DrawableRuleset? drawableRuleset { get; set; }
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private SkinManager skinManager { get; set; } = null!;
public DefaultSongProgress() public DefaultSongProgress()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -100,47 +93,6 @@ namespace osu.Game.Screens.Play.HUD
{ {
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true); AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
migrateSettingFromConfig();
}
/// <summary>
/// This setting has been migrated to a per-component level.
/// Only take the value from the config if it is in a non-default state (then reset it to default so it only applies once).
///
/// Can be removed 20221027.
/// </summary>
private void migrateSettingFromConfig()
{
Bindable<bool> configShowGraph = config.GetBindable<bool>(OsuSetting.ShowProgressGraph);
if (!configShowGraph.IsDefault)
{
ShowGraph.Value = configShowGraph.Value;
// This is pretty ugly, but the only way to make this stick...
var skinnableTarget = this.FindClosestParent<ISkinnableTarget>();
if (skinnableTarget != null)
{
// If the skin is not mutable, a mutable instance will be created, causing this migration logic to run again on the correct skin.
// Therefore we want to avoid resetting the config value on this invocation.
if (skinManager.EnsureMutableSkin())
return;
// If `EnsureMutableSkin` actually changed the skin, default layout may take a frame to apply.
// See `SkinnableTargetComponentsContainer`'s use of ScheduleAfterChildren.
ScheduleAfterChildren(() =>
{
var skin = skinManager.CurrentSkin.Value;
skin.UpdateDrawableTarget(skinnableTarget);
skinManager.Save(skin);
});
configShowGraph.SetDefault();
}
}
} }
protected override void PopIn() protected override void PopIn()

View File

@ -279,6 +279,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
switch (style) switch (style)
{ {
case LabelStyles.None: case LabelStyles.None:
labelEarly.Clear();
labelLate.Clear();
break; break;
case LabelStyles.Icons: case LabelStyles.Icons:

View File

@ -39,9 +39,16 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public float BottomScoringElementsHeight { get; private set; } public float BottomScoringElementsHeight { get; private set; }
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display. protected override bool ShouldBeConsideredForInput(Drawable child)
// Without blocking input, this would also allow them to be interacted with in such a state. {
public override bool PropagatePositionalInputSubTree => ShowHud.Value; // HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
// Without blocking input, this would also allow them to be interacted with in such a state.
if (ShowHud.Value)
return base.ShouldBeConsideredForInput(child);
// hold to quit button should always be interactive.
return child == bottomRightElements;
}
public readonly KeyCounterDisplay KeyCounter; public readonly KeyCounterDisplay KeyCounter;
public readonly ModDisplay ModDisplay; public readonly ModDisplay ModDisplay;

View File

@ -36,12 +36,6 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary> /// </summary>
public readonly bool RequiresHitEvents; public readonly bool RequiresHitEvents;
[Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803.
public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)
: this(name, () => content, true, dimension)
{
}
/// <summary> /// <summary>
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen. /// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
/// </summary> /// </summary>

View File

@ -66,8 +66,6 @@ namespace osu.Game.Skinning
} }
} }
void IHasComboColours.AddComboColours(params Color4[] colours) => CustomComboColours.AddRange(colours);
public Dictionary<string, Color4> CustomColours { get; } = new Dictionary<string, Color4>(); public Dictionary<string, Color4> CustomColours { get; } = new Dictionary<string, Color4>();
public readonly Dictionary<string, string> ConfigDictionary = new Dictionary<string, string>(); public readonly Dictionary<string, string> ConfigDictionary = new Dictionary<string, string>();

View File

@ -4,11 +4,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
@ -33,9 +31,6 @@ namespace osu.Game.Skinning
this.skinResources = skinResources; this.skinResources = skinResources;
modelManager = new ModelManager<SkinInfo>(storage, realm); modelManager = new ModelManager<SkinInfo>(storage, realm);
// can be removed 20220420.
populateMissingHashes();
} }
public override IEnumerable<string> HandledExtensions => new[] { ".osk" }; public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
@ -158,18 +153,6 @@ namespace osu.Game.Skinning
} }
modelManager.ReplaceFile(existingFile, stream, realm); modelManager.ReplaceFile(existingFile, stream, realm);
// can be removed 20220502.
if (!ensureIniWasUpdated(item))
{
Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important);
var existingIni = item.GetFile(@"skin.ini");
if (existingIni != null)
item.Files.Remove(existingIni);
writeNewSkinIni();
}
} }
} }
@ -194,38 +177,6 @@ namespace osu.Game.Skinning
} }
} }
private bool ensureIniWasUpdated(SkinInfo item)
{
// This is a final consistency check to ensure that hash computation doesn't enter an infinite loop.
// With other changes to the surrounding code this should never be hit, but until we are 101% sure that there
// are no other cases let's avoid a hard startup crash by bailing and alerting.
var instance = createInstance(item);
return instance.Configuration.SkinInfo.Name == item.Name;
}
private void populateMissingHashes()
{
Realm.Run(realm =>
{
var skinsWithoutHashes = realm.All<SkinInfo>().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray();
foreach (SkinInfo skin in skinsWithoutHashes)
{
try
{
realm.Write(_ => skin.Hash = ComputeHash(skin));
}
catch (Exception e)
{
modelManager.Delete(skin);
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
}
}
});
}
private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources); private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources);
public void Save(Skin skin) public void Save(Skin skin)

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.17.0" /> <PackageReference Include="Realm" Version="10.17.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1022.1" /> <PackageReference Include="ppy.osu.Framework" Version="2022.1028.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
<PackageReference Include="Sentry" Version="3.22.0" /> <PackageReference Include="Sentry" Version="3.22.0" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -62,7 +62,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<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.iOS" Version="2022.1022.1" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1028.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -82,7 +82,7 @@
<PackageReference Include="DiffPlex" Version="1.7.1" /> <PackageReference Include="DiffPlex" Version="1.7.1" />
<PackageReference Include="Humanizer" Version="2.14.1" /> <PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1022.1" /> <PackageReference Include="ppy.osu.Framework" Version="2022.1028.0" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />