1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-09 03:02:56 +08:00

Merge branch 'master' into grids-1

This commit is contained in:
Bartłomiej Dach 2024-06-03 12:33:51 +02:00
commit 4fcb902341
No known key found for this signature in database
89 changed files with 1566 additions and 256 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.523.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.528.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("112643")] [TestCase("112643")]
[TestCase("1041052", new[] { typeof(CatchModHardRock) })] [TestCase("1041052", new[] { typeof(CatchModHardRock) })]
[TestCase("high-speed-multiplier-precision")]
public new void Test(string name, params Type[] mods) => base.Test(name, mods); public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -0,0 +1 @@
{"Mappings":[{"StartTime":265568.0,"Objects":[{"StartTime":265568.0,"Position":486.0,"HyperDash":false},{"StartTime":265658.0,"Position":465.1873,"HyperDash":false},{"StartTime":265749.0,"Position":463.208435,"HyperDash":false},{"StartTime":265840.0,"Position":465.146484,"HyperDash":false},{"StartTime":265967.0,"Position":459.5862,"HyperDash":false}]}]}

View File

@ -0,0 +1,238 @@
osu file format v14
[General]
AudioFilename: audio.mp3
AudioLeadIn: 0
PreviewTime: 226943
Countdown: 0
SampleSet: Soft
StackLeniency: 0.7
Mode: 2
LetterboxInBreaks: 0
WidescreenStoryboard: 1
[Editor]
Bookmarks: 85568,86768,90968,265568
DistanceSpacing: 0.9
BeatDivisor: 12
GridSize: 16
TimelineZoom: 1
[Metadata]
Title:Snow
TitleUnicode:Snow
Artist:Ricky Montgomery
ArtistUnicode:Ricky Montgomery
Creator:Crowley
Version:Bury Me Six Feet in Snow
Source:
Tags:indie the honeysticks alternative english
BeatmapID:2062131
BeatmapSetID:971028
[Difficulty]
HPDrainRate:6
CircleSize:4.2
OverallDifficulty:8.3
ApproachRate:8.3
SliderMultiplier:3.59999990463257
SliderTickRate:1
[Events]
//Background and Video events
0,0,"me.jpg",0,0
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Layer 4 (Overlay)
//Storyboard Sound Samples
[TimingPoints]
368,1200,2,2,1,30,1,0
368,-66.6666666666667,2,2,1,30,0,0
29168,-58.8235294117647,2,2,1,40,0,0
30368,-58.8235294117647,2,2,2,40,0,0
30568,-58.8235294117647,2,2,1,40,0,0
31368,-58.8235294117647,2,2,2,40,0,0
31568,-58.8235294117647,2,2,1,40,0,0
32768,-58.8235294117647,2,2,2,40,0,0
33568,-58.8235294117647,2,2,2,40,0,0
33968,-58.8235294117647,2,2,1,40,0,0
35168,-58.8235294117647,2,2,2,40,0,0
35968,-58.8235294117647,2,2,1,40,0,0
36168,-58.8235294117647,2,2,2,40,0,0
36368,-58.8235294117647,2,2,1,40,0,0
37568,-58.8235294117647,2,2,2,40,0,0
37968,-58.8235294117647,2,2,1,40,0,0
38368,-58.8235294117647,2,2,2,40,0,0
38768,-58.8235294117647,2,2,1,40,0,0
39968,-58.8235294117647,2,2,2,40,0,0
40168,-58.8235294117647,2,2,1,40,0,0
40968,-58.8235294117647,2,2,2,40,0,0
41168,-58.8235294117647,2,2,1,40,0,0
42368,-58.8235294117647,2,2,2,40,0,0
43168,-58.8235294117647,2,2,2,40,0,0
43568,-58.8235294117647,2,2,1,40,0,0
44768,-58.8235294117647,2,2,2,40,0,0
45768,-58.8235294117647,2,2,2,40,0,0
45968,-58.8235294117647,2,2,1,50,0,0
47168,-58.8235294117647,2,2,2,50,0,0
48368,-62.5,2,2,1,50,0,0
67568,-58.8235294117647,2,2,1,70,0,1
84668,-58.8235294117647,2,2,1,5,0,1
84768,-58.8235294117647,2,2,1,70,0,1
85068,-58.8235294117647,2,2,1,5,0,1
85168,-58.8235294117647,2,2,1,70,0,1
85468,-58.8235294117647,2,2,1,5,0,1
85568,-58.8235294117647,2,2,1,70,0,1
86768,-58.8235294117647,2,2,1,30,0,0
91168,-58.8235294117647,2,2,1,50,0,0
91568,1200,2,2,1,50,1,0
91568,-58.8235294117647,2,2,1,50,0,1
91643,-58.8235294117647,2,2,1,50,0,0
92768,-58.8235294117647,2,2,2,50,0,0
92968,-58.8235294117647,2,2,1,50,0,0
95168,-58.8235294117647,2,2,2,50,0,0
95368,-58.8235294117647,2,2,1,50,0,0
97568,-58.8235294117647,2,2,2,50,0,0
97768,-58.8235294117647,2,2,1,50,0,0
99968,-58.8235294117647,2,2,2,50,0,0
100168,-58.8235294117647,2,2,1,50,0,0
100768,-58.8235294117647,2,2,2,50,0,0
101168,-58.8235294117647,2,2,1,50,0,0
102368,-58.8235294117647,2,2,2,50,0,0
102568,-58.8235294117647,2,2,1,50,0,0
104768,-58.8235294117647,2,2,2,50,0,0
104968,-58.8235294117647,2,2,1,50,0,0
107168,-58.8235294117647,2,2,2,50,0,0
107368,-58.8235294117647,2,2,1,50,0,0
108968,-58.8235294117647,2,2,2,50,0,0
109168,-58.8235294117647,2,2,1,50,0,0
109568,-58.8235294117647,2,2,2,50,0,0
109968,-58.8235294117647,2,2,1,50,0,0
110368,-58.8235294117647,2,2,2,50,0,0
110768,-100,2,2,1,40,0,0
127568,-62.5,2,2,2,50,0,0
127968,-62.5,2,2,1,50,0,0
128168,-62.5,2,2,2,50,0,0
129968,-58.8235294117647,2,2,1,50,0,0
131168,-58.8235294117647,2,2,2,50,0,0
131368,-58.8235294117647,2,2,1,50,0,0
133568,-58.8235294117647,2,2,2,50,0,0
133768,-58.8235294117647,2,2,1,50,0,0
135968,-58.8235294117647,2,2,2,50,0,0
136168,-58.8235294117647,2,2,1,50,0,0
138368,-58.8235294117647,2,2,2,50,0,0
138568,-58.8235294117647,2,2,1,50,0,0
139168,-58.8235294117647,2,2,2,50,0,0
139368,-58.8235294117647,2,2,1,50,0,0
139568,-58.8235294117647,2,2,1,50,0,0
140768,-58.8235294117647,2,2,2,50,0,0
140968,-58.8235294117647,2,2,1,50,0,0
143168,-58.8235294117647,2,2,2,50,0,0
143368,-58.8235294117647,2,2,1,50,0,0
145568,-58.8235294117647,2,2,2,50,0,0
145768,-58.8235294117647,2,2,1,50,0,0
147368,-58.8235294117647,2,2,2,50,0,0
147768,-58.8235294117647,2,2,1,50,0,0
147968,-58.8235294117647,2,2,1,60,0,0
148768,-58.8235294117647,2,2,2,60,0,0
149168,-58.8235294117647,2,2,1,70,0,1
158268,-58.8235294117647,2,2,2,70,0,1
158568,-58.8235294117647,2,2,1,70,0,1
166268,-58.8235294117647,2,2,1,5,0,1
166368,-58.8235294117647,2,2,1,70,0,1
166668,-58.8235294117647,2,2,1,5,0,1
166768,-58.8235294117647,2,2,1,70,0,1
167068,-58.8235294117647,2,2,1,5,0,1
167168,-58.8235294117647,2,2,1,70,0,1
168368,-62.5,2,2,1,50,0,0
172368,-62.5,2,2,1,50,0,1
173168,-62.5,2,2,1,50,0,0
185168,-62.5,2,2,1,60,0,0
185468,-62.5,2,2,1,5,0,0
185568,-62.5,2,2,1,60,0,0
185868,-62.5,2,2,1,5,0,0
185968,-62.5,2,2,1,60,0,0
186268,-62.5,2,2,1,5,0,0
186368,-62.5,2,2,1,60,0,0
186668,-62.5,2,2,1,5,0,0
186768,-52.6315789473684,2,2,1,60,0,0
187068,-62.5,2,2,1,5,0,0
187168,-62.5,2,2,1,60,0,0
187468,-62.5,2,2,1,5,0,0
187568,-62.5,2,2,1,20,0,0
187768,-62.5,2,2,1,24,0,0
187968,-62.5,2,2,1,28,0,0
188168,-62.5,2,2,1,32,0,0
188368,-62.5,2,2,1,36,0,0
188568,-62.5,2,2,1,40,0,0
188768,1200,2,2,1,50,1,1
188768,-58.8235294117647,2,2,1,50,0,1
188843,-58.8235294117647,2,2,1,50,0,0
189968,-58.8235294117647,2,2,2,50,0,0
190168,-58.8235294117647,2,2,1,50,0,0
192368,-58.8235294117647,2,2,2,50,0,0
192568,-58.8235294117647,2,2,1,50,0,0
194768,-58.8235294117647,2,2,2,50,0,0
194968,-58.8235294117647,2,2,1,50,0,0
196568,-58.8235294117647,2,2,2,50,0,0
196768,-58.8235294117647,2,2,1,50,0,0
197168,-58.8235294117647,2,2,2,50,0,0
197368,-58.8235294117647,2,2,1,50,0,0
197568,-58.8235294117647,2,2,2,50,0,0
197968,-58.8235294117647,2,2,1,50,0,0
198368,-58.8235294117647,2,2,1,50,0,0
199568,-58.8235294117647,2,2,2,50,0,0
199768,-58.8235294117647,2,2,1,50,0,0
201968,-58.8235294117647,2,2,2,50,0,0
202168,-58.8235294117647,2,2,1,50,0,0
204368,-58.8235294117647,2,2,2,50,0,0
204568,-58.8235294117647,2,2,1,50,0,0
206768,-58.8235294117647,2,2,1,60,0,0
207168,-58.8235294117647,2,2,2,60,0,0
207968,-58.8235294117647,2,2,1,70,0,1
216968,-58.8235294117647,2,2,2,70,0,1
217168,-58.8235294117647,2,2,1,70,0,1
217368,-58.8235294117647,2,2,2,70,0,1
217568,-58.8235294117647,2,2,1,70,0,1
225068,-58.8235294117647,2,2,1,5,0,1
225168,-58.8235294117647,2,2,1,70,0,1
225468,-58.8235294117647,2,2,1,5,0,1
225568,-58.8235294117647,2,2,1,70,0,1
225868,-58.8235294117647,2,2,1,5,0,1
225968,-58.8235294117647,2,2,1,70,0,1
227168,-58.8235294117647,2,2,1,30,0,0
234368,-58.8235294117647,2,2,1,40,0,0
236768,-58.8235294117647,2,2,1,70,0,1
255968,-58.8235294117647,2,2,1,70,0,1
261168,-58.8235294117647,2,2,1,70,0,1
263068,-58.8235294117647,2,2,1,70,0,0
263168,-58.8235294117647,2,2,1,60,0,1
263243,-58.8235294117647,2,2,1,60,0,0
264368,-58.8235294117647,2,2,1,60,0,1
264443,-58.8235294117647,2,2,1,60,0,0
265568,-444.444444444444,2,2,1,50,0,1
265643,-444.444444444444,2,2,1,50,0,0
266768,-444.444444444444,2,2,1,40,0,0
267968,-444.444444444444,2,2,1,30,0,0
269168,-444.444444444444,2,2,1,20,0,0
270368,-444.444444444444,2,2,1,10,0,0
271168,-444.444444444444,2,2,1,9,0,0
271568,-444.444444444444,2,2,1,8,0,0
271968,-444.444444444444,2,2,1,7,0,0
272368,-444.444444444444,2,2,1,6,0,0
272768,-444.444444444444,2,2,1,5,0,0
275168,-444.444444444444,2,2,1,5,0,0
[Colours]
Combo1 : 255,128,128
Combo2 : 72,72,255
Combo3 : 192,192,192
Combo4 : 255,136,79
[HitObjects]
486,179,265568,6,0,P|461:174|454:174,1,26.999997997284,6|0,1:2|0:0,0:0:0:0:

