1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Merge branch 'master' into new-overlay-sfx

This commit is contained in:
Dean Herbert 2023-08-24 18:29:00 +09:00
commit 081fb308e1
69 changed files with 1202 additions and 284 deletions

View File

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

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
<!-- for editor usage -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest> </manifest>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- using a different name because package name cannot contain 'catch' --> <!-- using a different name because package name cannot contain 'catch' -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" />
</manifest> </manifest>

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Tests
AddSliderStep("start time", 500, 600, 0, x => AddSliderStep("start time", 500, 600, 0, x =>
{ {
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x;
drawableFruit.RefreshStateTransforms();
drawableBanana.RefreshStateTransforms();
}); });
} }
@ -44,6 +46,8 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("Initialize start time", () => AddStep("Initialize start time", () =>
{ {
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
drawableFruit.RefreshStateTransforms();
drawableBanana.RefreshStateTransforms();
fruitRotation = drawableFruit.DisplayRotation; fruitRotation = drawableFruit.DisplayRotation;
bananaRotation = drawableBanana.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation;
@ -54,6 +58,8 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("change start time", () => AddStep("change start time", () =>
{ {
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time;
drawableFruit.RefreshStateTransforms();
drawableBanana.RefreshStateTransforms();
}); });
AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation);
@ -64,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("reset start time", () => AddStep("reset start time", () =>
{ {
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
drawableFruit.RefreshStateTransforms();
drawableBanana.RefreshStateTransforms();
}); });
AddAssert("rotation and size restored", () => AddAssert("rotation and size restored", () =>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" />
</manifest> </manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" />
</manifest> </manifest>

View File

@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
base.Content.Children = new Drawable[] base.Content.Children = new Drawable[]
{ {
editorClock = new EditorClock(editorBeatmap), editorClock = new EditorClock(editorBeatmap),
snapProvider, new PopoverContainer { Child = snapProvider },
Content Content
}; };
} }
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>

View File

@ -0,0 +1,95 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestScenePreciseRotation : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset);
[Test]
public void TestHotkeyHandling()
{
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddStep("select first three objects", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3));
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
}
[Test]
public void TestRotateCorrectness()
{
AddStep("replace objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.AddRange(new HitObject[]
{
new HitCircle { Position = new Vector2(100) },
new HitCircle { Position = new Vector2(200) },
});
});
AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddStep("rotate by 180deg", () => getPopover().ChildrenOfType<TextBox>().Single().Current.Value = "180");
AddAssert("first object rotated 180deg around playfield centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100)));
AddAssert("second object rotated 180deg around playfield centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
AddAssert("first object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
AddAssert("second object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100)));
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
}
}
}

View File

@ -0,0 +1,110 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderReversal : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
private readonly PathControlPoint[][] paths =
{
createPathSegment(
PathType.PerfectCurve,
new Vector2(200, -50),
new Vector2(250, 0)
),
createPathSegment(
PathType.Linear,
new Vector2(100, 0),
new Vector2(100, 100)
)
};
private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions)
{
return positions.Select(p => new PathControlPoint
{
Position = p
}).Prepend(new PathControlPoint
{
Type = type
}).ToArray();
}
private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0];
[TestCase(0, 250)]
[TestCase(0, 200)]
[TestCase(1, 120)]
[TestCase(1, 80)]
public void TestSliderReversal(int pathIndex, double length)
{
var controlPoints = paths[pathIndex];
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = default;
var oldControlPointTypes = controlPoints.Select(p => p.Type);
AddStep("Add slider", () =>
{
var slider = new Slider
{
Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2),
Path = new SliderPath(controlPoints)
{
ExpectedDistance = { Value = length }
}
};
EditorBeatmap.Add(slider);
oldStartPos = slider.Position;
oldEndPos = slider.EndPosition;
oldDistance = slider.Path.Distance;
});
AddStep("Select slider", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects[0];
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("Reverse slider", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
AddAssert("Slider has correct start position", () =>
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
AddAssert("Slider has correct end position", () =>
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
AddAssert("Control points have correct types", () =>
{
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
});
}
}
}

View File

@ -1,17 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -19,9 +21,11 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods; 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.Osu.Scoring;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -30,8 +34,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
{ {
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private readonly OsuHitWindows referenceHitWindows;
private const double late_miss_window = 500; // time after +500 is considered a miss
/// <summary>
/// This is provided as a convenience for testing note lock behaviour against osu!stable.
/// Setting this field to a non-null path will cause beatmap files and replays used in all test cases
/// to be exported to disk so that they can be cross-checked against stable.
/// </summary>
private readonly string? exportLocation = null;
public TestSceneObjectOrderedHitPolicy()
{
referenceHitWindows = new OsuHitWindows();
referenceHitWindows.SetDifficulty(0);
}
/// <summary> /// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
@ -46,12 +62,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -65,7 +81,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window); // note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
} }
/// <summary> /// <summary>
@ -81,12 +98,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -100,7 +117,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window); // note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
} }
/// <summary> /// <summary>
@ -116,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -135,7 +153,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window); // note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
} }
/// <summary> /// <summary>
@ -151,12 +170,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -165,14 +184,14 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Meh);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90
} }
/// <summary> /// <summary>
@ -188,12 +207,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -202,13 +221,13 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Meh);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Ok);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
} }
@ -225,19 +244,19 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
}, },
new TestSlider new Slider
{ {
StartTime = time_slider, StartTime = time_slider,
Position = positionSlider, Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(25, 0), new Vector2(50, 0),
}) })
} }
}; };
@ -267,19 +286,19 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
}, },
new TestSlider new Slider
{ {
StartTime = time_slider, StartTime = time_slider,
Position = positionSlider, Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(25, 0), new Vector2(50, 0),
}) })
} }
}; };
@ -287,11 +306,11 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Ok);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
@ -304,7 +323,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestHitCircleBeforeSpinner() public void TestHitCircleBeforeSpinner()
{ {
const double time_spinner = 1500; const double time_spinner = 1500;
const double time_circle = 1800; const double time_circle = 1600;
Vector2 positionCircle = Vector2.Zero; Vector2 positionCircle = Vector2.Zero;
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
@ -315,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = time_spinner + 1000, EndTime = time_spinner + 1000,
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
@ -324,7 +343,7 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
@ -333,7 +352,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh);
} }
[Test] [Test]
@ -346,12 +365,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
}, },
new TestSlider new Slider
{ {
StartTime = time_slider, StartTime = time_slider,
Position = positionSlider, Position = positionSlider,
@ -380,38 +399,105 @@ namespace osu.Game.Rulesets.Osu.Tests
() => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result)); () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
} }
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result) private void addJudgementAssert(string name, Func<OsuHitObject?> hitObject, HitResult result)
{ {
AddAssert($"{name} judgement is {result}", AddAssert($"{name} judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); () => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result));
} }
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
{ {
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); () => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100));
} }
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer = null!;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults = null!;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames) private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, [CallerMemberName] string testCaseName = "")
{ {
AddStep("load player", () => IBeatmap playableBeatmap = null!;
Score score = null!;
AddStep("create beatmap", () =>
{ {
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject> Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{ {
Metadata =
{
Title = testCaseName
},
HitObjects = hitObjects, HitObjects = hitObjects,
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Difficulty = new BeatmapDifficulty
{
OverallDifficulty = 0,
SliderTickRate = 3
},
BeatmapInfo = BeatmapInfo =
{ {
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo,
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
}, },
ControlPointInfo = cpi
});
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
});
AddStep("create score", () =>
{
score = new Score
{
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
// required for correct playback in stable
new OsuReplayFrame(0, new Vector2(256, -500)),
new OsuReplayFrame(0, new Vector2(256, -500))
}.Concat(frames).ToList()
},
ScoreInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = playableBeatmap.BeatmapInfo
}
};
});
if (exportLocation != null)
{
AddStep("export beatmap", () =>
{
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
{
var memoryStream = new MemoryStream();
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
beatmapEncoder.Encode(writer);
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(stream);
memoryStream.Seek(0, SeekOrigin.Begin);
playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash();
}
}); });
AddStep("export score", () =>
{
using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create);
var encoder = new LegacyScoreEncoder(score, playableBeatmap);
encoder.Encode(stream);
});
}
AddStep("load player", () =>
{
SelectedMods.Value = new[] { new OsuModClassic() }; SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); var p = new ScoreAccessibleReplayPlayer(score);
p.OnLoadComplete += _ => p.OnLoadComplete += _ =>
{ {
@ -430,28 +516,6 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class TestHitCircle : HitCircle
{
protected override HitWindows CreateHitWindows() => new TestHitWindows();
}
private class TestSlider : Slider
{
public TestSlider()
{
SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
HeadCircle.HitWindows.SetDifficulty(0);
TailCircle.HitWindows.SetDifficulty(0);
};
}
}
private class TestSpinner : Spinner private class TestSpinner : Spinner
{ {
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
@ -461,19 +525,6 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
} }
private class TestHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Great, 500, 500, 500),
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
};
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
protected override DifficultyRange[] GetRanges() => ranges;
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{ {
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;

View File

@ -85,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
RightToolbox.Add(new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
});
} }
protected override ComposeBlueprintContainer CreateBlueprintContainer() protected override ComposeBlueprintContainer CreateBlueprintContainer()

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PreciseRotationPopover : OsuPopover
{
private readonly SelectionRotationHandler rotationHandler;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
{
this.rotationHandler = rotationHandler;
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[]
{
angleInput = new SliderWithTextBoxInput<float>("Angle (degrees):")
{
Current = new BindableNumber<float>
{
MinValue = -360,
MaxValue = 360,
Precision = 1
},
Instantaneous = true
},
rotationOrigin = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Selection centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => angleInput.TakeFocus());
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select();
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
});
}
protected override void PopIn()
{
base.PopIn();
rotationHandler.Begin();
}
protected override void PopOut()
{
base.PopOut();
if (IsLoaded)
rotationHandler.Commit();
}
}
public enum RotationOrigin
{
PlayfieldCentre,
SelectionCentre
}
public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
{
private readonly Bindable<bool> canRotate = new BindableBool();
private EditorToolButton rotateButton = null!;
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public TransformToolboxGroup()
: base("transform")
{
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler)),
// TODO: scale
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
// 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.
canRotate.BindTo(RotationHandler.CanRotate);
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
switch (e.Action)
{
case GlobalAction.EditorToggleRotateControl:
{
rotateButton.TriggerClick();
return true;
}
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
texture.Bind(); texture.Bind();
for (int i = 0; i < points.Count; i++) for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex); drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex);
UnbindTextureShader(renderer); UnbindTextureShader(renderer);
renderer.PopLocalMatrix(); renderer.PopLocalMatrix();
@ -325,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1); private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index) private void drawPointQuad(IRenderer renderer, SmokePoint point, RectangleF textureRect, int index)
{ {
Debug.Assert(quadBatch != null); Debug.Assert(quadBatch != null);
@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning
var localBotLeft = point.Position + ortho - dir; var localBotLeft = point.Position + ortho - dir;
var localBotRight = point.Position + ortho + dir; var localBotRight = point.Position + ortho + dir;
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localTopLeft, Position = localTopLeft,
TexturePosition = textureRect.TopLeft, TexturePosition = textureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour), Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
}); });
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localTopRight, Position = localTopRight,
TexturePosition = textureRect.TopRight, TexturePosition = textureRect.TopRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour), Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
}); });
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localBotRight, Position = localBotRight,
TexturePosition = textureRect.BottomRight, TexturePosition = textureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour), Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
}); });
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localBotLeft, Position = localBotLeft,
TexturePosition = textureRect.BottomLeft, TexturePosition = textureRect.BottomLeft,

