diff --git a/Directory.Build.props b/Directory.Build.props
index 2cd40c8675..2d3478f256 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,7 +16,7 @@
-
+
diff --git a/Gemfile.lock b/Gemfile.lock
index bf971d2c22..a4b49af7e4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -6,35 +6,36 @@ GEM
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
- aws-partitions (1.329.0)
- aws-sdk-core (3.99.2)
+ aws-partitions (1.354.0)
+ aws-sdk-core (3.104.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.34.1)
+ aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.68.1)
- aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sdk-s3 (1.78.0)
+ aws-sdk-core (~> 3, >= 3.104.3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.1.4)
- aws-eventstream (~> 1.0, >= 1.0.2)
+ aws-sigv4 (1.2.1)
+ aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
- declarative (0.0.10)
+ declarative (0.0.20)
declarative-option (0.1.0)
- digest-crc (0.5.1)
+ digest-crc (0.6.1)
+ rake (~> 13.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- dotenv (2.7.5)
- emoji_regex (1.0.1)
- excon (0.74.0)
+ dotenv (2.7.6)
+ emoji_regex (3.0.0)
+ excon (0.76.0)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
@@ -42,34 +43,32 @@ GEM
http-cookie (~> 1.0.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
- fastimage (2.1.7)
- fastlane (2.149.1)
+ fastimage (2.2.0)
+ fastlane (2.156.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
- babosa (>= 1.0.2, < 2.0.0)
+ babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
- emoji_regex (>= 0.1, < 2.0)
+ emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
- faraday (>= 0.17, < 2.0)
+ faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (>= 0.13.1, < 2.0)
+ faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
- jwt (~> 2.1.0)
+ jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
- multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
- public_suffix (~> 2.0.0)
- rubyzip (>= 1.3.0, < 2.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
@@ -97,17 +96,17 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.2)
+ google-cloud-env (1.3.3)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
- google-cloud-storage (1.26.2)
+ google-cloud-storage (1.27.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.12.0)
+ googleauth (0.13.1)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -119,29 +118,29 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
- json (2.3.0)
- jwt (2.1.0)
+ json (2.3.1)
+ jwt (2.2.1)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
- multi_json (1.14.1)
- multi_xml (0.6.0)
+ multi_json (1.15.0)
multipart-post (2.0.0)
- nanaimo (0.2.6)
+ nanaimo (0.3.0)
naturally (2.2.0)
- nokogiri (1.10.7)
+ nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
- os (1.1.0)
+ os (1.1.1)
plist (3.5.0)
- public_suffix (2.0.5)
+ public_suffix (4.0.5)
+ rake (13.0.1)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
- rubyzip (1.3.0)
+ rubyzip (2.3.0)
security (0.1.3)
signet (0.14.0)
addressable (~> 2.3)
@@ -160,7 +159,7 @@ GEM
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
- tty-screen (0.8.0)
+ tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
@@ -169,12 +168,12 @@ GEM
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.16.0)
+ xcodeproj (1.18.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
- nanaimo (~> 0.2.6)
+ nanaimo (~> 0.3.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
diff --git a/README.md b/README.md
index dc3ee63844..d3e9ca5121 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,9 @@
[](https://www.codefactor.io/repository/github/ppy/osu)
[](https://discord.gg/ppy)
-Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
+A free-to-win rhythm game. Rhythm is just a *click* away!
+
+The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status
diff --git a/osu.Android.props b/osu.Android.props
index 13b4b6ebbb..f3fb949f76 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 7a99c70999..62e8f7c518 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -3,7 +3,7 @@
netcoreapp3.1
WinExe
true
- click the circles. to the beat.
+ A free-to-win rhythm game. Rhythm is just a *click* away!
osu!
osu!lazer
osu!lazer
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index a919d54f38..2fc6009183 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -9,8 +9,7 @@
https://osu.ppy.sh/
https://puu.sh/tYyXZ/9a01a5d1b0.ico
false
- click the circles. to the beat.
- click the circles.
+ A free-to-win rhythm game. Rhythm is just a *click* away!
testing
Copyright (c) 2020 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 7c0b73e8c3..f9d56dfa78 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 972cbec4a2..ed00ed0b4c 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 94b5ee9486..7f5b9a6ee0 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -31,12 +31,12 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// The minimum time range. This occurs at a of 40.
///
- public const double MIN_TIME_RANGE = 150;
+ public const double MIN_TIME_RANGE = 340;
///
/// The maximum time range. This occurs at a of 1.
///
- public const double MAX_TIME_RANGE = 6000;
+ public const double MAX_TIME_RANGE = 13720;
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
new file mode 100644
index 0000000000..d8064d36ea
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModSpunOut : OsuModTestScene
+ {
+ protected override bool AllowFail => true;
+
+ [Test]
+ public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSpunOut(),
+ Autoplay = false,
+ Beatmap = singleSpinnerBeatmap,
+ PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1
+ });
+
+ [TestCase(null)]
+ [TestCase(typeof(OsuModDoubleTime))]
+ [TestCase(typeof(OsuModHalfTime))]
+ public void TestSpinRateUnaffectedByMods(Type additionalModType)
+ {
+ var mods = new List { new OsuModSpunOut() };
+ if (additionalModType != null)
+ mods.Add((Mod)Activator.CreateInstance(additionalModType));
+
+ CreateModTest(new ModTestData
+ {
+ Mods = mods,
+ Autoplay = false,
+ Beatmap = singleSpinnerBeatmap,
+ PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType().Single().SpinsPerMinute, 286, 1)
+ });
+ }
+
+ private Beatmap singleSpinnerBeatmap => new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 500,
+ Duration = 2000
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index b57561f3e1..47b3926ceb 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -17,32 +17,58 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
- public TestSceneSpinner()
+ private TestDrawableSpinner drawableSpinner;
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestVariousSpinners(bool autoplay)
{
- AddStep("Miss Big", () => SetContents(() => testSingle(2)));
- AddStep("Miss Medium", () => SetContents(() => testSingle(5)));
- AddStep("Miss Small", () => SetContents(() => testSingle(7)));
- AddStep("Hit Big", () => SetContents(() => testSingle(2, true)));
- AddStep("Hit Medium", () => SetContents(() => testSingle(5, true)));
- AddStep("Hit Small", () => SetContents(() => testSingle(7, true)));
+ string term = autoplay ? "Hit" : "Miss";
+ AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay)));
+ AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay)));
+ AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
}
- private Drawable testSingle(float circleSize, bool auto = false)
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestLongSpinner(bool autoplay)
{
- var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
+ AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 2000)));
+ AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
+ AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSuperShortSpinner(bool autoplay)
+ {
+ AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200)));
+ AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
+ AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
+ }
+
+ private Drawable testSingle(float circleSize, bool auto = false, double length = 3000)
+ {
+ const double delay = 2000;
+
+ var spinner = new Spinner
+ {
+ StartTime = Time.Current + delay,
+ EndTime = Time.Current + delay + length
+ };
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
- var drawable = new TestDrawableSpinner(spinner, auto)
+ drawableSpinner = new TestDrawableSpinner(spinner, auto)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
- return drawable;
+ return drawableSpinner;
}
private class TestDrawableSpinner : DrawableSpinner
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index b46964e8b7..69857f8ef9 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -7,6 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -69,11 +70,11 @@ namespace osu.Game.Rulesets.Osu.Tests
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
- AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
+ AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
- AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
+ AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
}
[Test]
@@ -94,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Tests
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
- AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation);
+ AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation);
addSeekStep(2500);
AddAssert("disc rotation rewound",
@@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
- () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100));
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
AddAssert("is disc rotation almost same",
@@ -114,26 +115,14 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
- () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100));
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
}
[Test]
public void TestRotationDirection([Values(true, false)] bool clockwise)
{
if (clockwise)
- {
- AddStep("flip replay", () =>
- {
- var drawableRuleset = this.ChildrenOfType().Single();
- var score = drawableRuleset.ReplayScore;
- var scoreWithFlippedReplay = new Score
- {
- ScoreInfo = score.ScoreInfo,
- Replay = flipReplay(score.Replay)
- };
- drawableRuleset.SetReplayScore(scoreWithFlippedReplay);
- });
- }
+ transformReplay(flip);
addSeekStep(5000);
@@ -141,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
- private Replay flipReplay(Replay scoreReplay) => new Replay
+ private Replay flip(Replay scoreReplay) => new Replay
{
Frames = scoreReplay
.Frames
@@ -164,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
- return totalScore == (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
+ return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK;
});
addSeekStep(0);
@@ -196,6 +185,49 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
}
+ [TestCase(0.5)]
+ [TestCase(2.0)]
+ public void TestSpinUnaffectedByClockRate(double rate)
+ {
+ double expectedProgress = 0;
+ double expectedSpm = 0;
+
+ addSeekStep(1000);
+ AddStep("retrieve spinner state", () =>
+ {
+ expectedProgress = drawableSpinner.Progress;
+ expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ });
+
+ addSeekStep(0);
+
+ AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
+ // autoplay replay frames use track time;
+ // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
+ // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
+ // as real time is what we care about for spinners
+ // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate).
+ transformReplay(replay => applyRateAdjustment(replay, rate));
+
+ addSeekStep(1000);
+ AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ }
+
+ private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var adjustedTime = replayFrame.Time * rate;
+ return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => track.Seek(time));
@@ -203,6 +235,18 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
+ private void transformReplay(Func replayTransformation) => AddStep("set replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var transformedScore = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = replayTransformation.Invoke(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(transformedScore);
+ });
+
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs
deleted file mode 100644
index d1210db6b1..0000000000
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
-
-namespace osu.Game.Rulesets.Osu.Tests
-{
- [TestFixture]
- public class TestSceneSpinnerSpunOut : OsuTestScene
- {
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- SelectedMods.Value = new[] { new OsuModSpunOut() };
- });
-
- [Test]
- public void TestSpunOut()
- {
- DrawableSpinner spinner = null;
-
- AddStep("create spinner", () => spinner = createSpinner());
-
- AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd);
-
- AddAssert("spinner is completed", () => spinner.Progress >= 1);
- }
-
- private DrawableSpinner createSpinner()
- {
- var spinner = new Spinner
- {
- StartTime = Time.Current + 500,
- EndTime = Time.Current + 2500
- };
- spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
-
- var drawableSpinner = new DrawableSpinner(spinner)
- {
- Anchor = Anchor.Centre
- };
-
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
-
- Add(drawableSpinner);
- return drawableSpinner;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index d6a68abaf2..f3837ea6b1 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 47d765fecd..f080e11933 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -41,7 +41,16 @@ namespace osu.Game.Rulesets.Osu.Mods
var spinner = (DrawableSpinner)drawable;
spinner.RotationTracker.Tracking = true;
- spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
+
+ // early-return if we were paused to avoid division-by-zero in the subsequent calculations.
+ if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
+ return;
+
+ // because the spinner is under the gameplay clock, it is affected by rate adjustments on the track;
+ // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time.
+ // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here.
+ var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
+ spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 854fc4c91c..a438dc8be4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -14,8 +14,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
-using osuTK;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 68516bedf8..a57bb466c7 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -11,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
@@ -30,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable positionBindable = new Bindable();
+ private bool spinnerFrequencyModulate;
+
public DrawableSpinner(Spinner s)
: base(s)
{
@@ -81,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
private SkinnableSound spinningSample;
-
- private const float minimum_volume = 0.0001f;
+ private const float spinning_sample_initial_frequency = 1.0f;
+ private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void LoadSamples()
{
@@ -100,8 +104,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AddInternal(spinningSample = new SkinnableSound(clone)
{
- Volume = { Value = minimum_volume },
+ Volume = { Value = 0 },
Looping = true,
+ Frequency = { Value = spinning_sample_initial_frequency }
});
}
}
@@ -118,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
else
{
- spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
+ spinningSample?.VolumeTo(0, 200).Finally(_ => spinningSample.Stop());
}
}
@@ -172,10 +177,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
}
+ protected override void ApplySkin(ISkinSource skin, bool allowFallback)
+ {
+ base.ApplySkin(skin, allowFallback);
+ spinnerFrequencyModulate = skin.GetConfig(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true;
+ }
+
///
/// The completion progress of this spinner from 0..1 (clamped).
///
- public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ public float Progress
+ {
+ get
+ {
+ if (Spinner.SpinsRequired == 0)
+ // some spinners are so short they can't require an integer spin count.
+ // these become implicitly hit.
+ return 1;
+
+ return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ }
+ }
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
@@ -210,9 +232,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (HandleUserInput)
RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
- if (spinningSample != null)
- // todo: implement SpinnerFrequencyModulate
- spinningSample.Frequency.Value = 0.5f + Progress;
+ if (spinningSample != null && spinnerFrequencyModulate)
+ spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
}
protected override void UpdateAfterChildren()
@@ -221,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
- SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
+ SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation);
updateBonusScore();
}
@@ -233,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (ticks.Count == 0)
return;
- int spins = (int)(RotationTracker.CumulativeRotation / 360);
+ int spins = (int)(RotationTracker.RateAdjustedRotation / 360);
if (spins < wholeSpins)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
index dfb692eba9..1476fe6010 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
@@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
get
{
- int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360);
+ int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index 0cc6c842f4..f1a782cbb5 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -31,17 +31,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public readonly BindableBool Complete = new BindableBool();
///
- /// The total rotation performed on the spinner disc, disregarding the spin direction.
+ /// The total rotation performed on the spinner disc, disregarding the spin direction,
+ /// adjusted for the track's playback rate.
///
///
+ ///
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
+ ///
+ ///
+ /// The rotation from each frame is multiplied by the clock's current playback rate.
+ /// The reason this is done is to ensure that spinners give the same score and require the same number of spins
+ /// regardless of whether speed-modifying mods are applied.
+ ///
///
///
- /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
+ /// Assuming no speed-modifying mods are active,
+ /// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0 for ).
+ /// If Double Time is active instead (with a speed multiplier of 1.5x),
+ /// in the same scenario the property will return 720 * 1.5 = 1080.
///
- public float CumulativeRotation { get; private set; }
+ public float RateAdjustedRotation { get; private set; }
///
/// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
@@ -113,7 +124,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
currentRotation += angle;
- CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
+ // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
+ // (see: ModTimeRamp)
+ RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 619b49926e..1658a4e7c2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
- SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond));
+ SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index e7486ef9b0..0ab3e8825b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning
{
@@ -28,53 +29,67 @@ namespace osu.Game.Rulesets.Osu.Skinning
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
+ private Container circleSprites;
+ private Sprite hitCircleSprite;
+ private Sprite hitCircleOverlay;
+
+ private SkinnableSpriteText hitCircleText;
+
private readonly IBindable state = new Bindable();
private readonly Bindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
+ [Resolved]
+ private ISkinSource skin { get; set; }
+
[BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ private void load(DrawableHitObject drawableObject)
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
- Sprite hitCircleSprite;
- SkinnableSpriteText hitCircleText;
-
InternalChildren = new Drawable[]
{
- hitCircleSprite = new Sprite
+ circleSprites = new Container
{
- Texture = getTextureWithFallback(string.Empty),
- Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ hitCircleSprite = new Sprite
+ {
+ Texture = getTextureWithFallback(string.Empty),
+ Colour = drawableObject.AccentColour.Value,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ hitCircleOverlay = new Sprite
+ {
+ Texture = getTextureWithFallback("overlay"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
},
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
- }, confineMode: ConfineMode.NoScaling),
- new Sprite
+ }, confineMode: ConfineMode.NoScaling)
{
- Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- }
+ },
};
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
- if (!overlayAboveNumber)
- ChangeInternalChildDepth(hitCircleText, -float.MaxValue);
+ if (overlayAboveNumber)
+ AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
- state.BindValueChanged(updateState, true);
-
accentColour.BindTo(drawableObject.AccentColour);
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
-
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
- indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
Texture getTextureWithFallback(string name)
{
@@ -87,6 +102,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ state.BindValueChanged(updateState, true);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+ }
+
private void updateState(ValueChangedEvent state)
{
const double legacy_fade_duration = 240;
@@ -94,8 +118,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
switch (state.NewValue)
{
case ArmedState.Hit:
- this.FadeOut(legacy_fade_duration, Easing.Out);
- this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
+ circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+
+ var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
+
+ if (legacyVersion >= 2.0m)
+ // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
+ hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ else
+ {
+ // old skins scale and fade it normally along other pieces.
+ hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ }
+
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
index 72bc3ddc9a..739c87e037 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
@@ -79,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var spinner = (Spinner)drawableSpinner.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
- this.FadeInFromZero(spinner.TimePreempt / 2);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ this.FadeInFromZero(spinner.TimeFadeIn / 2);
fixedMiddle.FadeColour(Color4.White);
using (BeginAbsoluteSequence(spinner.StartTime, true))
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
index 0ae1d8f683..81a0df5ea5 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var spinner = drawableSpinner.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
- this.FadeInFromZero(spinner.TimePreempt / 2);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 154160fdb5..e034e14eb0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,
- HitCircleOverlayAboveNumer // Some old skins will have this typo
+ HitCircleOverlayAboveNumer, // Some old skins will have this typo
+ SpinnerFrequencyModulate
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
index b4d51d11c9..b2299398e1 100644
--- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
- public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer();
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay();
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 600efefca3..4ef9bbe091 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
- private readonly ApproachCircleProxyContainer approachCircles;
+ private readonly ProxyContainer approachCircles;
+ private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly OrderedHitPolicy hitPolicy;
@@ -38,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.UI
{
InternalChildren = new Drawable[]
{
+ spinnerProxies = new ProxyContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ },
followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
@@ -54,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
Child = HitObjectContainer,
},
- approachCircles = new ApproachCircleProxyContainer
+ approachCircles = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
@@ -76,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.UI
h.OnNewResult += onNewResult;
h.OnLoadComplete += d =>
{
+ if (d is DrawableSpinner)
+ spinnerProxies.Add(d.CreateProxy());
+
if (d is IDrawableHitObjectWithProxiedApproach c)
approachCircles.Add(c.ProxiedLayer.CreateProxy());
};
@@ -113,9 +121,9 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
- private class ApproachCircleProxyContainer : LifetimeManagementContainer
+ private class ProxyContainer : LifetimeManagementContainer
{
- public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy);
+ public void Add(Drawable proxy) => AddInternal(proxy);
}
private class DrawableJudgementPool : DrawablePool
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
index 9c8be868b0..0d1a5a8304 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
@@ -11,10 +11,19 @@ namespace osu.Game.Rulesets.Osu.UI
public class OsuPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
protected override Container Content => content;
- private readonly Container content;
+ private readonly ScalingContainer content;
private const float playfield_size_adjust = 0.8f;
+ ///
+ /// When true, an offset is applied to allow alignment with historical storyboards displayed in the same parent space.
+ /// This will shift the playfield downwards slightly.
+ ///
+ public bool AlignWithStoryboard
+ {
+ set => content.PlayfieldShift = value;
+ }
+
public OsuPlayfieldAdjustmentContainer()
{
Anchor = Anchor.Centre;
@@ -39,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
///
private class ScalingContainer : Container
{
+ internal bool PlayfieldShift { get; set; }
+
protected override void Update()
{
base.Update();
@@ -55,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI
// Scale = 819.2 / 512
// Scale = 1.6
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
+ Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X);
// Size = 0.625
Size = Vector2.Divide(Vector2.One, Scale);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index ada7ac5d74..e896606ee8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs
index e8b8c7c8e9..fc9cbb073e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs
@@ -272,7 +272,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
}
+ [Test]
+ public void TestSelectionResetOnVisibilityChange()
+ {
+ showOverlay();
+ AddStep("Select last button", () => InputManager.Key(Key.Up));
+
+ hideOverlay();
+ showOverlay();
+
+ AddAssert("No button selected",
+ () => pauseOverlay.Buttons.All(button => !button.Selected.Value));
+ }
+
private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show());
+ private void hideOverlay() => AddStep("Hide overlay", () => pauseOverlay.Hide());
private DialogButton getButton(int index) => pauseOverlay.Buttons.Skip(index).First();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 4c73065087..4fac7bb45f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -46,25 +46,36 @@ namespace osu.Game.Tests.Visual.Gameplay
///
/// If the test player should behave like the production one.
/// An action to run before player load but after bindable leases are returned.
- /// An action to run after container load.
- public void ResetPlayer(bool interactive, Action beforeLoadAction = null, Action afterLoadAction = null)
+ public void ResetPlayer(bool interactive, Action beforeLoadAction = null)
{
+ player = null;
+
audioManager.Volume.SetDefault();
InputManager.Clear();
+ container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
+
beforeLoadAction?.Invoke();
+
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToTrack(Beatmap.Value.Track);
- InputManager.Child = container = new TestPlayerLoaderContainer(
- loader = new TestPlayerLoader(() =>
- {
- afterLoadAction?.Invoke();
- return player = new TestPlayer(interactive, interactive);
- }));
+ InputManager.Child = container;
+ }
+
+ [Test]
+ public void TestEarlyExitBeforePlayerConstruction()
+ {
+ AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
+ AddStep("exit loader", () => loader.Exit());
+ AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
+ AddAssert("player did not load", () => player == null);
+ AddUntilStep("player disposed", () => loader.DisposalTask == null);
+ AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1);
}
///
@@ -73,11 +84,12 @@ namespace osu.Game.Tests.Visual.Gameplay
/// speed adjustments were undone too late, causing cross-screen pollution.
///
[Test]
- public void TestEarlyExit()
+ public void TestEarlyExitAfterPlayerConstruction()
{
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
+ AddUntilStep("wait for non-null player", () => player != null);
AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
AddAssert("player did not load", () => !player.IsLoaded);
@@ -94,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load ready", () =>
{
moveMouse();
- return player.LoadState == LoadState.Ready;
+ return player?.LoadState == LoadState.Ready;
});
AddRepeatStep("move mouse", moveMouse, 20);
@@ -195,19 +207,19 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMasterVolume()
{
- addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
+ addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.IsDefault);
}
[Test]
public void TestMutedNotificationTrackVolume()
{
- addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
+ addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.IsDefault);
}
[Test]
public void TestMutedNotificationMuteButton()
{
- addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
+ addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
}
///
@@ -215,14 +227,13 @@ namespace osu.Game.Tests.Visual.Gameplay
///
/// What part of the volume system is checked
/// The action to be invoked to set the volume before loading
- /// The action to be invoked to set the volume after loading
/// The function to be invoked and checked
- private void addVolumeSteps(string volumeName, Action beforeLoad, Action afterLoad, Func assert)
+ private void addVolumeSteps(string volumeName, Action beforeLoad, Func assert)
{
AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false);
- AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad));
- AddUntilStep("wait for player", () => player.LoadState == LoadState.Ready);
+ AddStep("load player", () => ResetPlayer(false, beforeLoad));
+ AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
index c62479faa0..3d225aa0a9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
@@ -16,7 +16,9 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Select;
@@ -145,6 +147,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2);
}
+ ///
+ /// Tests that the same instances are not shared between two playlist items.
+ ///
+ [Test]
+ public void TestNewItemHasNewModInstances()
+ {
+ AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
+ AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2);
+ AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
+
+ AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
+ AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
+ }
+
private class TestMatchSongSelect : MatchSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs
new file mode 100644
index 0000000000..a1251ca793
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Framework.Allocation;
+using osu.Game.Overlays;
+using System;
+using osu.Game.Overlays.Dashboard.Home.News;
+using osuTK;
+using System.Collections.Generic;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneHomeNewsPanel : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ public TestSceneHomeNewsPanel()
+ {
+ Add(new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Y,
+ Width = 500,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5),
+ Children = new Drawable[]
+ {
+ new FeaturedNewsItemPanel(new APINewsPost
+ {
+ Title = "This post has an image which starts with \"/\" and has many authors!",
+ Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
+ PublishedAt = DateTimeOffset.Now,
+ Slug = "2020-07-16-summer-theme-park-2020-voting-open"
+ }),
+ new NewsItemGroupPanel(new List
+ {
+ new APINewsPost
+ {
+ Title = "Title 1",
+ Slug = "2020-07-16-summer-theme-park-2020-voting-open",
+ PublishedAt = DateTimeOffset.Now,
+ },
+ new APINewsPost
+ {
+ Title = "Title of this post is Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ Slug = "2020-07-16-summer-theme-park-2020-voting-open",
+ PublishedAt = DateTimeOffset.Now,
+ }
+ }),
+ new ShowMoreNewsPanel()
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index 3d335995ac..e06b3a8a7e 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -64,5 +64,77 @@ namespace osu.Game.Tests.Visual.Settings
}, 0, true);
});
}
+
+ [Test]
+ public void TestClearButtonOnBindings()
+ {
+ KeyBindingRow backBindingRow = null;
+
+ AddStep("click back binding row", () =>
+ {
+ backBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(backBindingRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ clickClearButton();
+
+ AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().First().Text.Text));
+
+ AddStep("click second binding", () =>
+ {
+ var target = backBindingRow.ChildrenOfType().ElementAt(1);
+
+ InputManager.MoveMouseTo(target);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ clickClearButton();
+
+ AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().ElementAt(1).Text.Text));
+
+ void clickClearButton()
+ {
+ AddStep("click clear button", () =>
+ {
+ var clearButton = backBindingRow.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(clearButton);
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+ }
+
+ [Test]
+ public void TestClickRowSelectsFirstBinding()
+ {
+ KeyBindingRow backBindingRow = null;
+
+ AddStep("click back binding row", () =>
+ {
+ backBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(backBindingRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding);
+
+ AddStep("click second binding", () =>
+ {
+ var target = backBindingRow.ChildrenOfType().ElementAt(1);
+
+ InputManager.MoveMouseTo(target);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("click back binding row", () =>
+ {
+ backBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(backBindingRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
index 010e4330d7..5582cc6826 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -263,7 +264,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void moveLogoFacade()
{
- if (logoFacade?.Transforms.Count == 0 && transferContainer?.Transforms.Count == 0)
+ if (!(logoFacade?.Transforms).Any() && !(transferContainer?.Transforms).Any())
{
Random random = new Random();
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
index 7ff463361a..c5ce3751ef 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -15,6 +16,7 @@ using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.UserInterface
@@ -75,6 +77,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0);
}
+ [Test]
+ public void TestModSettingsUnboundWhenCopied()
+ {
+ OsuModDoubleTime original = null;
+ OsuModDoubleTime copy = null;
+
+ AddStep("create mods", () =>
+ {
+ original = new OsuModDoubleTime();
+ copy = (OsuModDoubleTime)original.CreateCopy();
+ });
+
+ AddStep("change property", () => original.SpeedChange.Value = 2);
+
+ AddAssert("original has new value", () => Precision.AlmostEquals(2.0, original.SpeedChange.Value));
+ AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
+ }
+
private void createModSelect()
{
AddStep("create mod select", () =>
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 4b0506d818..d767973528 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index f256b8e4e9..95f5deb2cc 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs
index c60bd0286e..6c229755e7 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs
@@ -67,19 +67,18 @@ namespace osu.Game.Beatmaps.Drawables
if (beatmapSet != null)
{
- BeatmapSetCover cover;
-
- Add(displayedCover = new DelayedLoadWrapper(
- cover = new BeatmapSetCover(beatmapSet, coverType)
+ Add(displayedCover = new DelayedLoadUnloadWrapper(() =>
+ {
+ var cover = new BeatmapSetCover(beatmapSet, coverType)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
- })
- );
-
- cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
+ };
+ cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
+ return cover;
+ }));
}
}
}
diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
index 387e189dc4..058d2ed0f9 100644
--- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
+++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Graphics.Backgrounds
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(LargeTextureStore textures)
{
Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName);
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 6ae420b162..45b07581ec 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -53,6 +53,7 @@ namespace osu.Game.Input.Bindings
public IEnumerable InGameKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
+ new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index b60d71cfe7..87b283f6b5 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -82,20 +82,10 @@ namespace osu.Game.Online.Leaderboards
Children = new Drawable[]
{
- new Container
+ new RankLabel(rank)
{
RelativeSizeAxes = Axes.Y,
Width = rank_width,
- Children = new[]
- {
- new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Font = OsuFont.GetFont(size: 20, italics: true),
- Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0),
- },
- },
},
content = new Container
{
@@ -356,6 +346,25 @@ namespace osu.Game.Online.Leaderboards
}
}
+ private class RankLabel : Container, IHasTooltip
+ {
+ public RankLabel(int? rank)
+ {
+ if (rank >= 1000)
+ TooltipText = $"#{rank:N0}";
+
+ Child = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.GetFont(size: 20, italics: true),
+ Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0),
+ };
+ }
+
+ public string TooltipText { get; }
+ }
+
public class LeaderboardScoreStatistic
{
public IconUsage Icon;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 26f7c3b93b..053eb01dcd 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -67,8 +67,6 @@ namespace osu.Game
[NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay();
- private NowPlayingOverlay nowPlaying;
-
private BeatmapListingOverlay beatmapListing;
private DashboardOverlay dashboard;
@@ -650,7 +648,7 @@ namespace osu.Game
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
- loadComponentSingleFile(nowPlaying = new NowPlayingOverlay
+ loadComponentSingleFile(new NowPlayingOverlay
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
@@ -685,9 +683,8 @@ namespace osu.Game
{
overlay.State.ValueChanged += state =>
{
- if (state.NewValue == Visibility.Hidden) return;
-
- informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+ if (state.NewValue != Visibility.Hidden)
+ showOverlayAboveOthers(overlay, informationalOverlays);
};
}
@@ -701,9 +698,8 @@ namespace osu.Game
// informational overlays should be dismissed on a show or hide of a full overlay.
informationalOverlays.ForEach(o => o.Hide());
- if (state.NewValue == Visibility.Hidden) return;
-
- singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+ if (state.NewValue != Visibility.Hidden)
+ showOverlayAboveOthers(overlay, singleDisplayOverlays);
};
}
@@ -728,6 +724,15 @@ namespace osu.Game
notifications.State.ValueChanged += _ => updateScreenOffset();
}
+ private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
+ {
+ otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+
+ // show above others if not visible at all, else leave at current depth.
+ if (!overlay.IsPresent)
+ overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
+ }
+
public class GameIdleTracker : IdleTracker
{
private InputManager inputManager;
@@ -862,18 +867,6 @@ namespace osu.Game
switch (action)
{
- case GlobalAction.ToggleNowPlaying:
- nowPlaying.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleChat:
- chatOverlay.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleSocial:
- dashboard.ToggleVisibility();
- return true;
-
case GlobalAction.ResetInputSettings:
var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity);
@@ -889,18 +882,6 @@ namespace osu.Game
Toolbar.ToggleVisibility();
return true;
- case GlobalAction.ToggleSettings:
- Settings.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleDirect:
- beatmapListing.ToggleVisibility();
- return true;
-
- case GlobalAction.ToggleNotifications:
- notifications.ToggleVisibility();
- return true;
-
case GlobalAction.ToggleGameplayMouseButtons:
LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons));
return true;
diff --git a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs
new file mode 100644
index 0000000000..ce053cd4ec
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Dashboard.Home
+{
+ public class HomePanel : Container
+ {
+ protected override Container Content => content;
+
+ private readonly Container content;
+ private readonly Box background;
+
+ public HomePanel()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Masking = true;
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.25f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 3,
+ Offset = new Vector2(0, 1)
+ };
+
+ AddRangeInternal(new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ background.Colour = colourProvider.Background4;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs
new file mode 100644
index 0000000000..ee88469e2f
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs
@@ -0,0 +1,195 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Platform;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.News;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Dashboard.Home.News
+{
+ public class FeaturedNewsItemPanel : HomePanel
+ {
+ private readonly APINewsPost post;
+
+ public FeaturedNewsItemPanel(APINewsPost post)
+ {
+ this.post = post;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new ClickableNewsBackground(post),
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, size: 60),
+ new Dimension(GridSizeMode.Absolute, size: 20),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Date(post.PublishedAt),
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Vertical = 10 },
+ Child = new Box
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ Colour = colourProvider.Light1
+ }
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Margin = new MarginPadding { Top = 5, Bottom = 10 },
+ Padding = new MarginPadding { Right = 10 },
+ Spacing = new Vector2(0, 10),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new NewsTitleLink(post),
+ new TextFlowContainer(f =>
+ {
+ f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
+ })
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = post.Preview
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ }
+
+ private class ClickableNewsBackground : OsuHoverContainer
+ {
+ private readonly APINewsPost post;
+
+ public ClickableNewsBackground(APINewsPost post)
+ {
+ this.post = post;
+
+ RelativeSizeAxes = Axes.X;
+ Height = 130;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host)
+ {
+ NewsPostBackground bg;
+
+ Child = new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage)
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fill,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0
+ })
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+
+ bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
+
+ TooltipText = "view in browser";
+ Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
+
+ HoverColour = Color4.White;
+ }
+ }
+
+ private class Date : CompositeDrawable, IHasCustomTooltip
+ {
+ public ITooltip GetCustomTooltip() => new DateTooltip();
+
+ public object TooltipContent => date;
+
+ private readonly DateTimeOffset date;
+
+ public Date(DateTimeOffset date)
+ {
+ this.date = date;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ AutoSizeAxes = Axes.Both;
+ Anchor = Anchor.TopRight;
+ Origin = Anchor.TopRight;
+ Margin = new MarginPadding { Top = 10 };
+ InternalChild = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative
+ Colour = colourProvider.Light1,
+ Text = $"{date:dd}"
+ },
+ new TextFlowContainer(f =>
+ {
+ f.Font = OsuFont.GetFont(size: 11, weight: FontWeight.Regular);
+ f.Colour = colourProvider.Light1;
+ })
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Text = $"{date:MMM yyyy}"
+ }
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs
new file mode 100644
index 0000000000..dc4f3f8c92
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs
@@ -0,0 +1,115 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Overlays.Dashboard.Home.News
+{
+ public class NewsGroupItem : CompositeDrawable
+ {
+ private readonly APINewsPost post;
+
+ public NewsGroupItem(APINewsPost post)
+ {
+ this.post = post;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ InternalChild = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, size: 60),
+ new Dimension(GridSizeMode.Absolute, size: 20),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Date(post.PublishedAt),
+ new Box
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ Colour = colourProvider.Light1
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Padding = new MarginPadding { Right = 10 },
+ Child = new NewsTitleLink(post)
+ }
+ }
+ }
+ };
+ }
+
+ private class Date : CompositeDrawable, IHasCustomTooltip
+ {
+ public ITooltip GetCustomTooltip() => new DateTooltip();
+
+ public object TooltipContent => date;
+
+ private readonly DateTimeOffset date;
+
+ public Date(DateTimeOffset date)
+ {
+ this.date = date;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ TextFlowContainer textFlow;
+
+ AutoSizeAxes = Axes.Both;
+ Anchor = Anchor.TopRight;
+ Origin = Anchor.TopRight;
+ InternalChild = textFlow = new TextFlowContainer(t =>
+ {
+ t.Colour = colourProvider.Light1;
+ })
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Margin = new MarginPadding { Vertical = 5 }
+ };
+
+ textFlow.AddText($"{date:dd}", t =>
+ {
+ t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
+ });
+
+ textFlow.AddText($"{date: MMM}", t =>
+ {
+ t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular);
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs
new file mode 100644
index 0000000000..c1d5a87ef5
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Overlays.Dashboard.Home.News
+{
+ public class NewsItemGroupPanel : HomePanel
+ {
+ private readonly List posts;
+
+ public NewsItemGroupPanel(List posts)
+ {
+ this.posts = posts;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Content.Padding = new MarginPadding { Vertical = 5 };
+
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = posts.Select(p => new NewsGroupItem(p)).ToArray()
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs
new file mode 100644
index 0000000000..d6a3a69fe0
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Platform;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Overlays.Dashboard.Home.News
+{
+ public class NewsTitleLink : OsuHoverContainer
+ {
+ private readonly APINewsPost post;
+
+ public NewsTitleLink(APINewsPost post)
+ {
+ this.post = post;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OverlayColourProvider colourProvider)
+ {
+ Child = new TextFlowContainer(t =>
+ {
+ t.Font = OsuFont.GetFont(weight: FontWeight.Bold);
+ })
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = post.Title
+ };
+
+ HoverColour = colourProvider.Light1;
+
+ TooltipText = "view in browser";
+ Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Home/News/ShowMoreNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/ShowMoreNewsPanel.cs
new file mode 100644
index 0000000000..d25df6f189
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/Home/News/ShowMoreNewsPanel.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Dashboard.Home.News
+{
+ public class ShowMoreNewsPanel : OsuHoverContainer
+ {
+ protected override IEnumerable EffectTargets => new[] { text };
+
+ [Resolved(canBeNull: true)]
+ private NewsOverlay overlay { get; set; }
+
+ private OsuSpriteText text;
+
+ public ShowMoreNewsPanel()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ Child = new HomePanel
+ {
+ Child = text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Vertical = 20 },
+ Text = "see more"
+ }
+ };
+
+ IdleColour = colourProvider.Light1;
+ HoverColour = Color4.White;
+
+ Action = () =>
+ {
+ overlay?.ShowFrontPage();
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index eafb4572ca..b808d49fa2 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -48,8 +48,7 @@ namespace osu.Game.Overlays.KeyBinding
public bool FilteringActive { get; set; }
private OsuSpriteText text;
- private Drawable pressAKey;
-
+ private FillFlowContainer cancelAndClearButtons;
private FillFlowContainer buttons;
public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text);
@@ -80,7 +79,7 @@ namespace osu.Game.Overlays.KeyBinding
Hollow = true,
};
- Children = new[]
+ Children = new Drawable[]
{
new Box
{
@@ -99,7 +98,7 @@ namespace osu.Game.Overlays.KeyBinding
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
- pressAKey = new FillFlowContainer
+ cancelAndClearButtons = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(padding) { Top = height + padding * 2 },
@@ -187,7 +186,8 @@ namespace osu.Game.Overlays.KeyBinding
if (bindTarget.IsHovered)
finalise();
- else
+ // prevent updating bind target before clear button's action
+ else if (!cancelAndClearButtons.Any(b => b.IsHovered))
updateBindTarget();
}
@@ -298,8 +298,8 @@ namespace osu.Game.Overlays.KeyBinding
if (HasFocus)
GetContainingInputManager().ChangeFocus(null);
- pressAKey.FadeOut(300, Easing.OutQuint);
- pressAKey.BypassAutoSizeAxes |= Axes.Y;
+ cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
+ cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;
}
protected override void OnFocus(FocusEvent e)
@@ -307,8 +307,8 @@ namespace osu.Game.Overlays.KeyBinding
AutoSizeDuration = 500;
AutoSizeEasing = Easing.OutQuint;
- pressAKey.FadeIn(300, Easing.OutQuint);
- pressAKey.BypassAutoSizeAxes &= ~Axes.Y;
+ cancelAndClearButtons.FadeIn(300, Easing.OutQuint);
+ cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y;
updateBindTarget();
base.OnFocus(e);
@@ -320,6 +320,9 @@ namespace osu.Game.Overlays.KeyBinding
base.OnFocusLost(e);
}
+ ///
+ /// Updates the bind target to the currently hovered key button or the first if clicked anywhere else.
+ ///
private void updateBindTarget()
{
if (bindTarget != null) bindTarget.IsBinding = false;
@@ -354,7 +357,7 @@ namespace osu.Game.Overlays.KeyBinding
}
}
- private class KeyButton : Container
+ public class KeyButton : Container
{
public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs
index 201c3ce826..599b45fa78 100644
--- a/osu.Game/Overlays/News/NewsCard.cs
+++ b/osu.Game/Overlays/News/NewsCard.cs
@@ -9,8 +9,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Game.Graphics;
@@ -48,7 +46,7 @@ namespace osu.Game.Overlays.News
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
}
- NewsBackground bg;
+ NewsPostBackground bg;
AddRange(new Drawable[]
{
background = new Box
@@ -70,7 +68,7 @@ namespace osu.Game.Overlays.News
CornerRadius = 6,
Children = new Drawable[]
{
- new DelayedLoadWrapper(bg = new NewsBackground(post.FirstImage)
+ new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
@@ -123,34 +121,6 @@ namespace osu.Game.Overlays.News
main.AddText(post.Author, t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold));
}
- [LongRunningLoad]
- private class NewsBackground : Sprite
- {
- private readonly string sourceUrl;
-
- public NewsBackground(string sourceUrl)
- {
- this.sourceUrl = sourceUrl;
- }
-
- [BackgroundDependencyLoader]
- private void load(LargeTextureStore store)
- {
- Texture = store.Get(createUrl(sourceUrl));
- }
-
- private string createUrl(string source)
- {
- if (string.IsNullOrEmpty(source))
- return "Headers/news";
-
- if (source.StartsWith('/'))
- return "https://osu.ppy.sh" + source;
-
- return source;
- }
- }
-
private class DateContainer : CircularContainer, IHasCustomTooltip
{
public ITooltip GetCustomTooltip() => new DateTooltip();
diff --git a/osu.Game/Overlays/News/NewsPostBackground.cs b/osu.Game/Overlays/News/NewsPostBackground.cs
new file mode 100644
index 0000000000..386ef7f669
--- /dev/null
+++ b/osu.Game/Overlays/News/NewsPostBackground.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Overlays.News
+{
+ [LongRunningLoad]
+ public class NewsPostBackground : Sprite
+ {
+ private readonly string sourceUrl;
+
+ public NewsPostBackground(string sourceUrl)
+ {
+ this.sourceUrl = sourceUrl;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LargeTextureStore store)
+ {
+ Texture = store.Get(createUrl(sourceUrl));
+ }
+
+ private string createUrl(string source)
+ {
+ if (string.IsNullOrEmpty(source))
+ return "Headers/news";
+
+ if (source.StartsWith('/'))
+ return "https://osu.ppy.sh" + source;
+
+ return source;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs
index a5687b77e2..09fb445b1f 100644
--- a/osu.Game/Overlays/NewsOverlay.cs
+++ b/osu.Game/Overlays/NewsOverlay.cs
@@ -67,7 +67,28 @@ namespace osu.Game.Overlays
protected override void LoadComplete()
{
base.LoadComplete();
- article.BindValueChanged(onArticleChanged, true);
+
+ // should not be run until first pop-in to avoid requesting data before user views.
+ article.BindValueChanged(onArticleChanged);
+ }
+
+ private bool displayUpdateRequired = true;
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ if (displayUpdateRequired)
+ {
+ article.TriggerChange();
+ displayUpdateRequired = false;
+ }
+ }
+
+ protected override void PopOutComplete()
+ {
+ base.PopOutComplete();
+ displayUpdateRequired = true;
}
public void ShowFrontPage()
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 5bdd86c671..beac6adc59 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -118,9 +118,9 @@ namespace osu.Game.Overlays.Toolbar
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Alpha = 0,
- Height = 90,
+ Height = 100,
Colour = ColourInfo.GradientVertical(
- OsuColour.Gray(0.1f).Opacity(0.5f), OsuColour.Gray(0.1f).Opacity(0)),
+ OsuColour.Gray(0).Opacity(0.9f), OsuColour.Gray(0).Opacity(0)),
},
};
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs
index eecb368ee9..cde305fffd 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Game.Graphics;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -11,6 +12,10 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarBeatmapListingButton()
{
SetIcon(OsuIcon.ChevronDownCircle);
+ TooltipMain = "Beatmap listing";
+ TooltipSub = "Browse for new beatmaps";
+
+ Hotkey = GlobalAction.ToggleDirect;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 86a3f5d8aa..0afc6642b2 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -1,27 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Input;
+using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Toolbar
{
- public class ToolbarButton : OsuClickableContainer
+ public abstract class ToolbarButton : OsuClickableContainer, IKeyBindingHandler
{
public const float WIDTH = Toolbar.HEIGHT * 1.4f;
+ protected GlobalAction? Hotkey { get; set; }
+
public void SetIcon(Drawable icon)
{
IconContainer.Icon = icon;
@@ -66,9 +73,13 @@ namespace osu.Game.Overlays.Toolbar
private readonly FillFlowContainer tooltipContainer;
private readonly SpriteText tooltip1;
private readonly SpriteText tooltip2;
+ private readonly SpriteText keyBindingTooltip;
protected FillFlowContainer Flow;
- public ToolbarButton()
+ [Resolved]
+ private KeyBindingStore keyBindings { get; set; }
+
+ protected ToolbarButton()
: base(HoverSampleSet.Loud)
{
Width = WIDTH;
@@ -123,7 +134,7 @@ namespace osu.Game.Overlays.Toolbar
Origin = TooltipAnchor,
Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),
Alpha = 0,
- Children = new[]
+ Children = new Drawable[]
{
tooltip1 = new OsuSpriteText
{
@@ -132,17 +143,44 @@ namespace osu.Game.Overlays.Toolbar
Shadow = true,
Font = OsuFont.GetFont(size: 22, weight: FontWeight.Bold),
},
- tooltip2 = new OsuSpriteText
+ new FillFlowContainer
{
+ AutoSizeAxes = Axes.Both,
Anchor = TooltipAnchor,
Origin = TooltipAnchor,
- Shadow = true,
+ Direction = FillDirection.Horizontal,
+ Children = new[]
+ {
+ tooltip2 = new OsuSpriteText { Shadow = true },
+ keyBindingTooltip = new OsuSpriteText { Shadow = true }
+ }
}
}
}
};
}
+ private readonly Cached tooltipKeyBinding = new Cached();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate();
+ updateKeyBindingTooltip();
+ }
+
+ private void updateKeyBindingTooltip()
+ {
+ if (tooltipKeyBinding.IsValid)
+ return;
+
+ var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey);
+ var keyBindingString = binding?.KeyCombination.ReadableString();
+ keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty;
+
+ tooltipKeyBinding.Validate();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e)
@@ -154,6 +192,8 @@ namespace osu.Game.Overlays.Toolbar
protected override bool OnHover(HoverEvent e)
{
+ updateKeyBindingTooltip();
+
HoverBackground.FadeIn(200);
tooltipContainer.FadeIn(100);
return base.OnHover(e);
@@ -164,6 +204,21 @@ namespace osu.Game.Overlays.Toolbar
HoverBackground.FadeOut(200);
tooltipContainer.FadeOut(100);
}
+
+ public bool OnPressed(GlobalAction action)
+ {
+ if (action == Hotkey)
+ {
+ Click();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(GlobalAction action)
+ {
+ }
}
public class OpaqueBackground : Container
diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs
index 84210e27a4..c88b418853 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarChangelogButton()
{
SetIcon(FontAwesome.Solid.Bullhorn);
+ TooltipMain = "Changelog";
+ TooltipSub = "Track recent dev updates in the osu! ecosystem";
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs
index ad0e5be551..dee4be0c1f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -11,6 +12,10 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarChatButton()
{
SetIcon(FontAwesome.Solid.Comments);
+ TooltipMain = "Chat";
+ TooltipSub = "Join the real-time discussion";
+
+ Hotkey = GlobalAction.ToggleChat;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs
index e642f0c453..4845c9a99f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs
@@ -2,33 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
- public class ToolbarHomeButton : ToolbarButton, IKeyBindingHandler
+ public class ToolbarHomeButton : ToolbarButton
{
public ToolbarHomeButton()
{
Icon = FontAwesome.Solid.Home;
TooltipMain = "Home";
TooltipSub = "Return to the main menu";
- }
- public bool OnPressed(GlobalAction action)
- {
- if (action == GlobalAction.Home)
- {
- Click();
- return true;
- }
-
- return false;
- }
-
- public void OnReleased(GlobalAction action)
- {
+ Hotkey = GlobalAction.Home;
}
}
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs
index b29aec5842..f9aa2de497 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs
@@ -2,15 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarMusicButton : ToolbarOverlayToggleButton
{
+ protected override Anchor TooltipAnchor => Anchor.TopRight;
+
public ToolbarMusicButton()
{
Icon = FontAwesome.Solid.Music;
+ TooltipMain = "Now playing";
+ TooltipSub = "Manage the currently playing track";
+
+ Hotkey = GlobalAction.ToggleNowPlaying;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs
index e813a3f4cb..106c67a041 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarNewsButton()
{
Icon = FontAwesome.Solid.Newspaper;
+ TooltipMain = "News";
+ TooltipSub = "Get up-to-date on community happenings";
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
index dbd6c557d3..a699fd907f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Toolbar
TooltipMain = "Notifications";
TooltipSub = "Waiting for 'ya";
+ Hotkey = GlobalAction.ToggleNotifications;
+
Add(countDisplay = new CountCircle
{
Alpha = 0,
diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs
index cbd097696d..c026ce99fe 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarRankingsButton()
{
SetIcon(FontAwesome.Regular.ChartBar);
+ TooltipMain = "Ranking";
+ TooltipSub = "Find out who's the best right now";
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs
index 79942012f9..ed2a23ec2a 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar
Icon = FontAwesome.Solid.Cog;
TooltipMain = "Settings";
TooltipSub = "Change your settings";
+
+ Hotkey = GlobalAction.ToggleSettings;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
index f6646eb81d..6faa58c559 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
@@ -11,6 +12,10 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarSocialButton()
{
Icon = FontAwesome.Solid.Users;
+ TooltipMain = "Friends";
+ TooltipSub = "Interact with those close to you";
+
+ Hotkey = GlobalAction.ToggleSocial;
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 0e5fe3fc9c..52ffa0ad2a 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
@@ -126,7 +127,25 @@ namespace osu.Game.Rulesets.Mods
///
/// Creates a copy of this initialised to a default state.
///
- public virtual Mod CreateCopy() => (Mod)MemberwiseClone();
+ public virtual Mod CreateCopy()
+ {
+ var copy = (Mod)Activator.CreateInstance(GetType());
+
+ // Copy bindable values across
+ foreach (var (_, prop) in this.GetSettingsSourceProperties())
+ {
+ var origBindable = prop.GetValue(this);
+ var copyBindable = prop.GetValue(copy);
+
+ // The bindables themselves are readonly, so the value must be transferred through the Bindable.Value property.
+ var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable
-
-
+
+
@@ -80,7 +80,7 @@
-
+