View File

@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
{ {
Precision = 0.01,
MinValue = 0.1, MinValue = 0.1,
MaxValue = 10 MaxValue = 10
}; };

View File

@ -140,10 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting) private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
{ {
if (bodySprite is TextureAnimation bodyAnimation) if (bodySprite is TextureAnimation bodyAnimation)
{
bodyAnimation.GotoFrame(0);
bodyAnimation.IsPlaying = isHitting.NewValue; bodyAnimation.IsPlaying = isHitting.NewValue;
}
if (lightContainer == null) if (lightContainer == null)
return; return;
@ -219,6 +216,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
base.Update(); base.Update();
if (!isHitting.Value)
(bodySprite as TextureAnimation)?.GotoFrame(0);
if (holdNote.Body.HasHoldBreak) if (holdNote.Body.HasHoldBreak)
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute; missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;

View File

@ -3,9 +3,12 @@
#nullable disable #nullable disable
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -71,4 +74,120 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void moveMouse(Vector2 pos) => private void moveMouse(Vector2 pos) =>
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
} }
[TestFixture]
public class TestSliderNearLinearScaling
{
private readonly Random rng = new Random(1337);
[Test]
public void TestScalingSliderFlat()
{
SliderPath sliderPathPerfect = new SliderPath(
[
new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(50, 25)),
new PathControlPoint(new Vector2(25, 100)),
]);
SliderPath sliderPathBezier = new SliderPath(
[
new PathControlPoint(new Vector2(0), PathType.BEZIER),
new PathControlPoint(new Vector2(50, 25)),
new PathControlPoint(new Vector2(25, 100)),
]);
scaleSlider(sliderPathPerfect, new Vector2(0.000001f, 1));
scaleSlider(sliderPathBezier, new Vector2(0.000001f, 1));
for (int i = 0; i < 100; i++)
{
Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
}
}
[Test]
public void TestPerfectCurveMatchesTheoretical()
{
for (int i = 0; i < 20000; i++)
{
//Only test points that are in the screen's bounds
float p1X = 640.0f * (float)rng.NextDouble();
float p2X = 640.0f * (float)rng.NextDouble();
float p1Y = 480.0f * (float)rng.NextDouble();
float p2Y = 480.0f * (float)rng.NextDouble();
SliderPath sliderPathPerfect = new SliderPath(
[
new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(p1X, p1Y)),
new PathControlPoint(new Vector2(p2X, p2Y)),
]);
assertMatchesPerfectCircle(sliderPathPerfect);
scaleSlider(sliderPathPerfect, new Vector2(0.00001f, 1));
assertMatchesPerfectCircle(sliderPathPerfect);
}
}
private void assertMatchesPerfectCircle(SliderPath path)
{
if (path.ControlPoints.Count != 3)
return;
//Replication of PathApproximator.CircularArcToPiecewiseLinear
CircularArcProperties circularArcProperties = new CircularArcProperties(path.ControlPoints.Select(x => x.Position).ToArray());
if (!circularArcProperties.IsValid)
return;
//Addresses cases where circularArcProperties.ThetaRange>0.5
//Occurs in code in PathControlPointVisualiser.ensureValidPathType
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(path.ControlPoints.Select(x => x.Position).ToArray());
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
return;
int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius)))));
//ignore cases where subpoints is int.MaxValue, result will be garbage
//as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage
if (subpoints == int.MaxValue)
return;
for (int i = 0; i < Math.Min(subpoints, 100); i++)
{
float progress = (float)rng.NextDouble();
//To avoid errors from interpolating points, ensure we check only positions that would be subpoints.
progress = (float)Math.Ceiling(progress * (subpoints - 1)) / (subpoints - 1);
//Special case - if few subpoints, ensure checking every single one rather than randomly
if (subpoints < 100)
progress = i / (float)(subpoints - 1);
//edge points cause issue with interpolation, so ignore the last two points and first
if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1))
continue;
double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange);
Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius;
Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
"A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints"
+ ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress)
+ " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress))
);
}
}
private void scaleSlider(SliderPath path, Vector2 scale)
{
for (int i = 0; i < path.ControlPoints.Count; i++)
{
path.ControlPoints[i].Position *= scale;
}
}
}
} }

View File

@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private partial class TestDrawableOsuJudgement : DrawableOsuJudgement private partial class TestDrawableOsuJudgement : DrawableOsuJudgement
{ {
public new SkinnableSprite Lighting => base.Lighting; public new SkinnableSprite Lighting => base.Lighting;
public new SkinnableDrawable JudgementBody => base.JudgementBody; public new SkinnableDrawable? JudgementBody => base.JudgementBody;
} }
} }
} }

View File

@ -104,7 +104,11 @@ namespace osu.Game.Rulesets.Osu.Edit
RightToolbox.AddRange(new EditorToolboxGroup[] RightToolbox.AddRange(new EditorToolboxGroup[]
{ {
OsuGridToolboxGroup, OsuGridToolboxGroup,
new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
},
FreehandlSliderToolboxGroup FreehandlSliderToolboxGroup
} }
); );

View File

@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private void updateState() private void updateState()
{ {
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; CanRotateAroundSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any(); CanRotateAroundPlayfieldOrigin.Value = selectedMovableObjects.Any();
} }
private OsuHitObject[]? objectsInRotation; private OsuHitObject[]? objectsInRotation;

View File

@ -24,6 +24,16 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuSelectionScaleHandler : SelectionScaleHandler public partial class OsuSelectionScaleHandler : SelectionScaleHandler
{ {
/// <summary>
/// Whether scaling anchored by the center of the playfield can currently be performed.
/// </summary>
public Bindable<bool> CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool();
/// <summary>
/// Whether a single slider is currently selected, which results in a different scaling behaviour.
/// </summary>
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
[Resolved] [Resolved]
private IEditorChangeHandler? changeHandler { get; set; } private IEditorChangeHandler? changeHandler { get; set; }
@ -53,6 +63,8 @@ namespace osu.Game.Rulesets.Osu.Edit
CanScaleX.Value = quad.Width > 0; CanScaleX.Value = quad.Width > 0;
CanScaleY.Value = quad.Height > 0; CanScaleY.Value = quad.Height > 0;
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any();
IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider;
} }
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale; private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
@ -67,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Edit
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)) ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
defaultOrigin = OriginalSurroundingQuad.Value.Centre; defaultOrigin = OriginalSurroundingQuad.Value.Centre;
} }
@ -92,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
else else
{ {
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale); scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
foreach (var (ho, originalState) in objectsInScale) foreach (var (ho, originalState) in objectsInScale)
{ {
@ -158,27 +170,36 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <summary> /// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary> /// </summary>
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
/// <param name="origin">The origin from which the scale operation is performed</param> /// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param> /// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns> /// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale) public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
{ {
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
if (objectsInScale == null)
return scale;
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin); Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0)) if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); origin = slider.Position;
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); Vector2 actualOrigin = origin ?? defaultOrigin.Value;
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0)) var selectionQuad = OriginalSurroundingQuad.Value;
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0)) var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
} }

View File

@ -78,11 +78,15 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => angleInput.TakeFocus()); ScheduleAfterChildren(() =>
{
angleInput.TakeFocus();
angleInput.SelectAll();
});
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select(); rotationOrigin.Items.First().Select();
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e => rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
{ {
selectionCentreButton.Selected.Disabled = !e.NewValue; selectionCentreButton.Selected.Disabled = !e.NewValue;
}, true); }, true);

View File