View File

@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1) if (time - part.Time >= 1)
continue; continue;
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft, TexturePosition = textureRect.BottomLeft,
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight, TexturePosition = textureRect.BottomRight,
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight, TexturePosition = textureRect.TopRight,
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft, TexturePosition = textureRect.TopLeft,
@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[VertexMember(1, VertexAttribPointerType.Float)] [VertexMember(1, VertexAttribPointerType.Float)]
public float Time; public float Time;
[VertexMember(1, VertexAttribPointerType.Int)]
private readonly int maskingIndex;
public TexturedTrailVertex(IRenderer renderer)
{
this = default;
maskingIndex = renderer.CurrentMaskingIndex;
}
public bool Equals(TexturedTrailVertex other) public bool Equals(TexturedTrailVertex other)
{ {
return Position.Equals(other.Position) return Position.Equals(other.Position)
&& TexturePosition.Equals(other.TexturePosition) && TexturePosition.Equals(other.TexturePosition)
&& Colour.Equals(other.Colour) && Colour.Equals(other.Colour)
&& Time.Equals(other.Time); && Time.Equals(other.Time)
&& maskingIndex == other.maskingIndex;
} }
} }
} }

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" />
</manifest> </manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" />
</manifest> </manifest>

View File

@ -8,6 +8,9 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -15,7 +18,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
{ {
[HeadlessTest] [HeadlessTest]
public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
{ {
public IBindable<bool> IsPlaying => isPlaying; public IBindable<bool> IsPlaying => isPlaying;
@ -59,7 +62,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () => AddStep("Run background processor", () =>
{ {
Add(new TestBackgroundBeatmapProcessor()); Add(new TestBackgroundDataStoreProcessor());
}); });
AddUntilStep("wait for difficulties repopulated", () => AddUntilStep("wait for difficulties repopulated", () =>
@ -98,7 +101,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () => AddStep("Run background processor", () =>
{ {
Add(new TestBackgroundBeatmapProcessor()); Add(new TestBackgroundDataStoreProcessor());
}); });
AddWaitStep("wait some", 500); AddWaitStep("wait some", 500);
@ -124,7 +127,58 @@ namespace osu.Game.Tests.Database
}); });
} }
public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor [Test]
public void TestScoreUpgradeSuccess()
{
ScoreInfo scoreInfo = null!;
AddStep("Add score which requires upgrade (and has beatmap)", () =>
{
Realm.Write(r =>
{
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
{
TotalScoreVersion = 30000002,
LegacyTotalScore = 123456,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
[Test]
public void TestScoreUpgradeFailed()
{
ScoreInfo scoreInfo = null!;
AddStep("Add score which requires upgrade (but has no beatmap)", () =>
{
Realm.Write(r =>
{
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo(),
Ruleset = r.All<RulesetInfo>().First(),
})
{
TotalScoreVersion = 30000002,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
}
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
{ {
protected override int TimeToSleepDuringGameplay => 10; protected override int TimeToSleepDuringGameplay => 10;
} }

View File

@ -1,19 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
{ {
[TestFixture] [TestFixture]
public class LegacyBeatmapImporterTest public class LegacyBeatmapImporterTest : RealmTest
{ {
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter(); private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
@ -60,6 +64,33 @@ namespace osu.Game.Tests.Database
} }
} }
[Test]
public void TestStableDateAddedApplied()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
{
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus"));
string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly);
File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0));
await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage);
var importedSet = realm.Realm.All<BeatmapSetInfo>().Single();
Assert.NotNull(importedSet);
Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), importedSet.DateAdded);
}
});
}
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
{ {
public TestLegacyBeatmapImporter() public TestLegacyBeatmapImporter()

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Editing
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
public TestSceneHitObjectComposerDistanceSnapping() public TestSceneHitObjectComposerDistanceSnapping()
{ {

View File

@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
void main(void) void main(void)
{ {
// Transform from screen space to masking space. // Transform from screen space to masking space.
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z; v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour; v_Colour = m_Colour;

View File

@ -8,7 +8,7 @@ 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.Cursor;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
} }
public partial class EditorBeatmapContainer : Container public partial class EditorBeatmapContainer : PopoverContainer
{ {
private readonly IWorkingBeatmap working; private readonly IWorkingBeatmap working;

View File

@ -185,6 +185,37 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10); AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
} }
[Test]
public void TestGetSegmentEnds()
{
var positions = new[]
{
Vector2.Zero,
new Vector2(100, 0),
new Vector2(100),
new Vector2(200, 100),
};
double[] distances = { 100d, 200d, 300d };
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150)));
// see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]).
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[]
{
positions[1],
new Vector2(100, 50),
new Vector2(100, 50),
}));
}
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints) private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
{ {
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList(); var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus
foreach (var fountain in Children.OfType<StarFountain>()) foreach (var fountain in Children.OfType<StarFountain>())
{ {
if (RNG.NextSingle() > 0.8f) if (RNG.NextSingle() > 0.8f)
fountain.Shoot(); fountain.Shoot(RNG.Next(-1, 2));
} }
}, 150); }, 150);
} }