@ -0,0 +1,212 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PreciseScalePopover : OsuPopover
{
private readonly OsuSelectionScaleHandler scaleHandler;
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
private SliderWithTextBoxInput<float> scaleInput = null!;
private BindableNumber<float> scaleInputBindable = null!;
private EditorRadioButtonCollection scaleOrigin = null!;
private RadioButton playfieldCentreButton = null!;
private RadioButton selectionCentreButton = null!;
private OsuCheckbox xCheckBox = null!;
private OsuCheckbox yCheckBox = null!;
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
{
this.scaleHandler = scaleHandler;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
scaleInput = new SliderWithTextBoxInput<float>("Scale:")
{
Current = scaleInputBindable = new BindableNumber<float>
{
MinValue = 0.5f,
MaxValue = 2,
Precision = 0.001f,
Value = 1,
Default = 1,
},
Instantaneous = true
},
scaleOrigin = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
playfieldCentreButton = new RadioButton("Playfield centre",
() => setOrigin(ScaleOrigin.PlayfieldCentre),
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
selectionCentreButton = new RadioButton("Selection centre",
() => setOrigin(ScaleOrigin.SelectionCentre),
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(4),
Children = new Drawable[]
{
xCheckBox = new OsuCheckbox(false)
{
RelativeSizeAxes = Axes.X,
LabelText = "X-axis",
Current = { Value = true },
},
yCheckBox = new OsuCheckbox(false)
{
RelativeSizeAxes = Axes.X,
LabelText = "Y-axis",
Current = { Value = true },
},
}
},
}
};
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
{
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
};
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
{
selectionCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to its centre." : string.Empty;
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() =>
{
scaleInput.TakeFocus();
scaleInput.SelectAll();
});
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
scaleInfo.BindValueChanged(scale =>
{
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
});
}
private void updateAxisCheckBoxesEnabled()
{
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
{
toggleAxisAvailable(xCheckBox.Current, true);
toggleAxisAvailable(yCheckBox.Current, true);
}
else
{
toggleAxisAvailable(xCheckBox.Current, scaleHandler.CanScaleX.Value);
toggleAxisAvailable(yCheckBox.Current, scaleHandler.CanScaleY.Value);
}
}
private void toggleAxisAvailable(Bindable<bool> axisBindable, bool available)
{
// enable the bindable to allow setting the value
axisBindable.Disabled = false;
// restore the presumed default value given the axis's new availability state
axisBindable.Value = available;
axisBindable.Disabled = !available;
}
private void updateMaxScale()
{
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
return;
const float max_scale = 10;
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
if (!scaleInfo.Value.XAxis)
scale.X = max_scale;
if (!scaleInfo.Value.YAxis)
scale.Y = max_scale;
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
}
private void setOrigin(ScaleOrigin origin)
{
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
updateMaxScale();
updateAxisCheckBoxesEnabled();
}
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
private void setAxis(bool x, bool y)
{
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
updateMaxScale();
}
protected override void PopIn()
{
base.PopIn();
scaleHandler.Begin();
updateMaxScale();
}
protected override void PopOut()
{
base.PopOut();
if (IsLoaded) scaleHandler.Commit();
}
}
public enum ScaleOrigin
{
PlayfieldCentre,
SelectionCentre
}
public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis);
}

View File

@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction> public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
{ {
private readonly Bindable<bool> canRotate = new BindableBool(); private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
private EditorToolButton rotateButton = null!; private EditorToolButton rotateButton = null!;
private EditorToolButton scaleButton = null!;
private Bindable<bool> canRotatePlayfieldOrigin = null!;
private Bindable<bool> canRotateSelectionOrigin = null!;
public SelectionRotationHandler RotationHandler { get; init; } = null!; public SelectionRotationHandler RotationHandler { get; init; } = null!;
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
public TransformToolboxGroup() public TransformToolboxGroup()
: base("transform") : base("transform")
@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit
rotateButton = new EditorToolButton("Rotate", rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler)), () => new PreciseRotationPopover(RotationHandler)),
// TODO: scale scaleButton = new EditorToolButton("Scale",
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
() => new PreciseScalePopover(ScaleHandler))
} }
}; };
} }
@ -54,21 +56,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
base.LoadComplete(); base.LoadComplete();
// aggregate two values into canRotate canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy(); canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy(); canScale.AddSource(ScaleHandler.CanScaleX);
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); canScale.AddSource(ScaleHandler.CanScaleY);
canScale.AddSource(ScaleHandler.CanScaleFromPlayfieldOrigin);
void updateCanRotateAggregate()
{
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
}
// bindings to `Enabled` on the buttons are decoupled on purpose // bindings to `Enabled` on the buttons are decoupled on purpose
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
} }
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@ -82,6 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit
rotateButton.TriggerClick(); rotateButton.TriggerClick();
return true; return true;
} }
case GlobalAction.EditorToggleScaleControl:
{
scaleButton.TriggerClick();
return true;
}
} }
return false; return false;

View File

@ -67,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.Mods
// Generate the replay frames the cursor should follow // Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList(); replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
drawableRuleset.UseResumeOverlay = false;
} }
} }
} }

View File

@ -47,9 +47,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
// Hide judgment displays and follow points as they won't make any sense. // Hide follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
} }

View File

@ -39,9 +39,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
// Hide judgment displays and follow points as they won't make any sense. // Hide follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
} }

View File

@ -38,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
// Hide judgment displays and follow points as they won't make any sense. // Hide follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -14,10 +12,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public partial class DrawableOsuJudgement : DrawableJudgement public partial class DrawableOsuJudgement : DrawableJudgement
{ {
internal SkinnableLighting Lighting { get; private set; } internal SkinnableLighting Lighting { get; private set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; } = null!;
private bool positionTransferred;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -39,10 +39,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Lighting.ResetAnimation(); Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result); Lighting.SetColourFrom(JudgedObject, Result);
if (JudgedObject?.HitObject is OsuHitObject osuObject) positionTransferred = false;
}
protected override void Update()
{ {
Position = osuObject.StackedEndPosition; base.Update();
Scale = new Vector2(osuObject.Scale);
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
Scale = new Vector2(osuObject.HitObject.Scale);
positionTransferred = true;
} }
} }

View File

@ -10,9 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -63,22 +61,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplyTransformsAt(time, false); base.ApplyTransformsAt(time, false);
} }
private Vector2? lastPosition;
public void UpdateProgress(double completionProgress) public void UpdateProgress(double completionProgress)
{ {
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); Slider slider = drawableSlider.HitObject;
Position = slider.CurvePositionAt(completionProgress);
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); //0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true;
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
// Needed for when near completion, or in case of a very short slider.
if (diff.LengthFast < 0.01f) if (diff.LengthFast < 0.01f)
return; return;
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0); ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = Position;
} }
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -45,7 +46,13 @@ namespace osu.Game.Rulesets.Osu.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true }; public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); protected override ResumeOverlay CreateResumeOverlay()
{
if (Mods.Any(m => m is OsuModAutopilot))
return new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
return new OsuResumeOverlay();
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Alpha = 0, Alpha = 0,
Scale = new Vector2(0.7f), Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
Colour = new Colour4(255, 228, 0, 255), Colour = new Colour4(255, 228, 0, 255),
}; };
@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (!result.IsHit || !isKiaiActive) if (!result.IsHit || !isKiaiActive)
return; return;
sprite.ScaleTo(0.85f).Then() sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
.ScaleTo(0.7f, 80, Easing.OutQuad); .ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
} }
} }
} }

View File

@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
public partial class TaikoLegacyHitTarget : CompositeDrawable public partial class TaikoLegacyHitTarget : CompositeDrawable
{ {
/// <summary>
/// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592)
/// but for whatever reason this doesn't match visually.
/// </summary>
public const float SCALE = 0.8f;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin)
{ {
@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("approachcircle"), Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.83f), Scale = new Vector2(SCALE + 0.03f),
Alpha = 0.47f, // eyeballed to match stable Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("taikobigcircle"), Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.8f), Scale = new Vector2(SCALE),
Alpha = 0.22f, // eyeballed to match stable Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -33,6 +33,7 @@ using osu.Game.Scoring;
using osu.Game.Scoring.Legacy; using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users; using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Beatmaps.Formats namespace osu.Game.Tests.Beatmaps.Formats
{ {
@ -126,17 +127,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied) public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
{ {
const double first_frame_time = 48; const double first_frame_time = 31;
const double second_frame_time = 65; const double second_frame_time = 48;
const double third_frame_time = 65;
var decoder = new TestLegacyScoreDecoder(beatmapVersion); var decoder = new TestLegacyScoreDecoder(beatmapVersion);
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{ {
var score = decoder.Parse(resourceStream); var score = decoder.Parse(resourceStream);
int offset = offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + offset));
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + offset));
Assert.That(score.Replay.Frames[2].Time, Is.EqualTo(third_frame_time + offset));
} }
} }
@ -177,6 +181,94 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
} }
[Test]
public void TestNegativeFrameSkipped()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2()),
new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE),
new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2),
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE),
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0));
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(1000));
Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(2000));
}
[Test]
public void FirstTwoFramesSwappedIfInWrongOrder()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(100, new Vector2()),
new OsuReplayFrame(50, OsuPlayfield.BASE_SIZE / 2),
new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE),
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0));
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(100));
Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(1000));
}
[Test]
public void FirstTwoFramesPulledTowardThirdIfTheyAreAfterIt()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2()),
new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2),
new OsuReplayFrame(-1500, OsuPlayfield.BASE_SIZE),
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3));
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(-1500));
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(-1500));
Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(-1500));
}
[Test] [Test]
public void TestCultureInvariance() public void TestCultureInvariance()
{ {

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Chat
return true; return true;
case ChatAckRequest ack: case ChatAckRequest ack:
ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() }); ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToArray() });
silencedUserIds.Clear(); silencedUserIds.Clear();
return true; return true;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value); var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online. beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online.
Child = thumbnail = new BeatmapCardThumbnail(beatmapSet) Child = thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
this.getTargetContainer = getTargetContainer; this.getTargetContainer = getTargetContainer;
CanRotateSelectionOrigin.Value = true; CanRotateAroundSelectionOrigin.Value = true;
} }
[CanBeNull] [CanBeNull]

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK.Input; using osuTK.Input;
@ -15,8 +16,14 @@ namespace osu.Game.Tests.Visual.Menus
{ {
private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType<OnlineMenuBanner>().Single(); private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType<OnlineMenuBanner>().Single();
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false);
}
[Test] [Test]
public void TestOnlineMenuBanner() public void TestOnlineMenuBannerTrusted()
{ {
AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent
{ {
@ -25,13 +32,51 @@ namespace osu.Game.Tests.Visual.Menus
new APIMenuImage new APIMenuImage
{ {
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023",
} }
} }
}); });
AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("enter menu", () => InputManager.Key(Key.Enter)); AddStep("enter menu", () => InputManager.Key(Key.Enter));
AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>().FirstOrDefault()?.IsLoaded, () => Is.True);
AddStep("click banner", () =>
{
InputManager.MoveMouseTo(onlineMenuBanner);
InputManager.Click(MouseButton.Left);
});
// Might not catch every occurrence due to async nature, but works in manual testing and saves annoying test setup.
AddAssert("no dialog", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog == null);
}
[Test]
public void TestOnlineMenuBannerUntrustedDomain()
{
AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent
{
Images = new[]
{
new APIMenuImage
{
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
Url = @"https://google.com",
}
}
});
AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("enter menu", () => InputManager.Key(Key.Enter));
AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>().FirstOrDefault()?.IsLoaded, () => Is.True);
AddStep("click banner", () =>
{
InputManager.MoveMouseTo(onlineMenuBanner);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for dialog", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog != null);
} }
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
@ -317,13 +318,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
p.RequestResults = _ => resultsRequested = true; p.RequestResults = _ => resultsRequested = true;
}); });
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddStep("move mouse to first item title", () => AddStep("move mouse to first item title", () =>
{ {
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad; var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
InputManager.MoveMouseTo(location); InputManager.MoveMouseTo(location);
}); });
AddUntilStep("wait for text load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any());
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False); AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True); AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);

View File

@ -87,6 +87,105 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("delete all beatmaps", () => manager.Delete()); AddStep("delete all beatmaps", () => manager.Delete());
} }
[Test]
public void TestSpeedChange()
{
createSongSelect();
changeMods();
decreaseModSpeed();
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
decreaseModSpeed();
AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
increaseModSpeed();
AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
increaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
increaseModSpeed();
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
increaseModSpeed();
AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
decreaseModSpeed();
AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
OsuModNightcore nc = new OsuModNightcore
{
SpeedChange = { Value = 1.05 }
};
changeMods(nc);
increaseModSpeed();
AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
decreaseModSpeed();
AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
decreaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
decreaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
decreaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
increaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
OsuModDoubleTime dt = new OsuModDoubleTime
{
SpeedChange = { Value = 1.02 },
AdjustPitch = { Value = true },
};
changeMods(dt);
decreaseModSpeed();
AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005));
AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
OsuModHalfTime ht = new OsuModHalfTime
{
SpeedChange = { Value = 0.97 },
AdjustPitch = { Value = true },
};
Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() };
changeMods(modlist);
increaseModSpeed();
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005));
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.True);
AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType<ModHidden>().SingleOrDefault(), () => Is.Not.Null);
AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType<ModHardRock>().SingleOrDefault(), () => Is.Not.Null);
changeMods(new ModWindUp());
increaseModSpeed();
AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp);
changeMods(new ModAdaptiveSpeed());
increaseModSpeed();
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
void increaseModSpeed() => AddStep("increase mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Up);
InputManager.ReleaseKey(Key.ControlLeft);
});
void decreaseModSpeed() => AddStep("decrease mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Down);
InputManager.ReleaseKey(Key.ControlLeft);
});
}
[Test] [Test]
public void TestPlaceholderBeatmapPresence() public void TestPlaceholderBeatmapPresence()
{ {

View File

@ -7,11 +7,13 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -35,7 +37,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (bigButton) if (bigButton)
{ {
Child = button = new ShearedButton(400) Child = button = new ShearedButton(400, 80)
{ {
LighterColour = Colour4.FromHex("#FFFFFF"), LighterColour = Colour4.FromHex("#FFFFFF"),
DarkerColour = Colour4.FromHex("#FFCC22"), DarkerColour = Colour4.FromHex("#FFCC22"),
@ -44,13 +46,12 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "Let's GO!", Text = "Let's GO!",
Height = 80,
Action = () => actionFired = true, Action = () => actionFired = true,
}; };
} }
else else
{ {
Child = button = new ShearedButton(200) Child = button = new ShearedButton(200, 80)
{ {
LighterColour = Colour4.FromHex("#FF86DD"), LighterColour = Colour4.FromHex("#FF86DD"),
DarkerColour = Colour4.FromHex("#DE31AE"), DarkerColour = Colour4.FromHex("#DE31AE"),
@ -58,7 +59,6 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "Press me", Text = "Press me",
Height = 80,
Action = () => actionFired = true, Action = () => actionFired = true,
}; };
} }
@ -171,5 +171,48 @@ namespace osu.Game.Tests.Visual.UserInterface
void setToggleDisabledState(bool disabled) => AddStep($"{(disabled ? "disable" : "enable")} toggle", () => button.Active.Disabled = disabled); void setToggleDisabledState(bool disabled) => AddStep($"{(disabled ? "disable" : "enable")} toggle", () => button.Active.Disabled = disabled);
} }
[Test]
public void TestButtons()
{
AddStep("create buttons", () => Children = new[]
{
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Scale = new Vector2(2.5f),
Children = new Drawable[]
{
new ShearedButton(120)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = "Test",
Action = () => { },
Padding = new MarginPadding(),
},
new ShearedButton(120, 40)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = "Test",
Action = () => { },
Padding = new MarginPadding { Left = -1f },
},
new ShearedButton(120, 70)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = "Test",
Action = () => { },
Padding = new MarginPadding { Left = 3f },
},
}
}
});
}
} }
} }

View File

@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
thumbnail = new BeatmapCardThumbnail(BeatmapSet) thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet)
{ {
Name = @"Left (icon) area", Name = @"Left (icon) area",
Size = new Vector2(height), Size = new Vector2(height),

View File

@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
thumbnail = new BeatmapCardThumbnail(BeatmapSet) thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet)
{ {
Name = @"Left (icon) area", Name = @"Left (icon) area",
Size = new Vector2(height), Size = new Vector2(height),

View File

@ -8,7 +8,6 @@ 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.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osuTK; using osuTK;
@ -36,14 +35,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
OnlineInfo = beatmapSetInfo OnlineInfo = onlineInfo
}, },
background = new Box background = new Box
{ {
@ -62,7 +61,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(50),
InnerRadius = 0.2f InnerRadius = 0.2f
}, },
content = new Container content = new Container
@ -93,6 +91,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
base.Update(); base.Update();
progress.Progress = playButton.Progress.Value; progress.Progress = playButton.Progress.Value;
playButton.Scale = new Vector2(DrawWidth / 100);
progress.Size = new Vector2(50 * DrawWidth / 100);
} }
private void updateState() private void updateState()

View File

@ -16,7 +16,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Database namespace osu.Game.Database
{ {
@ -248,8 +247,7 @@ namespace osu.Game.Database
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = computeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor);
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap);
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
} }
/// <summary> /// <summary>
@ -273,7 +271,7 @@ namespace osu.Game.Database
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = computeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor);
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes);
} }
/// <summary> /// <summary>
@ -283,17 +281,13 @@ namespace osu.Game.Database
/// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param> /// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
/// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param> /// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
if (ruleset is not ILegacyRuleset legacyRuleset) if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
var mods = score.Mods;
if (mods.Any(mod => mod is ModScoreV2))
return score.TotalScore;
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
@ -302,8 +296,13 @@ namespace osu.Game.Database
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
var legacyBeatmapConversionDifficultyInfo = LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap);
return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); var mods = score.Mods;
if (mods.Any(mod => mod is ModScoreV2))
return ((long)Math.Round(score.TotalScore / sv1Simulator.GetLegacyScoreMultiplier(mods, legacyBeatmapConversionDifficultyInfo)), score.TotalScore);
return convertFromLegacyTotalScore(score, ruleset, legacyBeatmapConversionDifficultyInfo, attributes);
} }
/// <summary> /// <summary>
@ -314,15 +313,15 @@ namespace osu.Game.Database
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param> /// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
Debug.Assert(score.LegacyTotalScore != null); Debug.Assert(score.LegacyTotalScore != null);
if (ruleset is not ILegacyRuleset legacyRuleset) if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty);
int maximumLegacyAccuracyScore = attributes.AccuracyScore; int maximumLegacyAccuracyScore = attributes.AccuracyScore;
@ -354,17 +353,18 @@ namespace osu.Game.Database
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
long convertedTotalScore; long convertedTotalScoreWithoutMods;
switch (score.Ruleset.OnlineID) switch (score.Ruleset.OnlineID)
{ {
case 0: case 0:
if (score.MaxCombo == 0 || score.Accuracy == 0) if (score.MaxCombo == 0 || score.Accuracy == 0)
{ {
return (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
0 0
+ 500000 * Math.Pow(score.Accuracy, 5) + 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier); + bonusProportion);
break;
} }
// see similar check above. // see similar check above.
@ -372,10 +372,11 @@ namespace osu.Game.Database
// are either pointless or wildly wrong. // are either pointless or wildly wrong.
if (maximumLegacyComboScore + maximumLegacyBonusScore == 0) if (maximumLegacyComboScore + maximumLegacyBonusScore == 0)
{ {
return (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise 500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise
+ 500000 * Math.Pow(score.Accuracy, 5) + 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier); + bonusProportion);
break;
} }
// Assumptions: // Assumptions:
@ -472,17 +473,17 @@ namespace osu.Game.Database
double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore; double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore;
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
500000 * newComboScoreProportion * score.Accuracy 500000 * newComboScoreProportion * score.Accuracy
+ 500000 * Math.Pow(score.Accuracy, 5) + 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
case 1: case 1:
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
250000 * comboProportion 250000 * comboProportion
+ 750000 * Math.Pow(score.Accuracy, 3.6) + 750000 * Math.Pow(score.Accuracy, 3.6)
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
case 2: case 2:
@ -507,28 +508,28 @@ namespace osu.Game.Database
? 0 ? 0
: (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit); : (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit);
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss)) comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss))
+ dropletsPortion * dropletsHit + dropletsPortion * dropletsHit
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
case 3: case 3:
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
850000 * comboProportion 850000 * comboProportion
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
default: default:
convertedTotalScore = score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
break;
} }
if (convertedTotalScore < 0) if (convertedTotalScoreWithoutMods < 0)
throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}"); throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}");
return convertedTotalScore; long convertedTotalScore = (long)Math.Round(convertedTotalScoreWithoutMods * modMultiplier);
return (convertedTotalScoreWithoutMods, convertedTotalScore);
} }
/// <summary> /// <summary>

View File