View File

@ -2,27 +2,36 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Tests.Components namespace osu.Game.Tournament.Tests.Components
{ {
[TestFixture] [TestFixture]
public partial class TestSceneSongBar : OsuTestScene public partial class TestSceneSongBar : TournamentTestScene
{ {
[Cached]
private readonly LadderInfo ladder = new LadderInfo();
private SongBar songBar = null!; private SongBar songBar = null!;
private TournamentBeatmap ladderBeatmap = null!;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps();
AddStep("setup picks bans", () =>
{
ladderBeatmap = CreateSampleBeatmap();
Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice
{
BeatmapID = ladderBeatmap.OnlineID,
Team = TeamColour.Red,
Type = ChoiceType.Pick,
});
});
AddStep("create bar", () => Child = songBar = new SongBar AddStep("create bar", () => Child = songBar = new SongBar
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -38,12 +47,14 @@ namespace osu.Game.Tournament.Tests.Components
AddStep("set beatmap", () => AddStep("set beatmap", () =>
{ {
var beatmap = CreateAPIBeatmap(Ruleset.Value); var beatmap = CreateAPIBeatmap(Ruleset.Value);
beatmap.CircleSize = 3.4f; beatmap.CircleSize = 3.4f;
beatmap.ApproachRate = 6.8f; beatmap.ApproachRate = 6.8f;
beatmap.OverallDifficulty = 5.5f; beatmap.OverallDifficulty = 5.5f;
beatmap.StarRating = 4.56f; beatmap.StarRating = 4.56f;
beatmap.Length = 123456; beatmap.Length = 123456;
beatmap.BPM = 133; beatmap.BPM = 133;
beatmap.OnlineID = ladderBeatmap.OnlineID;
songBar.Beatmap = new TournamentBeatmap(beatmap); songBar.Beatmap = new TournamentBeatmap(beatmap);
}); });

View File

@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Components
} }
} }
}, },
new UnmaskedTournamentBeatmapPanel(beatmap) new TournamentBeatmapPanel(beatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 0.5f, Width = 0.5f,
@ -277,18 +277,4 @@ namespace osu.Game.Tournament.Components
} }
} }
} }
internal partial class UnmaskedTournamentBeatmapPanel : TournamentBeatmapPanel
{
public UnmaskedTournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
: base(beatmap, mod)
{
}
[BackgroundDependencyLoader]
private void load()
{
Masking = false;
}
}
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Tournament.Models
public Bindable<int> LastYearPlacing = new BindableInt public Bindable<int> LastYearPlacing = new BindableInt
{ {
MinValue = 1, MinValue = 0,
MaxValue = 256 MaxValue = 256
}; };

View File

@ -10,7 +10,9 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors
Width = 0.2f, Width = 0.2f,
Current = Model.Seed Current = Model.Seed
}, },
new SettingsSlider<int> new SettingsSlider<int, LastYearPlacementSlider>
{ {
LabelText = "Last Year Placement", LabelText = "Last Year Placement",
Width = 0.33f, Width = 0.33f,
@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors
}; };
} }
private partial class LastYearPlacementSlider : RoundedSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;
}
public partial class PlayerEditor : CompositeDrawable public partial class PlayerEditor : CompositeDrawable
{ {
private readonly TournamentTeam team; private readonly TournamentTeam team;

View File

@ -274,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } }, new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"), new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
new RowDisplay("Seed:", team.Seed.Value), new RowDisplay("Seed:", team.Seed.Value),
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "0"), new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"),
new Container { Margin = new MarginPadding { Bottom = 30 } }, new Container { Margin = new MarginPadding { Bottom = 30 } },
} }
}, },

View File

@ -24,7 +24,10 @@ using osu.Game.Screens.Play;
namespace osu.Game namespace osu.Game
{ {
public partial class BackgroundBeatmapProcessor : Component /// <summary>
/// Performs background updating of data stores at startup.
/// </summary>
public partial class BackgroundDataStoreProcessor : Component
{ {
[Resolved] [Resolved]
private RulesetStore rulesetStore { get; set; } = null!; private RulesetStore rulesetStore { get; set; } = null!;
@ -61,7 +64,8 @@ namespace osu.Game
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
Logger.Log("Beginning background beatmap processing.."); Logger.Log("Beginning background data store processing..");
checkForOutdatedStarRatings(); checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics(); processBeatmapSetsWithMissingMetrics();
processScoresWithMissingStatistics(); processScoresWithMissingStatistics();
@ -74,7 +78,7 @@ namespace osu.Game
return; return;
} }
Logger.Log("Finished background beatmap processing!"); Logger.Log("Finished background data store processing!");
}); });
} }
@ -182,7 +186,7 @@ namespace osu.Game
realmAccess.Run(r => realmAccess.Run(r =>
{ {
foreach (var score in r.All<ScoreInfo>()) foreach (var score in r.All<ScoreInfo>().Where(s => !s.BackgroundReprocessingFailed))
{ {
if (score.BeatmapInfo != null if (score.BeatmapInfo != null
&& score.Statistics.Sum(kvp => kvp.Value) > 0 && score.Statistics.Sum(kvp => kvp.Value) > 0
@ -221,6 +225,7 @@ namespace osu.Game
catch (Exception e) catch (Exception e)
{ {
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
} }
} }
} }
@ -230,7 +235,7 @@ namespace osu.Game
Logger.Log("Querying for scores that need total score conversion..."); Logger.Log("Querying for scores that need total score conversion...");
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>() HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
.Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
.AsEnumerable().Select(s => s.ID))); .AsEnumerable().Select(s => s.ID)));
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
@ -279,6 +284,7 @@ namespace osu.Game
catch (Exception e) catch (Exception e)
{ {
Logger.Log($"Failed to convert total score for {id}: {e}"); Logger.Log($"Failed to convert total score for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
++failedCount; ++failedCount;
} }
} }

View File

@ -152,6 +152,8 @@ namespace osu.Game.Beatmaps
if (archive != null) if (archive != null)
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm)); beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
beatmapSet.DateAdded = getDateAdded(archive);
foreach (BeatmapInfo b in beatmapSet.Beatmaps) foreach (BeatmapInfo b in beatmapSet.Beatmaps)
{ {
b.BeatmapSet = beatmapSet; b.BeatmapSet = beatmapSet;
@ -305,11 +307,36 @@ namespace osu.Game.Beatmaps
return new BeatmapSetInfo return new BeatmapSetInfo
{ {
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1, OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1,
// Metadata = beatmap.Metadata,
DateAdded = DateTimeOffset.UtcNow
}; };
} }
/// <summary>
/// Determine the date a given beatmapset has been added to the game.
/// For legacy imports, we can use the oldest file write time for any `.osu` file in the directory.
/// For any other import types, use "now".
/// </summary>
private DateTimeOffset getDateAdded(ArchiveReader? reader)
{
DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
if (reader is LegacyDirectoryArchiveReader legacyReader)
{
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
dateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmaps.First()));
foreach (string beatmapName in beatmaps)
{
var currentDateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmapName));
if (currentDateAdded < dateAdded)
dateAdded = currentDateAdded;
}
}
return dateAdded;
}
/// <summary> /// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive. /// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary> /// </summary>

View File