@ -26,11 +26,14 @@ namespace osu.Game.Graphics.UserInterface
// Right mouse button is a special case where we allow actioning without dismissing the menu. // Right mouse button is a special case where we allow actioning without dismissing the menu.
// This is achieved by not calling `Clicked` (as done by the base implementation in OnClick). // This is achieved by not calling `Clicked` (as done by the base implementation in OnClick).
if (IsActionable && e.Button == MouseButton.Right) if (IsActionable && e.Button == MouseButton.Right)
{
Item.Action.Value?.Invoke(); Item.Action.Value?.Invoke();
return true; return true;
} }
return false;
}
private partial class ToggleTextContainer : TextContainer private partial class ToggleTextContainer : TextContainer
{ {
private readonly StatefulMenuItem menuItem; private readonly StatefulMenuItem menuItem;

View File

@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
public partial class ShearedButton : OsuClickableContainer public partial class ShearedButton : OsuClickableContainer
{ {
public const float HEIGHT = 50; public const float DEFAULT_HEIGHT = 50;
public const float CORNER_RADIUS = 7; public const float CORNER_RADIUS = 7;
public const float BORDER_THICKNESS = 2; public const float BORDER_THICKNESS = 2;
@ -66,7 +66,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box background; private readonly Box background;
private readonly OsuSpriteText text; private readonly OsuSpriteText text;
private const float shear = 0.2f; private const float shear = OsuGame.SHEAR;
private Colour4? darkerColour; private Colour4? darkerColour;
private Colour4? lighterColour; private Colour4? lighterColour;
@ -75,6 +75,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly Container backgroundLayer; private readonly Container backgroundLayer;
private readonly Box flashLayer; private readonly Box flashLayer;
protected readonly Container ButtonContent;
/// <summary> /// <summary>
/// Creates a new <see cref="ShearedToggleButton"/> /// Creates a new <see cref="ShearedToggleButton"/>
/// </summary> /// </summary>
@ -85,10 +87,11 @@ namespace osu.Game.Graphics.UserInterface
/// <item>If a <see langword="null"/> value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text.</item> /// <item>If a <see langword="null"/> value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text.</item>
/// </list> /// </list>
/// </param> /// </param>
public ShearedButton(float? width = null) /// <param name="height">The height of the button.</param>
public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT)
{ {
Height = HEIGHT; Height = height;
Padding = new MarginPadding { Horizontal = shear * 50 }; Padding = new MarginPadding { Horizontal = shear * height };
Content.CornerRadius = CORNER_RADIUS; Content.CornerRadius = CORNER_RADIUS;
Content.Shear = new Vector2(shear, 0); Content.Shear = new Vector2(shear, 0);
@ -109,12 +112,16 @@ namespace osu.Game.Graphics.UserInterface
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
text = new OsuSpriteText ButtonContent = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Shear = new Vector2(-shear, 0),
Child = text = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 17), Font = OsuFont.TorusAlternate.With(size: 17),
Shear = new Vector2(-shear, 0) }
}, },
} }
}, },
@ -188,7 +195,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
var colourDark = darkerColour ?? ColourProvider.Background3; var colourDark = darkerColour ?? ColourProvider.Background3;
var colourLight = lighterColour ?? ColourProvider.Background1; var colourLight = lighterColour ?? ColourProvider.Background1;
var colourText = textColour ?? ColourProvider.Content1; var colourContent = textColour ?? ColourProvider.Content1;
if (!Enabled.Value) if (!Enabled.Value)
{ {
@ -205,9 +212,9 @@ namespace osu.Game.Graphics.UserInterface
backgroundLayer.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(colourDark, colourLight), 150, Easing.OutQuint); backgroundLayer.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(colourDark, colourLight), 150, Easing.OutQuint);
if (!Enabled.Value) if (!Enabled.Value)
colourText = colourText.Opacity(0.6f); colourContent = colourContent.Opacity(0.6f);
text.FadeColour(colourText, 150, Easing.OutQuint); ButtonContent.FadeColour(colourContent, 150, Easing.OutQuint);
} }
} }
} }

View File

@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
@ -53,7 +52,7 @@ namespace osu.Game.Graphics.UserInterface
public ShearedSearchTextBox() public ShearedSearchTextBox()
{ {
Height = 42; Height = 42;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); Shear = new Vector2(OsuGame.SHEAR, 0);
Masking = true; Masking = true;
CornerRadius = corner_radius; CornerRadius = corner_radius;
@ -116,7 +115,7 @@ namespace osu.Game.Graphics.UserInterface
PlaceholderText = CommonStrings.InputSearch; PlaceholderText = CommonStrings.InputSearch;
CornerRadius = corner_radius; CornerRadius = corner_radius;
TextContainer.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0); TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0);
} }
protected override SpriteText CreatePlaceholder() => new SearchPlaceholder(); protected override SpriteText CreatePlaceholder() => new SearchPlaceholder();

View File

@ -50,6 +50,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
Component.BorderColour = colours.Blue; Component.BorderColour = colours.Blue;
} }
public bool SelectAll() => Component.SelectAll();
protected virtual OsuTextBox CreateTextBox() => new OsuTextBox(); protected virtual OsuTextBox CreateTextBox() => new OsuTextBox();
public override bool AcceptsFocus => true; public override bool AcceptsFocus => true;

View File

@ -87,6 +87,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox); public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
public bool SelectAll() => textBox.SelectAll();
private bool updatingFromTextBox; private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change) private void textChanged(ValueChangedEvent<string> change)

View File

@ -142,6 +142,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
}; };
private static IEnumerable<KeyBinding> inGameKeyBindings => new[] private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
@ -182,6 +183,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions),
new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods),
new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed),
}; };
private static IEnumerable<KeyBinding> audioControlKeyBindings => new[] private static IEnumerable<KeyBinding> audioControlKeyBindings => new[]
@ -409,6 +412,9 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
EditorToggleRotateControl, EditorToggleRotateControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
EditorToggleScaleControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))]
IncreaseOffset, IncreaseOffset,
@ -420,6 +426,12 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))]
StepReplayBackward, StepReplayBackward,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseModSpeed))]
IncreaseModSpeed,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))]
DecreaseModSpeed,
} }
public enum GlobalActionCategory public enum GlobalActionCategory

View File

@ -5,14 +5,14 @@ using osu.Framework.Localisation;
namespace osu.Game.Localisation namespace osu.Game.Localisation
{ {
public static class DeleteConfirmationDialogStrings public static class DialogStrings
{ {
private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog"; private const string prefix = @"osu.Game.Resources.Localisation.Dialog";
/// <summary> /// <summary>
/// "Caution" /// "Caution"
/// </summary> /// </summary>
public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Caution"); public static LocalisableString Caution => new TranslatableString(getKey(@"header_text"), @"Caution");
/// <summary> /// <summary>
/// "Yes. Go for it." /// "Yes. Go for it."

View File

@ -369,6 +369,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
/// <summary>
/// "Toggle scale control"
/// </summary>
public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control");
/// <summary>
/// "Increase mod speed"
/// </summary>
public static LocalisableString IncreaseModSpeed => new TranslatableString(getKey(@"increase_mod_speed"), @"Increase mod speed");
/// <summary>
/// "Decrease mod speed"
/// </summary>
public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -49,6 +49,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied");
/// <summary>
/// "Speed changed to {0:N2}x"
/// </summary>
public static LocalisableString SpeedChangedTo(double speed) => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to {0:N2}x", speed);
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -42,7 +42,11 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; } public string WebsiteRootUrl { get; }
public int APIVersion => 20220705; // We may want to pull this from the game version eventually. /// <summary>
/// The API response version.
/// See: https://osu.ppy.sh/docs/index.html#api-versions
/// </summary>
public int APIVersion { get; }
public Exception LastLoginError { get; private set; } public Exception LastLoginError { get; private set; }
@ -84,12 +88,23 @@ namespace osu.Game.Online.API
this.config = config; this.config = config;
this.versionHash = versionHash; this.versionHash = versionHash;
if (game.IsDeployedBuild)
APIVersion = game.AssemblyVersion.Major * 10000 + game.AssemblyVersion.Minor;
else
{
var now = DateTimeOffset.Now;
APIVersion = now.Year * 10000 + now.Month * 100 + now.Day;
}
APIEndpointUrl = endpointConfiguration.APIEndpointUrl; APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
NotificationsClient = setUpNotificationsClient(); NotificationsClient = setUpNotificationsClient();
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
log = Logger.GetLogger(LoggingTarget.Network); log = Logger.GetLogger(LoggingTarget.Network);
log.Add($@"API endpoint root: {APIEndpointUrl}");
log.Add($@"API request version: {APIVersion}");
ProvidedUsername = config.Get<string>(OsuSetting.Username); ProvidedUsername = config.Get<string>(OsuSetting.Username);

View File

@ -84,6 +84,13 @@ namespace osu.Game.Online.API
{ {
if (HandleRequest?.Invoke(request) != true) if (HandleRequest?.Invoke(request) != true)
{ {
// Noisy so let's silently allow these to succeed.
if (request is ChatAckRequest ack)
{
ack.TriggerSuccess(new ChatAckResponse());
return;
}
request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request.")); request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request."));
} }
}); });

View File

@ -61,7 +61,7 @@ namespace osu.Game.Online.API
string APIEndpointUrl { get; } string APIEndpointUrl { get; }
/// <summary> /// <summary>
/// The root URL of of the website, excluding the trailing slash. /// The root URL of the website, excluding the trailing slash.
/// </summary> /// </summary>
string WebsiteRootUrl { get; } string WebsiteRootUrl { get; }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.API.Requests.Responses
@ -10,6 +10,6 @@ namespace osu.Game.Online.API.Requests.Responses
public class ChatAckResponse public class ChatAckResponse
{ {
[JsonProperty("silences")] [JsonProperty("silences")]
public List<ChatSilence> Silences { get; set; } = null!; public ChatSilence[] Silences { get; set; } = Array.Empty<ChatSilence>();
} }
} }

View File

@ -8,8 +8,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {
@ -44,8 +46,8 @@ namespace osu.Game.Online.Chat
{ {
public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction)
{ {
HeaderText = "Just checking..."; HeaderText = DialogStrings.Caution;
BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}";
Icon = FontAwesome.Solid.ExclamationTriangle; Icon = FontAwesome.Solid.ExclamationTriangle;
@ -53,17 +55,17 @@ namespace osu.Game.Online.Chat
{ {
new PopupDialogOkButton new PopupDialogOkButton
{ {
Text = @"Yes. Go for it.", Text = @"Open in browser",
Action = openExternalLinkAction Action = openExternalLinkAction
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = @"Copy URL to the clipboard instead.", Text = @"Copy URL to the clipboard",
Action = copyExternalLinkAction Action = copyExternalLinkAction
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = @"No! Abort mission!" Text = CommonStrings.ButtonsCancel,
}, },
}; };
} }

View File

@ -91,6 +91,11 @@ namespace osu.Game
/// </summary> /// </summary>
protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f; protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f;
/// <summary>
/// A common shear factor applied to most components of the game.
/// </summary>
public const float SHEAR = 0.2f;
public Toolbar Toolbar { get; private set; } public Toolbar Toolbar { get; private set; }
private ChatOverlay chatOverlay; private ChatOverlay chatOverlay;
@ -480,10 +485,19 @@ namespace osu.Game
} }
}); });
public void OpenUrlExternally(string url, bool bypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ =>
{ {
bool isTrustedDomain;
if (url.StartsWith('/')) if (url.StartsWith('/'))
url = $"{API.APIEndpointUrl}{url}"; {
url = $"{API.WebsiteRootUrl}{url}";
isTrustedDomain = true;
}
else
{
isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal);
}
if (!url.CheckIsValidUrl()) if (!url.CheckIsValidUrl())
{ {
@ -495,7 +509,7 @@ namespace osu.Game
return; return;
} }
externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning); externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain);
}); });
/// <summary> /// <summary>

View File