@ -1,13 +1,10 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats
/// Register dependencies for use with static decoder classes. /// Register dependencies for use with static decoder classes.
/// </summary> /// </summary>
/// <param name="rulesets">A store containing all available rulesets (used by <see cref="LegacyBeatmapDecoder"/>).</param> /// <param name="rulesets">A store containing all available rulesets (used by <see cref="LegacyBeatmapDecoder"/>).</param>
public static void RegisterDependencies([NotNull] RulesetStore rulesets) public static void RegisterDependencies(RulesetStore rulesets)
{ {
LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets)); LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets));
} }
@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
throw new IOException(@"Unknown decoder type"); throw new IOException(@"Unknown decoder type");
// start off with the first line of the file // start off with the first line of the file
string line = stream.PeekLine()?.Trim(); string? line = stream.PeekLine()?.Trim();
while (line != null && line.Length == 0) while (line != null && line.Length == 0)
{ {

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 System.Collections.Generic; using System.Collections.Generic;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
/// <summary> /// <summary>
/// Retrieves the list of combo colours for presentation only. /// Retrieves the list of combo colours for presentation only.
/// </summary> /// </summary>
IReadOnlyList<Color4> ComboColours { get; } IReadOnlyList<Color4>? ComboColours { get; }
/// <summary> /// <summary>
/// The list of custom combo colours. /// The list of custom combo colours.

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
#pragma warning disable 618 #pragma warning disable 618
using System; using System;
@ -36,11 +34,11 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
private const double control_point_leniency = 1; private const double control_point_leniency = 1;
internal static RulesetStore RulesetStore; internal static RulesetStore? RulesetStore;
private Beatmap beatmap; private Beatmap beatmap = null!;
private ConvertHitObjectParser parser; private ConvertHitObjectParser? parser;
private LegacySampleBank defaultSampleBank; private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100; private int defaultSampleVolume = 100;
@ -222,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats
case @"Mode": case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value); int rulesetID = Parsing.ParseInt(pair.Value);
beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
switch (rulesetID) switch (rulesetID)
{ {

View File

@ -1,15 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using JetBrains.Annotations;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats
private readonly IBeatmap beatmap; private readonly IBeatmap beatmap;
[CanBeNull] private readonly ISkin? skin;
private readonly ISkin skin;
private readonly int onlineRulesetID; private readonly int onlineRulesetID;
@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
/// <param name="beatmap">The beatmap to encode.</param> /// <param name="beatmap">The beatmap to encode.</param>
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param> /// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
this.skin = skin; this.skin = skin;
@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[TimingPoints]"); writer.WriteLine("[TimingPoints]");
SampleControlPoint lastRelevantSamplePoint = null; SampleControlPoint? lastRelevantSamplePoint = null;
DifficultyControlPoint lastRelevantDifficultyPoint = null; DifficultyControlPoint? lastRelevantDifficultyPoint = null;
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats. // In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
// In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored. // In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored.
@ -585,7 +581,7 @@ namespace osu.Game.Beatmaps.Formats
return type; return type;
} }
private LegacySampleBank toLegacySampleBank(string sampleBank) private LegacySampleBank toLegacySampleBank(string? sampleBank)
{ {
switch (sampleBank?.ToLowerInvariant()) switch (sampleBank?.ToLowerInvariant())
{ {
@ -603,7 +599,7 @@ namespace osu.Game.Beatmaps.Formats
} }
} }
private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) private int toLegacyCustomSampleBank(HitSampleInfo? hitSampleInfo)
{ {
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
return legacy.CustomSampleBank; return legacy.CustomSampleBank;

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 System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats
{ {
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard> public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{ {
private StoryboardSprite storyboardSprite; private StoryboardSprite? storyboardSprite;
private CommandTimelineGroup timelineGroup; private CommandTimelineGroup? timelineGroup;
private Storyboard storyboard; private Storyboard storyboard = null!;
private readonly Dictionary<string, string> variables = new Dictionary<string, string>(); private readonly Dictionary<string, string> variables = new Dictionary<string, string>();

View File

@ -70,7 +70,22 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime); hitObject.StartTime = Math.Floor(hitObject.StartTime);
if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; if (hitObject is not IHasPath hasPath) continue;
// stable's hit object parsing expects the entire slider to use only one type of curve,
// and happens to use the last non-empty curve type read for the entire slider.
// this clear of the last control point type handles an edge case
// wherein the last control point of an otherwise-single-segment slider path has a different type than previous,
// which would lead to sliders being mangled when exported back to stable.
// normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below,
// which outputs a slider path containing only Bezier control points,
// but a non-inherited last control point is (rightly) not considered to be starting a new segment,
// therefore it would fail to clear the `CountSegments() <= 1` check.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier.
if (hasPath.Path.ControlPoints.Count > 1)
hasPath.Path.ControlPoints[^1].Type = null;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);

View File

@ -83,8 +83,9 @@ namespace osu.Game.Database
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
/// </summary> /// </summary>
private const int schema_version = 33; private const int schema_version = 34;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(hoverClickSounds = new HoverClickSounds()); AddInternal(hoverClickSounds = new HoverClickSounds());
updateTextColour(); updateTextColour();
}
protected override void LoadComplete()
{
base.LoadComplete();
Item.Action.BindDisabledChanged(_ => updateState(), true); Item.Action.BindDisabledChanged(_ => updateState(), true);
FinishTransforms();
} }
private void updateTextColour() private void updateTextColour()

View File

@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public string Text public string Text
{ {
get => Component.Text;
set => Component.Text = value; set => Component.Text = value;
} }

View File

@ -85,6 +85,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
Current.BindValueChanged(updateTextBoxFromSlider, true); Current.BindValueChanged(updateTextBoxFromSlider, true);
} }
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
private bool updatingFromTextBox; private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change) private void textChanged(ValueChangedEvent<string> change)

View File

@ -21,7 +21,9 @@ namespace osu.Game.IO.Archives
this.path = Path.GetFullPath(path); this.path = Path.GetFullPath(path);
} }
public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name)); public override Stream GetStream(string name) => File.OpenRead(GetFullPath(name));
public string GetFullPath(string filename) => Path.Combine(path, filename);
public override void Dispose() public override void Dispose()
{ {

View File

@ -3,33 +3,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation; using osu.Game.Localisation;
namespace osu.Game.Input.Bindings namespace osu.Game.Input.Bindings
{ {
public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput, IKeyBindingHandler<GlobalAction>
{ {
private readonly Drawable? handler; private readonly IKeyBindingHandler<GlobalAction>? handler;
private InputManager? parentInputManager;
public GlobalActionContainer(OsuGameBase? game) public GlobalActionContainer(OsuGameBase? game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers) : base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{ {
if (game is IKeyBindingHandler<GlobalAction>) if (game is IKeyBindingHandler<GlobalAction> h)
handler = game; handler = h;
} }
protected override void LoadComplete() protected override bool Prioritised => true;
{
base.LoadComplete();
parentInputManager = GetContainingInputManager();
}
// IMPORTANT: Take care when changing order of the items in the enumerable. // IMPORTANT: Take care when changing order of the items in the enumerable.
// It is used to decide the order of precedence, with the earlier items having higher precedence. // It is used to decide the order of precedence, with the earlier items having higher precedence.
@ -105,6 +98,7 @@ namespace osu.Game.Input.Bindings
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
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),
}; };
public IEnumerable<KeyBinding> InGameKeyBindings => new[] public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -160,20 +154,9 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
}; };
protected override IEnumerable<Drawable> KeyBindingInputQueue public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
{
get
{
// To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content.
// It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly
// allow the whole game to handle these actions.
// An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging. public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
return handler != null ? inputQueue.Prepend(handler) : inputQueue;
}
}
} }
public enum GlobalAction public enum GlobalAction
@ -378,5 +361,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
ToggleInGameLeaderboard, ToggleInGameLeaderboard,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
EditorToggleRotateControl,
} }
} }