@ -16,7 +16,6 @@ using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
@ -26,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
private readonly Statistic length, bpm, circleCount, sliderCount; private readonly Statistic length, bpm, circleCount, sliderCount;
private APIBeatmapSet beatmapSet; private IBeatmapSetInfo beatmapSet;
public APIBeatmapSet BeatmapSet public IBeatmapSetInfo BeatmapSet
{ {
get => beatmapSet; get => beatmapSet;
set set

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -28,9 +29,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
[CanBeNull] [CanBeNull]
public PreviewTrack Preview { get; private set; } public PreviewTrack Preview { get; private set; }
private APIBeatmapSet beatmapSet; private IBeatmapSetInfo beatmapSet;
public APIBeatmapSet BeatmapSet public IBeatmapSetInfo BeatmapSet
{ {
get => beatmapSet; get => beatmapSet;
set set

View File

@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Overlays.BeatmapSet.Buttons namespace osu.Game.Overlays.BeatmapSet.Buttons
@ -24,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
public IBindable<bool> Playing => playButton.Playing; public IBindable<bool> Playing => playButton.Playing;
public APIBeatmapSet BeatmapSet public IBeatmapSetInfo BeatmapSet
{ {
get => playButton.BeatmapSet; get => playButton.BeatmapSet;
set => playButton.BeatmapSet = value; set => playButton.BeatmapSet = value;
@ -32,8 +32,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
public PreviewButton() public PreviewButton()
{ {
Height = 42;
Children = new Drawable[] Children = new Drawable[]
{ {
background = new Box background = new Box

View File

@ -68,6 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet
preview = new PreviewButton preview = new PreviewButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 42,
}, },
new DetailBox new DetailBox
{ {

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Dialog
protected DangerousActionDialog() protected DangerousActionDialog()
{ {
HeaderText = DeleteConfirmationDialogStrings.HeaderText; HeaderText = DialogStrings.Caution;
Icon = FontAwesome.Regular.TrashAlt; Icon = FontAwesome.Regular.TrashAlt;
@ -38,12 +38,12 @@ namespace osu.Game.Overlays.Dialog
{ {
new PopupDialogDangerousButton new PopupDialogDangerousButton
{ {
Text = DeleteConfirmationDialogStrings.Confirm, Text = DialogStrings.Confirm,
Action = () => DangerousAction?.Invoke() Action = () => DangerousAction?.Invoke()
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = DeleteConfirmationDialogStrings.Cancel, Text = DialogStrings.Cancel,
Action = () => CancelAction?.Invoke() Action = () => CancelAction?.Invoke()
} }
}; };

View File

@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Mods
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
const float shear = ShearedOverlayContainer.SHEAR; const float shear = OsuGame.SHEAR;
LeftContent.AddRange(new Drawable[] LeftContent.AddRange(new Drawable[]
{ {

View File

@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f), Scale = new Vector2(0.8f),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) Shear = new Vector2(-OsuGame.SHEAR, 0)
}); });
ItemsFlow.Padding = new MarginPadding ItemsFlow.Padding = new MarginPadding
{ {

View File

@ -36,8 +36,8 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Height = ShearedButton.HEIGHT, Height = ShearedButton.DEFAULT_HEIGHT,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(OsuGame.SHEAR, 0),
CornerRadius = ShearedButton.CORNER_RADIUS, CornerRadius = ShearedButton.CORNER_RADIUS,
BorderThickness = ShearedButton.BORDER_THICKNESS, BorderThickness = ShearedButton.BORDER_THICKNESS,
Masking = true, Masking = true,

View File

@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Active = { BindTarget = Active }, Active = { BindTarget = Active },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
}; };
} }

View File

@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods
{ {
Width = WIDTH; Width = WIDTH;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); Shear = new Vector2(OsuGame.SHEAR, 0);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = header_height, Height = header_height,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
Velocity = 0.7f, Velocity = 0.7f,
ClampAxes = Axes.Y ClampAxes = Axes.Y
}, },
@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
Padding = new MarginPadding Padding = new MarginPadding
{ {
Horizontal = 17, Horizontal = 17,

View File

@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Height = 0 Height = 0
} },
}); });
MainAreaContent.AddRange(new Drawable[] MainAreaContent.AddRange(new Drawable[]
@ -227,7 +227,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Shear = new Vector2(SHEAR, 0), Shear = new Vector2(OsuGame.SHEAR, 0),
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 70 }, Margin = new MarginPadding { Horizontal = 70 },
@ -847,7 +847,7 @@ namespace osu.Game.Overlays.Mods
// DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear,
// so we have to manually compensate. // so we have to manually compensate.
var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent);
var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent); var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent);
bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound)
&& Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X);

View File

@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Mods
Content.CornerRadius = CORNER_RADIUS; Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2; Content.BorderThickness = 2;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); Shear = new Vector2(OsuGame.SHEAR, 0);
Children = new Drawable[] Children = new Drawable[]
{ {
@ -128,10 +128,10 @@ namespace osu.Game.Overlays.Mods
{ {
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
Margin = new MarginPadding Margin = new MarginPadding
{ {
Left = -18 * ShearedOverlayContainer.SHEAR Left = -18 * OsuGame.SHEAR
}, },
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
}, },
@ -139,7 +139,7 @@ namespace osu.Game.Overlays.Mods
{ {
Font = OsuFont.Default.With(size: 12), Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
} }
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(OsuGame.SHEAR, 0),
CornerRadius = ShearedButton.CORNER_RADIUS, CornerRadius = ShearedButton.CORNER_RADIUS,
Masking = true, Masking = true,
Children = new Drawable[] Children = new Drawable[]
@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Mods
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
} }
} }
@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = counter = new EffectCounter Child = counter = new EffectCounter
{ {
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-OsuGame.SHEAR, 0),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Current = { BindTarget = ModMultiplier } Current = { BindTarget = ModMultiplier }

View File

@ -22,8 +22,6 @@ namespace osu.Game.Overlays.Mods
{ {
protected const float PADDING = 14; protected const float PADDING = 14;
public const float SHEAR = 0.2f;
[Cached] [Cached]
protected readonly OverlayColourProvider ColourProvider; protected readonly OverlayColourProvider ColourProvider;

View File

@ -3,18 +3,30 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Utils; using osu.Game.Utils;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class UserModSelectOverlay : ModSelectOverlay public partial class UserModSelectOverlay : ModSelectOverlay
{ {
private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!;
public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme) : base(colourScheme)
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
Add(modSpeedHotkeyHandler = new ModSpeedHotkeyHandler());
}
protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false); protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false);
protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection)
@ -38,6 +50,20 @@ namespace osu.Game.Overlays.Mods
return modsAfterRemoval.ToList(); return modsAfterRemoval.ToList();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.IncreaseModSpeed:
return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod));
case GlobalAction.DecreaseModSpeed:
return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod));
}
return base.OnPressed(e);
}
private partial class UserModColumn : ModColumn private partial class UserModColumn : ModColumn
{ {
public UserModColumn(ModType modType, bool allowIncompatibleSelection) public UserModColumn(ModType modType, bool allowIncompatibleSelection)

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
namespace osu.Game.Overlays.OSD
{
public partial class SpeedChangeToast : Toast
{
public SpeedChangeToast(OsuConfigManager config, double newSpeed)
: base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed))
{
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
private void updateState() private void updateState()
{ {
CanRotateSelectionOrigin.Value = selectedItems.Count > 0; CanRotateAroundSelectionOrigin.Value = selectedItems.Count > 0;
} }
private Drawable[]? objectsInRotation; private Drawable[]? objectsInRotation;

View File

@ -43,7 +43,12 @@ namespace osu.Game.Rulesets
// add all legacy rulesets first to ensure they have exclusive choice of primary key. // add all legacy rulesets first to ensure they have exclusive choice of primary key.
foreach (var r in instances.Where(r => r is ILegacyRuleset)) foreach (var r in instances.Where(r => r is ILegacyRuleset))
availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); {
availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)
{
Available = true
});
}
} }
} }
} }

View File

@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -24,13 +21,13 @@ namespace osu.Game.Rulesets.Judgements
{ {
private const float judgement_size = 128; private const float judgement_size = 128;
public JudgementResult Result { get; private set; } public JudgementResult? Result { get; private set; }
public DrawableHitObject JudgedObject { get; private set; } public DrawableHitObject? JudgedObject { get; private set; }
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
protected SkinnableDrawable JudgementBody { get; private set; } protected SkinnableDrawable? JudgementBody { get; private set; }
private readonly Container aboveHitObjectsContent; private readonly Container aboveHitObjectsContent;
@ -97,12 +94,19 @@ namespace osu.Game.Rulesets.Judgements
/// </summary> /// </summary>
/// <param name="result">The applicable judgement.</param> /// <param name="result">The applicable judgement.</param>
/// <param name="judgedObject">The drawable object.</param> /// <param name="judgedObject">The drawable object.</param>
public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) public void Apply(JudgementResult result, DrawableHitObject? judgedObject)
{ {
Result = result; Result = result;
JudgedObject = judgedObject; JudgedObject = judgedObject;
} }
protected override void FreeAfterUse()
{
base.FreeAfterUse();
JudgedObject = null;
}
protected override void PrepareForUse() protected override void PrepareForUse()
{ {
base.PrepareForUse(); base.PrepareForUse();
@ -121,6 +125,8 @@ namespace osu.Game.Rulesets.Judgements
ApplyTransformsAt(double.MinValue, true); ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true); ClearTransforms(true);
Debug.Assert(Result != null && JudgementBody != null);
LifetimeStart = Result.TimeAbsolute; LifetimeStart = Result.TimeAbsolute;
using (BeginAbsoluteSequence(Result.TimeAbsolute)) using (BeginAbsoluteSequence(Result.TimeAbsolute))

View File

@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mods
{ {
overlay.ShowHud.Value = false; overlay.ShowHud.Value = false;
overlay.ShowHud.Disabled = true; overlay.ShowHud.Disabled = true;
overlay.PlayfieldSkinLayer.Hide();
} }
public void ApplyToPlayer(Player player) public void ApplyToPlayer(Player player)

View File

@ -330,6 +330,20 @@ namespace osu.Game.Rulesets.Objects
if (subControlPoints.Length != 3) if (subControlPoints.Length != 3)
break; break;
CircularArcProperties circularArcProperties = new CircularArcProperties(subControlPoints);
// `PathApproximator` will already internally revert to B-spline if the arc isn't valid.
if (!circularArcProperties.IsValid)
break;
// taken from https://github.com/ppy/osu-framework/blob/1201e641699a1d50d2f6f9295192dad6263d5820/osu.Framework/Utils/PathApproximator.cs#L181-L186
int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius)))));
// 1000 subpoints requires an arc length of at least ~120 thousand to occur
// See here for calculations https://www.desmos.com/calculator/umj6jvmcz7
if (subPoints >= 1000)
break;
List<Vector2> subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); List<Vector2> subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints);
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.

View File

@ -1,4 +1,4 @@
// 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 #nullable disable
@ -21,6 +21,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.LZMA;
namespace osu.Game.Scoring.Legacy namespace osu.Game.Scoring.Legacy
@ -262,7 +263,7 @@ namespace osu.Game.Scoring.Legacy
private void readLegacyReplay(Replay replay, StreamReader reader) private void readLegacyReplay(Replay replay, StreamReader reader)
{ {
float lastTime = beatmapOffset; float lastTime = beatmapOffset;
ReplayFrame currentFrame = null; var legacyFrames = new List<LegacyReplayFrame>();
string[] frames = reader.ReadToEnd().Split(','); string[] frames = reader.ReadToEnd().Split(',');
@ -285,23 +286,44 @@ namespace osu.Game.Scoring.Legacy
lastTime += diff; lastTime += diff;
if (i < 2 && mouseX == 256 && mouseY == -500) legacyFrames.Add(new LegacyReplayFrame(lastTime,
mouseX,
mouseY,
(ReplayButtonState)Parsing.ParseInt(split[3])));
}
// https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/ReplayWatcher.cs#L62-L67
if (legacyFrames.Count >= 2 && legacyFrames[1].Time < legacyFrames[0].Time)
{
legacyFrames[1].Time = legacyFrames[0].Time;
legacyFrames[0].Time = 0;
}
// https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/ReplayWatcher.cs#L69-L71
if (legacyFrames.Count >= 3 && legacyFrames[0].Time > legacyFrames[2].Time)
legacyFrames[0].Time = legacyFrames[1].Time = legacyFrames[2].Time;
// at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively.
// both frames use a position of (256, -500). // both frames use a position of (256, -500).
// ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania)
if (legacyFrames.Count >= 2 && legacyFrames[1].Position == new Vector2(256, -500))
legacyFrames.RemoveAt(1);
if (legacyFrames.Count >= 1 && legacyFrames[0].Position == new Vector2(256, -500))
legacyFrames.RemoveAt(0);
ReplayFrame currentFrame = null;
foreach (var legacyFrame in legacyFrames)
{
// never allow backwards time traversal in relation to the current frame.
// this handles frames with negative delta.
// this doesn't match stable 100% as stable will do something similar to adding an interpolated "intermediate frame"
// at the point wherein time flow changes from backwards to forwards, but it'll do for now.
if (currentFrame != null && legacyFrame.Time < currentFrame.Time)
continue; continue;
// Todo: At some point we probably want to rewind and play back the negative-time frames replay.Frames.Add(currentFrame = convertFrame(legacyFrame, currentFrame));
// but for now we'll achieve equal playback to stable by skipping negative frames
if (diff < 0)
continue;
currentFrame = convertFrame(new LegacyReplayFrame(lastTime,
mouseX,
mouseY,
(ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame);
replay.Frames.Add(currentFrame);
} }
} }

View File

@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void load() private void load()
{ {
if (rotationHandler != null) if (rotationHandler != null)
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin); canRotate.BindTo(rotationHandler.CanRotateAroundSelectionOrigin);
if (scaleHandler != null) if (scaleHandler != null)
{ {

View File

@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Whether rotation anchored by the selection origin can currently be performed. /// Whether rotation anchored by the selection origin can currently be performed.
/// </summary> /// </summary>
public Bindable<bool> CanRotateSelectionOrigin { get; private set; } = new BindableBool(); public Bindable<bool> CanRotateAroundSelectionOrigin { get; private set; } = new BindableBool();
/// <summary> /// <summary>
/// Whether rotation anchored by the center of the playfield can currently be performed. /// Whether rotation anchored by the center of the playfield can currently be performed.
/// </summary> /// </summary>
public Bindable<bool> CanRotatePlayfieldOrigin { get; private set; } = new BindableBool(); public Bindable<bool> CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool();
/// <summary> /// <summary>
/// Performs a single, instant, atomic rotation operation. /// Performs a single, instant, atomic rotation operation.

View File

@ -0,0 +1,64 @@
// 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.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Footer
{
public partial class ScreenBackButton : ShearedButton
{
// todo: see https://github.com/ppy/osu-framework/issues/3271
private const float torus_scale_factor = 1.2f;
public const float BUTTON_WIDTH = 240;
public ScreenBackButton()
: base(BUTTON_WIDTH, 70)
{
}
[BackgroundDependencyLoader]
private void load()
{
ButtonContent.Child = new FillFlowContainer
{
X = -10f,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20f, 0f),
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(20f),
Icon = FontAwesome.Solid.ChevronLeft,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor),
Text = CommonStrings.Back,
UseFullGlyphHeight = false,
}
}
};
DarkerColour = Color4Extensions.FromHex("#DE31AE");
LighterColour = Color4Extensions.FromHex("#FF86DD");
TextColour = Color4.White;
}
}
}

View File

@ -7,7 +7,6 @@ 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.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
@ -15,9 +14,8 @@ namespace osu.Game.Screens.Footer
{ {
public partial class ScreenFooter : InputBlockingContainer public partial class ScreenFooter : InputBlockingContainer
{ {
//Should be 60, setting to 50 for now for the sake of matching the current BackButton height. private const int height = 60;
private const int height = 50; private const int padding = 60;
private const int padding = 80;
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>(); private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
@ -68,13 +66,20 @@ namespace osu.Game.Screens.Footer
}, },
buttons = new FillFlowContainer<ScreenFooterButton> buttons = new FillFlowContainer<ScreenFooterButton>
{ {
Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10), Position = new Vector2(ScreenBackButton.BUTTON_WIDTH + padding, 10),
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(7, 0), Spacing = new Vector2(7, 0),
AutoSizeAxes = Axes.Both AutoSizeAxes = Axes.Both
} },
new ScreenBackButton
{
Margin = new MarginPadding { Bottom = 10f, Left = 12f },
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = () => { },
},
}; };
} }
} }

View File

@ -25,8 +25,7 @@ namespace osu.Game.Screens.Footer
{ {
public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction> public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{ {
// This should be 12 by design, but an extra allowance is added due to the corner radius specification. private const float shear = OsuGame.SHEAR;
private const float shear_width = 13.5f;
protected const int CORNER_RADIUS = 10; protected const int CORNER_RADIUS = 10;
protected const int BUTTON_HEIGHT = 90; protected const int BUTTON_HEIGHT = 90;
@ -34,7 +33,7 @@ namespace osu.Game.Screens.Footer
public Bindable<Visibility> OverlayState = new Bindable<Visibility>(); public Bindable<Visibility> OverlayState = new Bindable<Visibility>();
protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear_width / BUTTON_HEIGHT, 0); protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0);
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;

View File

@ -115,7 +115,6 @@ namespace osu.Game.Screens.Menu
backgroundContent = CreateBackground(colour).With(bg => backgroundContent = CreateBackground(colour).With(bg =>
{ {
bg.RelativeSizeAxes = Axes.Y; bg.RelativeSizeAxes = Axes.Y;
bg.X = -ButtonSystem.WEDGE_WIDTH;
bg.Anchor = Anchor.Centre; bg.Anchor = Anchor.Centre;
bg.Origin = Anchor.Centre; bg.Origin = Anchor.Centre;
}), }),

View File

@ -73,8 +73,19 @@ namespace osu.Game.Screens.Menu
Task.Run(() => request.Perform()) Task.Run(() => request.Perform())
.ContinueWith(r => .ContinueWith(r =>
{ {
if (!FetchOnlineContent)
return;
if (r.IsCompletedSuccessfully) if (r.IsCompletedSuccessfully)
Schedule(() => Current.Value = request.ResponseObject); {
Schedule(() =>
{
if (!FetchOnlineContent)
return;
Current.Value = request.ResponseObject;
});
}
// if the request failed, "observe" the exception. // if the request failed, "observe" the exception.
// it isn't very important why this failed, as it's only for display. // it isn't very important why this failed, as it's only for display.
@ -170,6 +181,11 @@ namespace osu.Game.Screens.Menu
private Sprite flash = null!; private Sprite flash = null!;
/// <remarks>
/// Overridden as a safety for <see cref="openUrlAction"/> functioning correctly.
/// </remarks>
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private ScheduledDelegate? openUrlAction; private ScheduledDelegate? openUrlAction;
public MenuImage(APIMenuImage image) public MenuImage(APIMenuImage image)

View File

@ -22,6 +22,7 @@ using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -81,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay
private Mod[] requiredMods = Array.Empty<Mod>(); private Mod[] requiredMods = Array.Empty<Mod>();
private Container maskingContainer; private Container maskingContainer;
private Container difficultyIconContainer; private FillFlowContainer difficultyIconContainer;
private LinkFlowContainer beatmapText; private LinkFlowContainer beatmapText;
private LinkFlowContainer authorText; private LinkFlowContainer authorText;
private ExplicitContentBeatmapBadge explicitContent; private ExplicitContentBeatmapBadge explicitContent;
@ -93,6 +94,7 @@ namespace osu.Game.Screens.OnlinePlay
private Drawable removeButton; private Drawable removeButton;
private PanelBackground panelBackground; private PanelBackground panelBackground;
private FillFlowContainer mainFillFlow; private FillFlowContainer mainFillFlow;
private BeatmapCardThumbnail thumbnail;
[Resolved] [Resolved]
private RealmAccess realm { get; set; } private RealmAccess realm { get; set; }
@ -282,10 +284,23 @@ namespace osu.Game.Screens.OnlinePlay
if (beatmap != null) if (beatmap != null)
{ {
difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods) difficultyIconContainer.Children = new Drawable[]
{
thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!, (IBeatmapSetOnlineInfo)beatmap.BeatmapSet!)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 60,
RelativeSizeAxes = Axes.Y,
Dimmed = { Value = IsHovered }
},
new DifficultyIcon(beatmap, ruleset, requiredMods)
{ {
Size = new Vector2(icon_height), Size = new Vector2(icon_height),
TooltipType = DifficultyIconTooltipType.Extended, TooltipType = DifficultyIconTooltipType.Extended,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}; };
} }
else else
@ -329,7 +344,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override Drawable CreateContent() protected override Drawable CreateContent()
{ {
Action<SpriteText> fontParameters = s => s.Font = OsuFont.Default.With(weight: FontWeight.SemiBold); Action<SpriteText> fontParameters = s => s.Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold);
return maskingContainer = new Container return maskingContainer = new Container
{ {
@ -364,12 +379,15 @@ namespace osu.Game.Screens.OnlinePlay
{ {
new Drawable[] new Drawable[]
{ {
difficultyIconContainer = new Container difficultyIconContainer = new FillFlowContainer
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Left = 8, Right = 8 }, RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Margin = new MarginPadding { Right = 4 },
}, },
mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection)
{ {
@ -398,6 +416,8 @@ namespace osu.Game.Screens.OnlinePlay
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0), Spacing = new Vector2(10f, 0),
Children = new Drawable[] Children = new Drawable[]
@ -420,7 +440,8 @@ namespace osu.Game.Screens.OnlinePlay
Child = modDisplay = new ModDisplay Child = modDisplay = new ModDisplay
{ {
Scale = new Vector2(0.4f), Scale = new Vector2(0.4f),
ExpansionMode = ExpansionMode.AlwaysExpanded ExpansionMode = ExpansionMode.AlwaysExpanded,
Margin = new MarginPadding { Vertical = -6 },
} }
} }
} }
@ -484,6 +505,20 @@ namespace osu.Game.Screens.OnlinePlay
}, },
}; };
protected override bool OnHover(HoverEvent e)
{
if (thumbnail != null)
thumbnail.Dimmed.Value = true;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (thumbnail != null)
thumbnail.Dimmed.Value = false;
base.OnHoverLost(e);
}
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (AllowSelection && valid.Value) if (AllowSelection && valid.Value)