View File

@ -344,6 +344,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
/// <summary>
/// "Toggle rotate control"
/// </summary>
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,19 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class OnlinePlayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay";
/// <summary>
/// "Playlist durations longer than 2 weeks require an active osu!supporter tag."
/// </summary>
public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -182,8 +182,6 @@ namespace osu.Game.Online.Chat
private readonly Message message; private readonly Message message;
private readonly Channel channel; private readonly Channel channel;
public override bool IsImportant => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay)
{ {

View File

@ -1025,7 +1025,7 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add); loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
Add(difficultyRecommender); Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());

View File

@ -392,17 +392,18 @@ namespace osu.Game
{ {
SafeAreaOverrideEdges = SafeAreaOverrideEdges, SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = CreateScalingContainer().WithChildren(new Drawable[] Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this)
{ {
(GlobalCursorDisplay = new GlobalCursorDisplay Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both (GlobalCursorDisplay = new GlobalCursorDisplay
}).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) {
{ RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
}), {
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. RelativeSizeAxes = Axes.Both
globalBindings = new GlobalActionContainer(this) }),
}
}) })
}); });

View File

@ -45,6 +45,9 @@ namespace osu.Game.Overlays
[Resolved] [Resolved]
private AudioManager audio { get; set; } = null!; private AudioManager audio { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
@ -178,6 +181,12 @@ namespace osu.Game.Overlays
playDebouncedSample(notification.PopInSampleName); playDebouncedSample(notification.PopInSampleName);
if (notification.IsImportant)
{
game?.Window?.Flash();
notification.Closed += () => game?.Window?.CancelFlash();
}
if (State.Value == Visibility.Hidden) if (State.Value == Visibility.Hidden)
{ {
notification.IsInToastTray = true; notification.IsInToastTray = true;

View File

@ -71,8 +71,21 @@ namespace osu.Game.Rulesets.Mods
{ {
var bindable = (IBindable)property.GetValue(this)!; var bindable = (IBindable)property.GetValue(this)!;
string valueText;
switch (bindable)
{
case Bindable<bool> b:
valueText = b.Value ? "on" : "off";
break;
default:
valueText = bindable.ToString() ?? string.Empty;
break;
}
if (!bindable.IsDefault) if (!bindable.IsDefault)
tooltipTexts.Add($"{attr.Label} {bindable}"); tooltipTexts.Add($"{attr.Label}: {valueText}");
} }
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));

View File

@ -260,7 +260,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
StartTimeBindable.BindTo(HitObject.StartTimeBindable); StartTimeBindable.BindTo(HitObject.StartTimeBindable);
StartTimeBindable.BindValueChanged(onStartTimeChanged);
if (HitObject is IHasComboInformation combo) if (HitObject is IHasComboInformation combo)
{ {
@ -311,9 +310,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
samplesBindable.UnbindFrom(HitObject.SamplesBindable); samplesBindable.UnbindFrom(HitObject.SamplesBindable);
// Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway.
StartTimeBindable.ValueChanged -= onStartTimeChanged;
// When a new hitobject is applied, the samples will be cleared before re-populating. // When a new hitobject is applied, the samples will be cleared before re-populating.
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
samplesBindable.CollectionChanged -= onSamplesChanged; samplesBindable.CollectionChanged -= onSamplesChanged;
@ -333,6 +329,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry);
ClearNestedHitObjects(); ClearNestedHitObjects();
// Changes to `HitObject` properties trigger default application, which triggers `State` updates.
// When a new hitobject is applied, `OnApply()` automatically performs a state update.
HitObject.DefaultsApplied -= onDefaultsApplied; HitObject.DefaultsApplied -= onDefaultsApplied;
entry.RevertResult -= onRevertResult; entry.RevertResult -= onRevertResult;
@ -375,8 +373,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => updateState(State.Value, true);
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
private void onRevertResult() private void onRevertResult()
@ -394,6 +390,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
Debug.Assert(Entry != null); Debug.Assert(Entry != null);
Apply(Entry); Apply(Entry);
// Applied defaults indicate a change in hit object state.
// We need to update the judgement result time to the new end time
// and update state to ensure the hit object fades out at the correct time.
if (Result is not null)
{
Result.TimeOffset = 0;
updateState(State.Value, true);
}
DefaultsApplied?.Invoke(this); DefaultsApplied?.Invoke(this);
} }

View File

@ -40,11 +40,13 @@ namespace osu.Game.Rulesets.Objects
private readonly List<Vector2> calculatedPath = new List<Vector2>(); private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>(); private readonly List<double> cumulativeLength = new List<double>();
private readonly List<int> segmentEnds = new List<int>();
private readonly Cached pathCache = new Cached(); private readonly Cached pathCache = new Cached();
private double calculatedLength; private double calculatedLength;
private readonly List<int> segmentEnds = new List<int>();
private double[] segmentEndDistances = Array.Empty<double>();
/// <summary> /// <summary>
/// Creates a new <see cref="SliderPath"/>. /// Creates a new <see cref="SliderPath"/>.
/// </summary> /// </summary>
@ -196,13 +198,28 @@ namespace osu.Game.Rulesets.Objects
} }
/// <summary> /// <summary>
/// Returns the progress values at which segments of the path end. /// Returns the progress values at which (control point) segments of the path end.
/// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path).
/// </summary> /// </summary>
/// <remarks>
/// <see cref="PositionAt"/> truncates the progression values to [0,1],
/// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path.
/// </remarks>
/// <example>
/// <para>
/// In case <see cref="Distance"/> is less than <see cref="CalculatedDistance"/>,
/// the last segment ends after the end of the path, hence it returns a value greater than 1.
/// </para>
/// <para>
/// In case <see cref="Distance"/> is greater than <see cref="CalculatedDistance"/>,
/// the last segment ends before the end of the path, hence it returns a value less than 1.
/// </para>
/// </example>
public IEnumerable<double> GetSegmentEnds() public IEnumerable<double> GetSegmentEnds()
{ {
ensureValid(); ensureValid();
return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength); return segmentEndDistances.Select(d => d / Distance);
} }
private void invalidate() private void invalidate()
@ -251,8 +268,11 @@ namespace osu.Game.Rulesets.Objects
calculatedPath.Add(t); calculatedPath.Add(t);
} }
// Remember the index of the segment end if (i > 0)
segmentEnds.Add(calculatedPath.Count - 1); {
// Remember the index of the segment end
segmentEnds.Add(calculatedPath.Count - 1);
}
// Start the new segment at the current vertex // Start the new segment at the current vertex
start = i; start = i;
@ -298,6 +318,14 @@ namespace osu.Game.Rulesets.Objects
cumulativeLength.Add(calculatedLength); cumulativeLength.Add(calculatedLength);
} }
// Store the distances of the segment ends now, because after shortening the indices may be out of range
segmentEndDistances = new double[segmentEnds.Count];
for (int i = 0; i < segmentEnds.Count; i++)
{
segmentEndDistances[i] = cumulativeLength[segmentEnds[i]];
}
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance) if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
{ {
// In osu-stable, if the last two control points of a slider are equal, extension is not performed. // In osu-stable, if the last two control points of a slider are equal, extension is not performed.
@ -319,10 +347,6 @@ namespace osu.Game.Rulesets.Objects
{ {
cumulativeLength.RemoveAt(cumulativeLength.Count - 1); cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--); calculatedPath.RemoveAt(pathEndIndex--);
// Shorten the last segment to the expected distance
if (segmentEnds.Count > 0)
segmentEnds[^1]--;
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -25,6 +26,53 @@ namespace osu.Game.Rulesets.Objects
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param> /// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param> /// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
{
var controlPoints = sliderPath.ControlPoints;
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList();
// Inherited points after a linear point, as well as the first control point if it inherited,
// should be treated as linear points, so their types are temporarily changed to linear.
inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear);
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
// Remove segments after the end of the slider.
for (int numSegmentsToRemove = segmentEnds.Count(se => se >= 1) - 1; numSegmentsToRemove > 0 && controlPoints.Count > 0;)
{
if (controlPoints.Last().Type is not null)
{
numSegmentsToRemove--;
segmentEnds = segmentEnds[..^1];
}
controlPoints.RemoveAt(controlPoints.Count - 1);
}
// Restore original control point types.
inheritedLinearPoints.ForEach(p => p.Type = null);
// Recalculate middle perfect curve control points at the end of the slider path.
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any())
{
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
double lastSegmentEnd = segmentEnds[^1];
var circleArcPath = new List<Vector2>();
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
}
sliderPath.reverseControlPoints(out positionalOffset);
}
/// <summary>
/// Reverses the order of the provided <see cref="SliderPath"/>'s <see cref="PathControlPoint"/>s.
/// </summary>
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset)
{ {
var points = sliderPath.ControlPoints.ToArray(); var points = sliderPath.ControlPoints.ToArray();
positionalOffset = sliderPath.PositionAt(1); positionalOffset = sliderPath.PositionAt(1);

View File

@ -66,7 +66,7 @@ namespace osu.Game.Scoring
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>, /// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
/// the total score has not yet been updated to reflect the current scoring values. /// the total score has not yet been updated to reflect the current scoring values.
/// ///
/// See <see cref="BackgroundBeatmapProcessor"/>'s conversion logic. /// See <see cref="BackgroundDataStoreProcessor"/>'s conversion logic.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This may not match the version stored in the replay files. /// This may not match the version stored in the replay files.
@ -81,6 +81,15 @@ namespace osu.Game.Scoring
/// </remarks> /// </remarks>
public long? LegacyTotalScore { get; set; } public long? LegacyTotalScore { get; set; }
/// <summary>
/// If background processing of this beatmap failed in some way, this flag will become <c>true</c>.
/// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail.
/// </summary>
/// <remarks>
/// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk).
/// </remarks>
public bool BackgroundReprocessingFailed { get; set; }
public int MaxCombo { get; set; } public int MaxCombo { get; set; }
public double Accuracy { get; set; } public double Accuracy { get; set; }

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components
{
public partial class EditorToolButton : OsuButton, IHasPopover
{
public BindableBool Selected { get; } = new BindableBool();
private readonly Func<Drawable> createIcon;
private readonly Func<Popover?> createPopover;
private Color4 defaultBackgroundColour;
private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
private Color4 selectedIconColour;
private Drawable icon = null!;
public EditorToolButton(LocalisableString text, Func<Drawable> createIcon, Func<Popover?> createPopover)
{
Text = text;
this.createIcon = createIcon;
this.createPopover = createPopover;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
defaultBackgroundColour = colourProvider.Background3;
selectedBackgroundColour = colourProvider.Background1;
defaultIconColour = defaultBackgroundColour.Darken(0.5f);
selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = createIcon().With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
Action = Selected.Toggle;
}
protected override void LoadComplete()
{
base.LoadComplete();
Selected.BindValueChanged(_ => updateSelectionState(), true);
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
BackgroundColour = Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = Selected.Value ? selectedIconColour : defaultIconColour;
if (Selected.Value)
this.ShowPopover();
else
this.HidePopover();
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
public Popover? GetPopover() => Enabled.Value
? createPopover()?.With(p =>
{
p.State.BindValueChanged(state =>
{
if (state.NewValue == Visibility.Hidden)
Selected.Value = false;
});
})
: null;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; } public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
protected SelectionHandler<T> SelectionHandler { get; private set; } public SelectionHandler<T> SelectionHandler { get; private set; }
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>(); private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();

View File

@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; } protected IEditorChangeHandler ChangeHandler { get; private set; }
protected SelectionRotationHandler RotationHandler { get; private set; } public SelectionRotationHandler RotationHandler { get; private set; }
protected SelectionHandler() protected SelectionHandler()
{ {

View File

@ -147,13 +147,25 @@ namespace osu.Game.Screens.Edit.Timing
trackedType = null; trackedType = null;
else else
{ {
// If the selected group only has one control point, update the tracking type. switch (selectedGroup.Value.ControlPoints.Count)
if (selectedGroup.Value.ControlPoints.Count == 1) {
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType(); // If the selected group has no control points, clear the tracked type.
// If the selected group has more than one control point, choose the first as the tracking type // Otherwise the user will be unable to select a group with no control points.
// if we don't already have a singular tracked type. case 0:
else if (trackedType == null) trackedType = null;
trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); break;
// If the selected group only has one control point, update the tracking type.
case 1:
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
break;
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
default:
trackedType ??= selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
break;
}
} }
if (trackedType != null) if (trackedType != null)

View File

@ -126,12 +126,9 @@ namespace osu.Game.Screens
private void load(ShaderManager manager) private void load(ShaderManager manager)
{ {
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR));
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
} }

View File

@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu
{ {
public partial class KiaiMenuFountains : BeatSyncedContainer public partial class KiaiMenuFountains : BeatSyncedContainer
{ {
private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu
Children = new[] Children = new[]
{ {
new StarFountain leftFountain = new StarFountain
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
X = 250, X = 250,
}, },
new StarFountain rightFountain = new StarFountain
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
return; return;
foreach (var fountain in Children.OfType<StarFountain>()) int direction = RNG.Next(-1, 2);
fountain.Shoot();
switch (direction)
{
case -1:
leftFountain.Shoot(1);
rightFountain.Shoot(-1);
break;
case 0:
leftFountain.Shoot(0);
rightFountain.Shoot(0);
break;
case 1:
leftFountain.Shoot(-1);
rightFountain.Shoot(1);
break;
}
lastTrigger = Clock.CurrentTime; lastTrigger = Clock.CurrentTime;
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Screens.Menu
InternalChild = spewer = new StarFountainSpewer(); InternalChild = spewer = new StarFountainSpewer();
} }
public void Shoot() => spewer.Shoot(); public void Shoot(int direction) => spewer.Shoot(direction);
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
@ -81,10 +81,10 @@ namespace osu.Game.Screens.Menu
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
} }
public void Shoot() public void Shoot(int direction)
{ {
lastShootTime = Clock.CurrentTime; lastShootTime = Clock.CurrentTime;
lastShootDirection = RNG.Next(-1, 2); lastShootDirection = direction;
} }
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);

View File

@ -113,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected partial class Section : Container protected partial class Section : Container
{ {
private readonly Container content; private readonly ReverseChildIDFillFlowContainer<Drawable> content;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
@ -135,10 +135,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12),
Text = title.ToUpperInvariant(), Text = title.ToUpperInvariant(),
}, },
content = new Container content = new ReverseChildIDFillFlowContainer<Drawable>
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical
}, },
}, },
}; };