View File

@ -109,7 +109,10 @@ namespace osu.Game.Screens.Play
private readonly List<Drawable> hideTargets; private readonly List<Drawable> hideTargets;
private readonly Drawable playfieldComponents; /// <summary>
/// The container for skin components attached to <see cref="SkinComponentsContainerLookup.TargetArea.Playfield"/>
/// </summary>
internal readonly Drawable PlayfieldSkinLayer;
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true) public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
{ {
@ -129,7 +132,7 @@ namespace osu.Game.Screens.Play
drawableRuleset != null drawableRuleset != null
? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, })
: Empty(), : Empty(),
playfieldComponents = drawableRuleset != null PlayfieldSkinLayer = drawableRuleset != null
? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, }
: Empty(), : Empty(),
topRightElements = new FillFlowContainer topRightElements = new FillFlowContainer
@ -247,10 +250,10 @@ namespace osu.Game.Screens.Play
{ {
Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad;
playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); PlayfieldSkinLayer.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft);
playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; PlayfieldSkinLayer.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; PlayfieldSkinLayer.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation; PlayfieldSkinLayer.Rotation = drawableRuleset.Playfield.Rotation;
} }
float? lowestTopScreenSpaceLeft = null; float? lowestTopScreenSpaceLeft = null;

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Screens.Select
{
public partial class ModSpeedHotkeyHandler : Component
{
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
private ModRateAdjust? lastActiveRateAdjustMod;
protected override void LoadComplete()
{
base.LoadComplete();
selectedMods.BindValueChanged(val =>
{
lastActiveRateAdjustMod = val.NewValue.OfType<ModRateAdjust>().SingleOrDefault() ?? lastActiveRateAdjustMod;
}, true);
}
public bool ChangeSpeed(double delta, IEnumerable<Mod> availableMods)
{
double targetSpeed = (selectedMods.Value.OfType<ModRateAdjust>().SingleOrDefault()?.SpeedChange.Value ?? 1) + delta;
if (Precision.AlmostEquals(targetSpeed, 1, 0.005))
{
selectedMods.Value = selectedMods.Value.Where(m => m is not ModRateAdjust).ToList();
onScreenDisplay?.Display(new SpeedChangeToast(config, targetSpeed));
return true;
}
ModRateAdjust? targetMod;
if (lastActiveRateAdjustMod is ModDaycore || lastActiveRateAdjustMod is ModNightcore)
{
targetMod = targetSpeed < 1
? availableMods.OfType<ModDaycore>().SingleOrDefault()
: availableMods.OfType<ModNightcore>().SingleOrDefault();
}
else
{
targetMod = targetSpeed < 1
? availableMods.OfType<ModHalfTime>().SingleOrDefault()
: availableMods.OfType<ModDoubleTime>().SingleOrDefault();
}
if (targetMod == null)
return false;
// preserve other settings from latest rate adjust mod instance seen
if (lastActiveRateAdjustMod != null)
{
foreach (var (_, sourceProperty) in lastActiveRateAdjustMod.GetSettingsSourceProperties())
{
if (sourceProperty.Name == nameof(ModRateAdjust.SpeedChange))
continue;
var targetProperty = targetMod.GetType().GetProperty(sourceProperty.Name);
if (targetProperty == null)
continue;
var targetBindable = (IBindable)targetProperty.GetValue(targetMod)!;
var sourceBindable = (IBindable)sourceProperty.GetValue(lastActiveRateAdjustMod)!;
if (targetBindable.GetType() != sourceBindable.GetType())
continue;
lastActiveRateAdjustMod.CopyAdjustedSetting(targetBindable, sourceBindable);
}
}
targetMod.SpeedChange.Value = targetSpeed;
var intendedMods = selectedMods.Value.Where(m => m is not ModRateAdjust).Append(targetMod).ToList();
if (!ModUtils.CheckCompatibleSet(intendedMods))
return false;
selectedMods.Value = intendedMods;
onScreenDisplay?.Display(new SpeedChangeToast(config, targetMod.SpeedChange.Value));
return true;
}
}
}

View File

@ -39,6 +39,7 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Select.Details; using osu.Game.Screens.Select.Details;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -98,6 +99,9 @@ namespace osu.Game.Screens.Select
new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap())) new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap()))
}; };
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved] [Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!; private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
@ -133,6 +137,7 @@ namespace osu.Game.Screens.Select
private double audioFeedbackLastPlaybackTime; private double audioFeedbackLastPlaybackTime;
private IDisposable? modSelectOverlayRegistration; private IDisposable? modSelectOverlayRegistration;
private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!;
private AdvancedStats advancedStats = null!; private AdvancedStats advancedStats = null!;
@ -319,6 +324,7 @@ namespace osu.Game.Screens.Select
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(),
}); });
if (ShowFooter) if (ShowFooter)
@ -1007,11 +1013,20 @@ namespace osu.Game.Screens.Select
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (!this.IsCurrentScreen()) return false;
switch (e.Action)
{
case GlobalAction.IncreaseModSpeed:
return modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value)));
case GlobalAction.DecreaseModSpeed:
return modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value)));
}
if (e.Repeat) if (e.Repeat)
return false; return false;
if (!this.IsCurrentScreen()) return false;
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.Select: case GlobalAction.Select:

View File

@ -33,12 +33,9 @@ namespace osu.Game.Screens.SelectV2.Footer
{ {
// todo: see https://github.com/ppy/osu-framework/issues/3271 // todo: see https://github.com/ppy/osu-framework/issues/3271
private const float torus_scale_factor = 1.2f; private const float torus_scale_factor = 1.2f;
private const float bar_shear_width = 7f;
private const float bar_height = 37f; private const float bar_height = 37f;
private const float mod_display_portion = 0.65f; private const float mod_display_portion = 0.65f;
private static readonly Vector2 bar_shear = new Vector2(bar_shear_width / bar_height, 0);
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>()); private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public Bindable<IReadOnlyList<Mod>> Current public Bindable<IReadOnlyList<Mod>> Current
@ -77,7 +74,7 @@ namespace osu.Game.Screens.SelectV2.Footer
Y = -5f, Y = -5f,
Depth = float.MaxValue, Depth = float.MaxValue,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Shear = bar_shear, Shear = BUTTON_SHEAR,
CornerRadius = CORNER_RADIUS, CornerRadius = CORNER_RADIUS,
Size = new Vector2(BUTTON_WIDTH, bar_height), Size = new Vector2(BUTTON_WIDTH, bar_height),
Masking = true, Masking = true,
@ -107,7 +104,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Shear = -bar_shear, Shear = -BUTTON_SHEAR,
UseFullGlyphHeight = false, UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold)
} }
@ -129,7 +126,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Shear = -bar_shear, Shear = -BUTTON_SHEAR,
Scale = new Vector2(0.6f), Scale = new Vector2(0.6f),
Current = { BindTarget = Current }, Current = { BindTarget = Current },
ExpansionMode = ExpansionMode.AlwaysContracted, ExpansionMode = ExpansionMode.AlwaysContracted,
@ -138,7 +135,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Shear = -bar_shear, Shear = -BUTTON_SHEAR,
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold),
Mods = { BindTarget = Current }, Mods = { BindTarget = Current },
} }
@ -304,7 +301,7 @@ namespace osu.Game.Screens.SelectV2.Footer
Y = -5f; Y = -5f;
Depth = float.MaxValue; Depth = float.MaxValue;
Origin = Anchor.BottomLeft; Origin = Anchor.BottomLeft;
Shear = bar_shear; Shear = BUTTON_SHEAR;
CornerRadius = CORNER_RADIUS; CornerRadius = CORNER_RADIUS;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Height = bar_height; Height = bar_height;
@ -328,7 +325,7 @@ namespace osu.Game.Screens.SelectV2.Footer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Shear = -bar_shear, Shear = -BUTTON_SHEAR,
Text = ModSelectOverlayStrings.Unranked.ToUpper(), Text = ModSelectOverlayStrings.Unranked.ToUpper(),
Margin = new MarginPadding { Horizontal = 15 }, Margin = new MarginPadding { Horizontal = 15 },
UseFullGlyphHeight = false, UseFullGlyphHeight = false,

View File

@ -83,6 +83,7 @@ namespace osu.Game.Storyboards.Drawables
Origin = animation.Origin; Origin = animation.Origin;
Position = animation.InitialPosition; Position = animation.InitialPosition;
Loop = animation.LoopType == AnimationLoopType.LoopForever; Loop = animation.LoopType == AnimationLoopType.LoopForever;
Name = animation.Path;
LifetimeStart = animation.StartTime; LifetimeStart = animation.StartTime;
LifetimeEnd = animation.EndTimeForDisplay; LifetimeEnd = animation.EndTimeForDisplay;

View File

@ -85,6 +85,7 @@ namespace osu.Game.Storyboards.Drawables
Sprite = sprite; Sprite = sprite;
Origin = sprite.Origin; Origin = sprite.Origin;
Position = sprite.InitialPosition; Position = sprite.InitialPosition;
Name = sprite.Path;
LifetimeStart = sprite.StartTime; LifetimeStart = sprite.StartTime;
LifetimeEnd = sprite.EndTimeForDisplay; LifetimeEnd = sprite.EndTimeForDisplay;

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="11.5.0" /> <PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.523.0" /> <PackageReference Include="ppy.osu.Framework" Version="2024.528.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.517.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2024.517.0" />
<PackageReference Include="Sentry" Version="4.3.0" /> <PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.523.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2024.528.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>