View File

@ -23,6 +23,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK; using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private IBindable<APIUser> localUser = null!; private IBindable<APIUser> localUser = null!;
private readonly Room room; private readonly Room room;
private OsuSpriteText durationNoticeText = null!;
public MatchSettings(Room room) public MatchSettings(Room room)
{ {
@ -141,14 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
new Section("Duration") new Section("Duration")
{ {
Child = new Container Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, new Container
Height = 40,
Child = DurationField = new DurationDropdown
{ {
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X,
} Height = 40,
Child = DurationField = new DurationDropdown
{
RelativeSizeAxes = Axes.X
},
},
durationNoticeText = new OsuSpriteText
{
Alpha = 0,
Colour = colours.Yellow,
},
} }
}, },
new Section("Allowed attempts (across all playlist items)") new Section("Allowed attempts (across all playlist items)")
@ -305,6 +315,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true); MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true);
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
DurationField.Current.BindValueChanged(duration =>
{
if (hasValidDuration)
durationNoticeText.Hide();
else
{
durationNoticeText.Show();
durationNoticeText.Text = OnlinePlayStrings.SupporterOnlyDurationNotice;
}
});
localUser = api.LocalUser.GetBoundCopy(); localUser = api.LocalUser.GetBoundCopy();
localUser.BindValueChanged(populateDurations, true); localUser.BindValueChanged(populateDurations, true);
@ -314,6 +335,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void populateDurations(ValueChangedEvent<APIUser> user) private void populateDurations(ValueChangedEvent<APIUser> user)
{ {
// roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
// if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
const int days_in_month = 31;
DurationField.Items = new[] DurationField.Items = new[]
{ {
TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30),
@ -326,18 +351,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
TimeSpan.FromDays(3), TimeSpan.FromDays(3),
TimeSpan.FromDays(7), TimeSpan.FromDays(7),
TimeSpan.FromDays(14), TimeSpan.FromDays(14),
TimeSpan.FromDays(days_in_month),
TimeSpan.FromDays(days_in_month * 3),
}; };
// TODO: show these in the interface at all times.
if (user.NewValue.IsSupporter)
{
// roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
// if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
const int days_in_month = 31;
DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month));
DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3));
}
} }
protected override void Update() protected override void Update()
@ -352,7 +368,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) => private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0
&& hasValidDuration;
private bool hasValidDuration => DurationField.Current.Value <= TimeSpan.FromDays(14) || localUser.Value.IsSupporter;
private void apply() private void apply()
{ {

View File

@ -232,6 +232,6 @@ namespace osu.Game.Skinning
} }
private static Color4 getComboColour(IHasComboColours source, int colourIndex) private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count]; => source.ComboColours![colourIndex % source.ComboColours.Count];
} }
} }

View File

@ -203,6 +203,6 @@ namespace osu.Game.Skinning
} }
private static Color4 getComboColour(IHasComboColours source, int colourIndex) private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count]; => source.ComboColours![colourIndex % source.ComboColours.Count];
} }
} }

View File

@ -57,7 +57,13 @@ namespace osu.Game.Tests.Visual
} }
if (CreateNestedActionContainer) if (CreateNestedActionContainer)
mainContent.Add(new GlobalActionContainer(null)); {
var globalActionContainer = new GlobalActionContainer(null)
{
Child = mainContent
};
mainContent = globalActionContainer;
}
base.Content.AddRange(new Drawable[] base.Content.AddRange(new Drawable[]
{ {

View File

@ -47,6 +47,7 @@ namespace osu.Game.Utils
options.AutoSessionTracking = true; options.AutoSessionTracking = true;
options.IsEnvironmentUser = false; options.IsEnvironmentUser = false;
options.IsGlobalModeEnabled = true;
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
}); });

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.1.2" /> <PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.815.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.823.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.817.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" />
<PackageReference Include="Sentry" Version="3.28.1" /> <PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />

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="2023.815.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.823.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>