diff --git a/.editorconfig b/.editorconfig
index 67f98f94eb..a5f7795882 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -191,4 +191,7 @@ dotnet_diagnostic.IDE0052.severity = silent
#Rules for disposable
dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none
-dotnet_diagnostic.IDE0069.severity = none
\ No newline at end of file
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
\ No newline at end of file
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 @@
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](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/fastlane/Fastfile b/fastlane/Fastfile
index 4fd0e5e8c7..8c278604aa 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -113,7 +113,7 @@ platform :ios do
souyuz(
platform: "ios",
- plist_path: "../osu.iOS/Info.plist"
+ plist_path: "osu.iOS/Info.plist"
)
end
@@ -127,7 +127,7 @@ platform :ios do
end
lane :update_version do |options|
- options[:plist_path] = '../osu.iOS/Info.plist'
+ options[:plist_path] = 'osu.iOS/Info.plist'
app_version(options)
end
diff --git a/global.json b/global.json
index 233a040d18..a9a531f59c 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.52"
+ "Microsoft.Build.Traversal": "2.1.1"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index a384ad4c34..2d3bfaf7ce 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/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index df54df7b01..8c48158acd 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
+ [TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
new file mode 100644
index 0000000000..1eb0975010
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
@@ -0,0 +1,84 @@
+// 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 NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ public class TestSceneCatchModRelax : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestModRelax() => CreateModTest(new ModTestData
+ {
+ Mod = new CatchModRelax(),
+ Autoplay = false,
+ PassCondition = passCondition,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X,
+ StartTime = 0
+ },
+ new Fruit
+ {
+ X = 0,
+ StartTime = 250
+ },
+ new Fruit
+ {
+ X = CatchPlayfield.WIDTH,
+ StartTime = 500
+ },
+ new JuiceStream
+ {
+ X = CatchPlayfield.CENTER_X,
+ StartTime = 750,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
+ }
+ }
+ }
+ });
+
+ private bool passCondition()
+ {
+ var playfield = this.ChildrenOfType().Single();
+
+ switch (Player.ScoreProcessor.Combo.Value)
+ {
+ case 0:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
+ break;
+
+ case 1:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft);
+ break;
+
+ case 2:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight);
+ break;
+
+ case 3:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
+ break;
+ }
+
+ return Player.ScoreProcessor.Combo.Value >= 6;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index d6bba3d55e..3c636a5b97 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -1,7 +1,9 @@
// 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.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -38,7 +40,11 @@ namespace osu.Game.Rulesets.Catch.Tests
new Vector2(width, 0)
}),
StartTime = i * 2000,
- NewCombo = i % 8 == 0
+ NewCombo = i % 8 == 0,
+ Samples = new List(new[]
+ {
+ new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 }
+ })
});
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index c07e4fdad3..385d8ed7fa 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -20,19 +20,19 @@ namespace osu.Game.Rulesets.Catch.Tests
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
- AddStep("show droplet", () => SetContents(createDrawableDroplet));
-
+ AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
+
+ AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
}
private Drawable createDrawableTinyDroplet()
{
- var droplet = new TinyDroplet
+ var droplet = new TestCatchTinyDroplet
{
- StartTime = Clock.CurrentTime,
Scale = 1.5f,
};
@@ -47,12 +47,12 @@ namespace osu.Game.Rulesets.Catch.Tests
};
}
- private Drawable createDrawableDroplet()
+ private Drawable createDrawableDroplet(bool hyperdash = false)
{
- var droplet = new Droplet
+ var droplet = new TestCatchDroplet
{
- StartTime = Clock.CurrentTime,
Scale = 1.5f,
+ HyperDashTarget = hyperdash ? new Banana() : null
};
return new DrawableDroplet(droplet)
@@ -95,5 +95,21 @@ namespace osu.Game.Rulesets.Catch.Tests
public override FruitVisualRepresentation VisualRepresentation { get; }
}
+
+ public class TestCatchDroplet : Droplet
+ {
+ public TestCatchDroplet()
+ {
+ StartTime = 1000000000000;
+ }
+ }
+
+ public class TestCatchTinyDroplet : TinyDroplet
+ {
+ public TestCatchTinyDroplet()
+ {
+ StartTime = 1000000000000;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index ad24adf352..db09b2bc6b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@@ -18,23 +19,43 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override bool Autoplay => true;
+ private int hyperDashCount;
+ private bool inHyperDash;
+
[Test]
public void TestHyperDash()
{
- AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
- AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast.
-
- AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0);
-
- for (int i = 0; i < 3; i++)
+ AddStep("reset count", () =>
{
- AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing);
- AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing);
+ inHyperDash = false;
+ hyperDashCount = 0;
+
+ // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
+ Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
+ {
+ var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher;
+
+ if (catcher == null)
+ return;
+
+ if (catcher.HyperDashing != inHyperDash)
+ {
+ inHyperDash = catcher.HyperDashing;
+ if (catcher.HyperDashing)
+ hyperDashCount++;
+ }
+ };
+ });
+
+ AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
+
+ for (int i = 0; i < 9; i++)
+ {
+ int count = i + 1;
+ AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count);
}
}
- private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher;
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -46,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
// Should produce a hyper-dash (edge case test)
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
@@ -63,6 +86,20 @@ namespace osu.Game.Rulesets.Catch.Tests
createObjects(() => new Fruit { X = right_x });
createObjects(() => new TestJuiceStream(left_x), 1);
+ beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint
+ {
+ BeatLength = 50
+ });
+
+ createObjects(() => new TestJuiceStream(left_x)
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(new Vector2(512, 0))
+ })
+ }, 1);
+
return beatmap;
void createObjects(Func createObject, int count = 3)
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 f9d56dfa78..dfe3bf8af4 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.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index bb14988414..a08c5b6fb1 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (amount > 0)
{
// Clamp to the right bound
- if (position + amount < 1)
+ if (position + amount < CatchPlayfield.WIDTH)
position += amount;
}
else
@@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
+
+ // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
+ // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
+ // For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size.
+ halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE;
+
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index c1d24395e4..1e42c6a240 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
- catcher.UpdatePosition(e.MousePosition.X / DrawSize.X);
+ catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
return base.OnMouseMove(e);
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 04932ecdbb..5985ec9b68 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects
set => x = value;
}
+ ///
+ /// Whether this object can be placed on the catcher's plate.
+ ///
+ public virtual bool CanBePlated => false;
+
///
/// A random offset applied to , set by the .
///
@@ -100,6 +105,14 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
+ ///
+ /// Represents a single object that can be caught by the catcher.
+ ///
+ public abstract class PalpableCatchHitObject : CatchHitObject
+ {
+ public override bool CanBePlated => true;
+ }
+
public enum FruitVisualRepresentation
{
Pear,
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index c6345a9df7..2fe017dc62 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -15,14 +15,12 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public abstract class PalpableCatchHitObject : DrawableCatchHitObject
- where TObject : CatchHitObject
+ public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject
+ where TObject : PalpableCatchHitObject
{
- public override bool CanBePlated => true;
-
protected Container ScaleContainer { get; private set; }
- protected PalpableCatchHitObject(TObject hitObject)
+ protected PalpableDrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
Origin = Anchor.Centre;
@@ -65,9 +63,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public abstract class DrawableCatchHitObject : DrawableHitObject
{
- public virtual bool CanBePlated => false;
-
- public virtual bool StaysOnPlate => CanBePlated;
+ public virtual bool StaysOnPlate => HitObject.CanBePlated;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
index cad8892283..688240fd86 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -4,12 +4,11 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableDroplet : PalpableCatchHitObject
+ public class DrawableDroplet : PalpableDrawableCatchHitObject
{
public override bool StaysOnPlate => false;
@@ -21,11 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new Pulp
- {
- Size = Size / 4,
- AccentColour = { BindTarget = AccentColour }
- });
+ ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece());
}
protected override void UpdateInitialTransforms()
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index fae5a10d04..c1c34e4157 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -8,7 +8,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableFruit : PalpableCatchHitObject
+ public class DrawableFruit : PalpableDrawableCatchHitObject
{
public DrawableFruit(Fruit h)
: base(h)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs
new file mode 100644
index 0000000000..c2499446fa
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs
@@ -0,0 +1,69 @@
+// 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.Graphics.Shapes;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class DropletPiece : CompositeDrawable
+ {
+ public DropletPiece()
+ {
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject)
+ {
+ DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
+ var hitObject = drawableCatchObject.HitObject;
+
+ InternalChild = new Pulp
+ {
+ RelativeSizeAxes = Axes.Both,
+ AccentColour = { BindTarget = drawableObject.AccentColour }
+ };
+
+ if (hitObject.HyperDash)
+ {
+ AddInternal(new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(2f),
+ Depth = 1,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ BorderThickness = 6,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0.3f,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
index 7ac9f11ad6..4bffdab3d8 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -21,11 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public const float RADIUS_ADJUST = 1.1f;
private Circle border;
-
private CatchHitObject hitObject;
- private readonly IBindable accentColour = new Bindable();
-
public FruitPiece()
{
RelativeSizeAxes = Axes.Both;
@@ -37,8 +33,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
hitObject = drawableCatchObject.HitObject;
- accentColour.BindTo(drawableCatchObject.AccentColour);
-
AddRangeInternal(new[]
{
getFruitFor(drawableCatchObject.HitObject.VisualRepresentation),
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
index 1e7506a257..d3e4945611 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Radius = Size.X / 2,
+ Radius = DrawWidth / 2,
Colour = colour.NewValue.Darken(0.2f).Opacity(0.75f)
};
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
index 7b0bb3f0ae..9c1004a04b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Droplet : CatchHitObject
+ public class Droplet : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchDropletJudgement();
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
index 6f0423b420..43486796ad 100644
--- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Fruit : CatchHitObject
+ public class Fruit : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchJudgement();
}
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
new file mode 100644
index 0000000000..3bde97070c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
@@ -0,0 +1,17 @@
+{
+ "Mappings": [{
+ "StartTime": 3368,
+ "Objects": [{
+ "StartTime": 3368,
+ "Position": 374
+ }]
+ },
+ {
+ "StartTime": 3501,
+ "Objects": [{
+ "StartTime": 3501,
+ "Position": 446
+ }]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu
new file mode 100644
index 0000000000..6630f369d5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu
@@ -0,0 +1,20 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.7
+Mode: 2
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:9.6
+ApproachRate:9.6
+SliderMultiplier:1.9
+SliderTickRate:1
+
+[TimingPoints]
+2169,266.666666666667,4,2,1,70,1,0
+
+[HitObjects]
+374,60,3368,1,0,0:0:0:0:
+410,146,3501,1,2,0:1:0:0:
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index d929da1a29..ea2f031d65 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning
{
@@ -61,7 +62,12 @@ namespace osu.Game.Rulesets.Catch.Skinning
switch (lookup)
{
case CatchSkinColour colour:
- return Source.GetConfig(new SkinCustomColourLookup(colour));
+ var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour));
+ if (result == null)
+ return null;
+
+ result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
+ return (IBindable)result;
}
return Source.GetConfig(lookup);
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
index 5be54d3882..381d066750 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning
colouredSprite = new Sprite
{
Texture = skin.GetTexture(lookupName),
- Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
@@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
base.LoadComplete();
- accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true);
+ accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index 8ee23461ba..efc1b24ed5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -10,15 +10,21 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
+ private const float playfield_size_adjust = 0.8f;
+
protected override Container Content => content;
private readonly Container content;
public CatchPlayfieldAdjustmentContainer()
{
- Anchor = Anchor.TopCentre;
- Origin = Anchor.TopCentre;
+ // because we are using centre anchor/origin, we will need to limit visibility in the future
+ // to ensure tall windows do not get a readability advantage.
+ // it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values
+ // which are compatible with TopCentre alignment.
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
- Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate
+ Size = new Vector2(playfield_size_adjust);
InternalChild = new Container
{
@@ -27,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
- Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
+ Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both, }
};
}
@@ -40,8 +46,14 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
+ // in stable, fruit fall vertically from -100 to 340.
+ // to emulate this, we want to make our playfield 440 gameplay pixels high.
+ // we then offset it -100 vertically in the position set below.
+ const float stable_v_offset_ratio = 440 / 384f;
+
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
- Size = Vector2.Divide(Vector2.One, Scale);
+ Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X);
+ Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 8820dff730..9289a6162c 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
///
- private const float allowed_catch_range = 0.8f;
+ public const float ALLOWED_CATCH_RANGE = 0.8f;
///
/// The drawable catcher for .
@@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// The scale of the catcher.
internal static float CalculateCatchWidth(Vector2 scale)
- => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range;
+ => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
///
/// Calculates the width of the area used for attempting catches in gameplay.
@@ -216,6 +216,9 @@ namespace osu.Game.Rulesets.Catch.UI
/// Whether the catch is possible.
public bool AttemptCatch(CatchHitObject fruit)
{
+ if (!fruit.CanBePlated)
+ return false;
+
var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
@@ -226,9 +229,8 @@ namespace osu.Game.Rulesets.Catch.UI
catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
- // only update hyperdash state if we are catching a fruit.
- // exceptions are Droplets and JuiceStreams.
- if (!(fruit is Fruit)) return validCatch;
+ // only update hyperdash state if we are not catching a tiny droplet.
+ if (fruit is TinyDroplet) return validCatch;
if (validCatch && fruit.HyperDash)
{
@@ -283,8 +285,6 @@ namespace osu.Game.Rulesets.Catch.UI
private void runHyperDashStateTransition(bool hyperDashing)
{
- trails.HyperDashTrailsColour = hyperDashColour;
- trails.EndGlowSpritesColour = hyperDashEndGlowColour;
updateTrailVisibility();
if (hyperDashing)
@@ -401,6 +401,9 @@ namespace osu.Game.Rulesets.Catch.UI
skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ??
hyperDashColour;
+ trails.HyperDashTrailsColour = hyperDashColour;
+ trails.EndGlowSpritesColour = hyperDashEndGlowColour;
+
runHyperDashStateTransition(HyperDashing);
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 4255c3b1af..03ebf01b9b 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.UI
lastPlateableFruit.OnLoadComplete += _ => action();
}
- if (result.IsHit && fruit.CanBePlated)
+ if (result.IsHit && fruit.HitObject.CanBePlated)
{
// create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index bab3cb748b..f7e9fd19a7 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly Container hyperDashTrails;
private readonly Container endGlowSprites;
- private Color4 hyperDashTrailsColour;
+ private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
public Color4 HyperDashTrailsColour
{
@@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Catch.UI
return;
hyperDashTrailsColour = value;
- hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ hyperDashTrails.Colour = hyperDashTrailsColour;
}
}
- private Color4 endGlowSpritesColour;
+ private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
public Color4 EndGlowSpritesColour
{
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI
return;
endGlowSpritesColour = value;
- endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ endGlowSprites.Colour = endGlowSpritesColour;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
index 957743c5f1..b22687a0a7 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
+ [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs
new file mode 100644
index 0000000000..f2cc254e38
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public class TestSceneManiaModInvert : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Test]
+ public void TestInversion() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModInvert(),
+ PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
index ff4865c71d..8ba58e3af3 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
@@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached]
private readonly Column column;
- public ColumnTestContainer(int column, ManiaAction action)
+ public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{
- this.column = new Column(column)
+ InternalChildren = new[]
{
- Action = { Value = action },
- AccentColour = Color4.Orange,
- ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd
- };
-
- InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
- {
- RelativeSizeAxes = Axes.Both
+ this.column = new Column(column)
+ {
+ Action = { Value = action },
+ AccentColour = Color4.Orange,
+ ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
+ Alpha = showColumn ? 1 : 0
+ },
+ content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ this.column.TopLevelContainer.CreateProxy()
};
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index 18eebada00..d24c81dac6 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
- new ColumnTestContainer(0, ManiaAction.Key1)
+ new ColumnTestContainer(0, ManiaAction.Key1, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
}));
})
},
- new ColumnTestContainer(1, ManiaAction.Key2)
+ new ColumnTestContainer(1, ManiaAction.Key2, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
index 87c84cf89c..a15fb392d6 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning;
@@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
+ _ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
index 4e99068ed5..bceee1c599 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
new file mode 100644
index 0000000000..ab840e1c46
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -0,0 +1,117 @@
+// 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 NUnit.Framework;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
+ {
+ [Test]
+ public void TestPreviousHitWindowDoesNotExtendPastNextObject()
+ {
+ var objects = new List();
+ var frames = new List();
+
+ for (int i = 0; i < 7; i++)
+ {
+ double time = 1000 + i * 100;
+
+ objects.Add(new Note { StartTime = time });
+
+ if (i > 0)
+ {
+ frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1));
+ frames.Add(new ManiaReplayFrame(time + 11));
+ }
+ }
+
+ performTest(objects, frames);
+
+ addJudgementAssert(objects[0], HitResult.Miss);
+
+ for (int i = 1; i < 7; i++)
+ {
+ addJudgementAssert(objects[i], HitResult.Perfect);
+ addJudgementOffsetAssert(objects[i], 10);
+ }
+ }
+
+ private void addJudgementAssert(ManiaHitObject hitObject, HitResult result)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ }
+
+ private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
+ () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
+ }
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+ private List judgementResults;
+
+ private void performTest(List hitObjects, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo =
+ {
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, false, false)
+ {
+ }
+ }
+ }
+}
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 ed00ed0b4c..892f27d27f 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/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
index 2557f2acdf..3052fc7d34 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
@@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
/// The 0-based column index.
/// Whether the column is a special column.
- public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
+ public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
///
/// Get the type of column given a column index.
///
/// The 0-based column index.
/// The type of the column.
- public ColumnType GetTypeOfColumn(int column)
+ public readonly ColumnType GetTypeOfColumn(int column)
{
if (IsSpecialColumn(column))
return ColumnType.Special;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
index efcfe11dad..5fa687298a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@@ -15,7 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
AccentColour.Value = colours.Yellow;
Background.Alpha = 0.5f;
- Foreground.Alpha = 0;
}
+
+ protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
}
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index 294aab1e4e..28e5d2cc1b 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
- protected override int NumericResultFor(HitResult result) => 20;
+ protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0;
protected override double HealthIncreaseFor(HitResult result)
{
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 68dce8b139..37b34d1721 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom();
+
+ if (mods.HasFlag(LegacyMods.Mirror))
+ yield return new ManiaModMirror();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@@ -175,6 +178,10 @@ namespace osu.Game.Rulesets.Mania
case ManiaModFadeIn _:
value |= LegacyMods.FadeIn;
break;
+
+ case ManiaModMirror _:
+ value |= LegacyMods.Mirror;
+ break;
}
}
@@ -220,6 +227,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
+ new ManiaModInvert(),
};
case ModType.Automation:
@@ -325,6 +333,16 @@ namespace osu.Game.Rulesets.Mania
Height = 250
}),
}
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
+ {
+ new UnstableRate(score.HitEvents)
+ }))
+ }
}
};
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index c0c8505f44..f078345fc1 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
@@ -14,15 +15,23 @@ namespace osu.Game.Rulesets.Mania
///
public readonly int? TargetColumn;
+ ///
+ /// The intended for this component.
+ /// May be null if the component is not a direct member of a .
+ ///
+ public readonly StageDefinition? StageDefinition;
+
///
/// Creates a new .
///
/// The component.
/// The intended index for this component. May be null if the component does not exist in a .
- public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null)
+ /// The intended for this component. May be null if the component is not a direct member of a .
+ public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null)
: base(component)
{
TargetColumn = targetColumn;
+ StageDefinition = stageDefinition;
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
new file mode 100644
index 0000000000..1ea45c295c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
@@ -0,0 +1,76 @@
+// 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 osu.Framework.Graphics.Sprites;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public class ManiaModInvert : Mod, IApplicableAfterBeatmapConversion
+ {
+ public override string Name => "Invert";
+
+ public override string Acronym => "IN";
+ public override double ScoreMultiplier => 1;
+
+ public override string Description => "Hold the keys. To the beat.";
+
+ public override IconUsage? Icon => FontAwesome.Solid.YinYang;
+
+ public override ModType Type => ModType.Conversion;
+
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+
+ var newObjects = new List();
+
+ foreach (var column in maniaBeatmap.HitObjects.GroupBy(h => h.Column))
+ {
+ var newColumnObjects = new List();
+
+ var locations = column.OfType().Select(n => (startTime: n.StartTime, samples: n.Samples))
+ .Concat(column.OfType().SelectMany(h => new[]
+ {
+ (startTime: h.StartTime, samples: h.GetNodeSamples(0)),
+ (startTime: h.EndTime, samples: h.GetNodeSamples(1))
+ }))
+ .OrderBy(h => h.startTime).ToList();
+
+ for (int i = 0; i < locations.Count - 1; i++)
+ {
+ // Full duration of the hold note.
+ double duration = locations[i + 1].startTime - locations[i].startTime;
+
+ // Beat length at the end of the hold note.
+ double beatLength = beatmap.ControlPointInfo.TimingPointAt(locations[i + 1].startTime).BeatLength;
+
+ // Decrease the duration by at most a 1/4 beat to ensure there's no instantaneous notes.
+ duration = Math.Max(duration / 2, duration - beatLength / 4);
+
+ newColumnObjects.Add(new HoldNote
+ {
+ Column = column.Key,
+ StartTime = locations[i].startTime,
+ Duration = duration,
+ NodeSamples = new List> { locations[i].samples, Array.Empty() }
+ });
+ }
+
+ newObjects.AddRange(newColumnObjects);
+ }
+
+ maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ToList();
+
+ // No breaks
+ maniaBeatmap.Breaks.Clear();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 0c5289efe1..2ebcc5451a 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -1,6 +1,7 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@@ -32,7 +33,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private readonly Container tailContainer;
private readonly Container tickContainer;
- private readonly Drawable bodyPiece;
+ ///
+ /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
+ ///
+ private readonly Container sizingContainer;
+
+ ///
+ /// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of .
+ ///
+ private readonly Container maskingContainer;
+
+ private readonly SkinnableDrawable bodyPiece;
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@@ -44,24 +55,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public bool HasBroken { get; private set; }
+ ///
+ /// Whether the hold note has been released potentially without having caused a break.
+ ///
+ private double? releaseTime;
+
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
{
RelativeSizeAxes = Axes.X;
- AddRangeInternal(new[]
+ Container maskedContents;
+
+ AddRangeInternal(new Drawable[]
{
+ sizingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ maskingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = maskedContents = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ }
+ },
+ headContainer = new Container { RelativeSizeAxes = Axes.Both }
+ }
+ },
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
})
{
RelativeSizeAxes = Axes.X
},
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
- headContainer = new Container { RelativeSizeAxes = Axes.Both },
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
});
+
+ maskedContents.AddRange(new[]
+ {
+ bodyPiece.CreateProxy(),
+ tickContainer.CreateProxy(),
+ tailContainer.CreateProxy(),
+ });
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@@ -127,7 +168,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
base.OnDirectionChanged(e);
- bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
+ if (e.NewValue == ScrollingDirection.Up)
+ {
+ bodyPiece.Anchor = bodyPiece.Origin = Anchor.TopLeft;
+ sizingContainer.Anchor = sizingContainer.Origin = Anchor.BottomLeft;
+ }
+ else
+ {
+ bodyPiece.Anchor = bodyPiece.Origin = Anchor.BottomLeft;
+ sizingContainer.Anchor = sizingContainer.Origin = Anchor.TopLeft;
+ }
}
public override void PlaySamples()
@@ -135,13 +185,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// Samples are played by the head/tail notes.
}
+ public override void OnKilled()
+ {
+ base.OnKilled();
+ (bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
+ }
+
protected override void Update()
{
base.Update();
- // Make the body piece not lie under the head note
+ if (Time.Current < releaseTime)
+ releaseTime = null;
+
+ // Pad the full size container so its contents (i.e. the masking container) reach under the tail.
+ // This is required for the tail to not be masked away, since it lies outside the bounds of the hold note.
+ sizingContainer.Padding = new MarginPadding
+ {
+ Top = Direction.Value == ScrollingDirection.Down ? -Tail.Height : 0,
+ Bottom = Direction.Value == ScrollingDirection.Up ? -Tail.Height : 0,
+ };
+
+ // Pad the masking container to the starting position of the body piece (half-way under the head).
+ // This is required to make the body start getting masked immediately as soon as the note is held.
+ maskingContainer.Padding = new MarginPadding
+ {
+ Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0,
+ Bottom = Direction.Value == ScrollingDirection.Down ? Head.Height / 2 : 0,
+ };
+
+ // Position and resize the body to lie half-way under the head and the tail notes.
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
+
+ // As the note is being held, adjust the size of the sizing container. This has two effects:
+ // 1. The contained masking container will mask the body and ticks.
+ // 2. The head note will move along with the new "head position" in the container.
+ if (Head.IsHit && releaseTime == null)
+ {
+ // How far past the hit target this hold note is. Always a positive value.
+ float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);
+ sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1);
+ }
}
protected override void UpdateStateTransforms(ArmedState state)
@@ -153,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Tail.AllJudged)
+ {
ApplyResult(r => r.Type = HitResult.Perfect);
+ endHold();
+ }
if (Tail.Result.Type == HitResult.Miss)
HasBroken = true;
@@ -167,6 +255,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
+ if (CheckHittable?.Invoke(this, Time.Current) == false)
+ return false;
+
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
// Note: Unlike below, we use the tail's start time to determine the time offset.
@@ -206,6 +297,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
HasBroken = true;
+
+ releaseTime = Time.Current;
}
private void endHold()
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index a73fe259e4..cd56b81e10 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Objects.Drawables;
+
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
///
@@ -17,6 +19,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ // This hitobject should never expire, so this is just a safe maximum.
+ LifetimeEnd = LifetimeStart + 30000;
+ }
+
public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
public override void OnReleased(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index a44d8b09aa..08c41b0d75 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -1,6 +1,7 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -8,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -34,6 +36,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
+ ///
+ /// Whether this can be hit, given a time value.
+ /// If non-null, judgements will be ignored whilst the function returns false.
+ ///
+ public Func CheckHittable;
+
protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject)
{
@@ -120,10 +128,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
break;
case ArmedState.Hit:
- this.FadeOut(150, Easing.OutQuint);
+ this.FadeOut();
break;
}
}
+
+ ///
+ /// Causes this to get missed, disregarding all conditions in implementations of .
+ ///
+ public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
}
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 9451bc4430..973dc06e05 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -64,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
+ if (CheckHittable?.Invoke(this, Time.Current) == false)
+ return false;
+
return UpdateResult(true);
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
index bc4a095395..9999983af5 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
@@ -19,24 +19,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
///
/// Represents length-wise portion of a hold note.
///
- public class DefaultBodyPiece : CompositeDrawable
+ public class DefaultBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable AccentColour = new Bindable();
-
- private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
- private readonly IBindable isHitting = new Bindable();
+ protected readonly IBindable IsHitting = new Bindable();
protected Drawable Background { get; private set; }
- protected BufferedContainer Foreground { get; private set; }
-
- private BufferedContainer subtractionContainer;
- private Container subtractionLayer;
+ private Container foregroundContainer;
public DefaultBodyPiece()
{
Blending = BlendingParameters.Additive;
-
- AddLayout(subtractionCache);
}
[BackgroundDependencyLoader(true)]
@@ -45,7 +38,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
InternalChildren = new[]
{
Background = new Box { RelativeSizeAxes = Axes.Both },
- Foreground = new BufferedContainer
+ foregroundContainer = new Container { RelativeSizeAxes = Axes.Both }
+ };
+
+ if (drawableObject != null)
+ {
+ var holdNote = (DrawableHoldNote)drawableObject;
+
+ AccentColour.BindTo(drawableObject.AccentColour);
+ IsHitting.BindTo(holdNote.IsHitting);
+ }
+
+ AccentColour.BindValueChanged(onAccentChanged, true);
+
+ Recycle();
+ }
+
+ public void Recycle() => foregroundContainer.Child = CreateForeground();
+
+ protected virtual Drawable CreateForeground() => new ForegroundPiece
+ {
+ AccentColour = { BindTarget = AccentColour },
+ IsHitting = { BindTarget = IsHitting }
+ };
+
+ private void onAccentChanged(ValueChangedEvent accent) => Background.Colour = accent.NewValue.Opacity(0.7f);
+
+ private class ForegroundPiece : CompositeDrawable
+ {
+ public readonly Bindable AccentColour = new Bindable();
+ public readonly IBindable IsHitting = new Bindable();
+
+ private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
+
+ private BufferedContainer foregroundBuffer;
+ private BufferedContainer subtractionBuffer;
+ private Container subtractionLayer;
+
+ public ForegroundPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ AddLayout(subtractionCache);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = foregroundBuffer = new BufferedContainer
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
@@ -53,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
Children = new Drawable[]
{
new Box { RelativeSizeAxes = Axes.Both },
- subtractionContainer = new BufferedContainer
+ subtractionBuffer = new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
// This is needed because we're blending with another object
@@ -77,60 +117,51 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
}
- }
- };
-
- if (drawableObject != null)
- {
- var holdNote = (DrawableHoldNote)drawableObject;
-
- AccentColour.BindTo(drawableObject.AccentColour);
- isHitting.BindTo(holdNote.IsHitting);
- }
-
- AccentColour.BindValueChanged(onAccentChanged, true);
- isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true);
- }
-
- private void onAccentChanged(ValueChangedEvent accent)
- {
- Foreground.Colour = accent.NewValue.Opacity(0.5f);
- Background.Colour = accent.NewValue.Opacity(0.7f);
-
- const float animation_length = 50;
-
- Foreground.ClearTransforms(false, nameof(Foreground.Colour));
-
- if (isHitting.Value)
- {
- // wait for the next sync point
- double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
- using (Foreground.BeginDelayedSequence(synchronisedOffset))
- Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
- }
-
- subtractionCache.Invalidate();
- }
-
- protected override void Update()
- {
- base.Update();
-
- if (!subtractionCache.IsValid)
- {
- subtractionLayer.Width = 5;
- subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
- subtractionLayer.EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.White,
- Type = EdgeEffectType.Glow,
- Radius = DrawWidth
};
- Foreground.ForceRedraw();
- subtractionContainer.ForceRedraw();
+ AccentColour.BindValueChanged(onAccentChanged, true);
+ IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true);
+ }
- subtractionCache.Validate();
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f);
+
+ const float animation_length = 50;
+
+ foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour));
+
+ if (IsHitting.Value)
+ {
+ // wait for the next sync point
+ double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
+ using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset))
+ foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop();
+ }
+
+ subtractionCache.Invalidate();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!subtractionCache.IsValid)
+ {
+ subtractionLayer.Width = 5;
+ subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
+ subtractionLayer.EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.White,
+ Type = EdgeEffectType.Glow,
+ Radius = DrawWidth
+ };
+
+ foregroundBuffer.ForceRedraw();
+ subtractionBuffer.ForceRedraw();
+
+ subtractionCache.Validate();
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs
new file mode 100644
index 0000000000..ac3792c01d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
+{
+ ///
+ /// Interface for mania hold note bodies.
+ ///
+ public interface IHoldNoteBody
+ {
+ ///
+ /// Recycles the contents of this to free used resources.
+ ///
+ void Recycle();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index a100c9a58e..6cc7ff92d3 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{
StartTime = StartTime,
Column = Column,
- Samples = getNodeSamples(0),
+ Samples = GetNodeSamples(0),
});
AddNested(Tail = new TailNote
{
StartTime = EndTime,
Column = Column,
- Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
+ Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
}
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
- private IList getNodeSamples(int nodeIndex) =>
+ public IList GetNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs
new file mode 100644
index 0000000000..c8b05ed2f8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs
@@ -0,0 +1,46 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class HitTargetInsetContainer : Container
+ {
+ private readonly IBindable direction = new Bindable();
+
+ protected override Container Content => content;
+ private readonly Container content;
+
+ private float hitPosition;
+
+ public HitTargetInsetContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = content = new Container { RelativeSizeAxes = Axes.Both };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION;
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ content.Padding = direction.NewValue == ScrollingDirection.Up
+ ? new MarginPadding { Top = hitPosition }
+ : new MarginPadding { Bottom = hitPosition };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index 9f716428c0..c0f0fcb4af 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -1,6 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
private readonly IBindable direction = new Bindable();
private readonly IBindable isHitting = new Bindable();
- private Drawable sprite;
+ [CanBeNull]
+ private Drawable bodySprite;
+
+ [CanBeNull]
+ private Drawable lightContainer;
+
+ [CanBeNull]
+ private Drawable light;
public LegacyBodyPiece()
{
@@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning
string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
- sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
+ string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value
+ ?? "lightingL";
+
+ float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
+ ?? 1;
+
+ // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
+ // This animation is discarded and re-queried with the appropriate frame length afterwards.
+ var tmp = skin.GetAnimation(lightImage, true, false);
+ double frameLength = 0;
+ if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
+ frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
+
+ light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
+ {
+ if (d == null)
+ return;
+
+ d.Origin = Anchor.Centre;
+ d.Blending = BlendingParameters.Additive;
+ d.Scale = new Vector2(lightScale);
+ });
+
+ if (light != null)
+ {
+ lightContainer = new HitTargetInsetContainer
+ {
+ Alpha = 0,
+ Child = light
+ };
+ }
+
+ bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
{
if (d == null)
return;
@@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
// Todo: Wrap
});
- if (sprite != null)
- InternalChild = sprite;
+ if (bodySprite != null)
+ InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
@@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning
private void onIsHittingChanged(ValueChangedEvent isHitting)
{
- if (!(sprite is TextureAnimation animation))
+ if (bodySprite is TextureAnimation bodyAnimation)
+ {
+ bodyAnimation.GotoFrame(0);
+ bodyAnimation.IsPlaying = isHitting.NewValue;
+ }
+
+ if (lightContainer == null)
return;
- animation.GotoFrame(0);
- animation.IsPlaying = isHitting.NewValue;
+ if (isHitting.NewValue)
+ {
+ // Clear the fade out and, more importantly, the removal.
+ lightContainer.ClearTransforms();
+
+ // Only add the container if the removal has taken place.
+ if (lightContainer.Parent == null)
+ Column.TopLevelContainer.Add(lightContainer);
+
+ // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847).
+ if (light is TextureAnimation lightAnimation)
+ lightAnimation.GotoFrame(0);
+
+ lightContainer.FadeIn(80);
+ }
+ else
+ {
+ lightContainer.FadeOut(120)
+ .OnComplete(d => Column.TopLevelContainer.Remove(d));
+ }
}
private void onDirectionChanged(ValueChangedEvent direction)
{
- if (sprite == null)
- return;
-
if (direction.NewValue == ScrollingDirection.Up)
{
- sprite.Origin = Anchor.BottomCentre;
- sprite.Scale = new Vector2(1, -1);
+ if (bodySprite != null)
+ {
+ bodySprite.Origin = Anchor.BottomCentre;
+ bodySprite.Scale = new Vector2(1, -1);
+ }
+
+ if (light != null)
+ light.Anchor = Anchor.TopCentre;
}
else
{
- sprite.Origin = Anchor.TopCentre;
- sprite.Scale = Vector2.One;
+ if (bodySprite != null)
+ {
+ bodySprite.Origin = Anchor.TopCentre;
+ bodySprite.Scale = Vector2.One;
+ }
+
+ if (light != null)
+ light.Anchor = Anchor.BottomCentre;
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ lightContainer?.Expire();
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
index 64a7641421..3bf51b3073 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI.Scrolling;
@@ -18,14 +17,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler
{
private readonly IBindable direction = new Bindable();
- private readonly bool isLastColumn;
private Container lightContainer;
private Sprite light;
- public LegacyColumnBackground(bool isLastColumn)
+ public LegacyColumnBackground()
{
- this.isLastColumn = isLastColumn;
RelativeSizeAxes = Axes.Both;
}
@@ -35,52 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value
?? "mania-stage-light";
- float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth)
- ?.Value ?? 1;
- float rightLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth)
- ?.Value ?? 1;
-
- bool hasLeftLine = leftLineWidth > 0;
- bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
- || isLastColumn;
-
float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
?? 0;
- Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
- ?? Color4.White;
-
- Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
- ?? Color4.Black;
-
Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
?? Color4.White;
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = backgroundColour
- },
- new Box
- {
- RelativeSizeAxes = Axes.Y,
- Width = leftLineWidth,
- Scale = new Vector2(0.740f, 1),
- Colour = lineColour,
- Alpha = hasLeftLine ? 1 : 0
- },
- new Box
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = rightLineWidth,
- Scale = new Vector2(0.740f, 1),
- Colour = lineColour,
- Alpha = hasRightLine ? 1 : 0
- },
lightContainer = new Container
{
Origin = Anchor.BottomCentre,
@@ -90,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
- Colour = lightColour,
+ Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
Texture = skin.GetTexture(lightImage),
RelativeSizeAxes = Axes.X,
Width = 1,
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index 12747924de..7c5d41efcf 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -66,6 +67,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
public void Animate(JudgementResult result)
{
+ if (result.Judgement is HoldNoteTickJudgement)
+ return;
+
(explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(80)
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
index d055ef3480..6eced571d2 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
@@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Container directionContainer;
- public LegacyHitTarget()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
@@ -56,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
- Colour = lineColour,
+ Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour),
Alpha = showJudgementLine ? 0.9f : 0
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
index 44f3e7d7b3..b269ea25d4 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
@@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
+
+ if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false)
+ Column.UnderlayElements.Add(CreateProxy());
}
private void onDirectionChanged(ValueChangedEvent direction)
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
index 7f5de601ca..b0bab8e760 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
@@ -4,19 +4,27 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyStageBackground : CompositeDrawable
{
+ private readonly StageDefinition stageDefinition;
+
private Drawable leftSprite;
private Drawable rightSprite;
+ private ColumnFlow columnBackgrounds;
- public LegacyStageBackground()
+ public LegacyStageBackground(StageDefinition stageDefinition)
{
+ this.stageDefinition = stageDefinition;
RelativeSizeAxes = Axes.Both;
}
@@ -44,8 +52,19 @@ namespace osu.Game.Rulesets.Mania.Skinning
Origin = Anchor.TopLeft,
X = -0.05f,
Texture = skin.GetTexture(rightImage)
+ },
+ columnBackgrounds = new ColumnFlow(stageDefinition)
+ {
+ RelativeSizeAxes = Axes.Y
+ },
+ new HitTargetInsetContainer
+ {
+ Child = new LegacyHitTarget { RelativeSizeAxes = Axes.Both }
}
};
+
+ for (int i = 0; i < stageDefinition.Columns; i++)
+ columnBackgrounds.SetContentForColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1));
}
protected override void Update()
@@ -58,5 +77,72 @@ namespace osu.Game.Rulesets.Mania.Skinning
if (rightSprite?.Height > 0)
rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
}
+
+ private class ColumnBackground : CompositeDrawable
+ {
+ private readonly int columnIndex;
+ private readonly bool isLastColumn;
+
+ public ColumnBackground(int columnIndex, bool isLastColumn)
+ {
+ this.columnIndex = columnIndex;
+ this.isLastColumn = isLastColumn;
+
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ float leftLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftLineWidth, columnIndex)?.Value ?? 1;
+ float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1;
+
+ bool hasLeftLine = leftLineWidth > 0;
+ bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
+ || isLastColumn;
+
+ Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;
+ Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black;
+
+ InternalChildren = new Drawable[]
+ {
+ LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }, backgroundColour),
+ new HitTargetInsetContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = leftLineWidth,
+ Scale = new Vector2(0.740f, 1),
+ Alpha = hasLeftLine ? 1 : 0,
+ Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }, lineColour)
+ },
+ new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = rightLineWidth,
+ Scale = new Vector2(0.740f, 1),
+ Alpha = hasRightLine ? 1 : 0,
+ Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }, lineColour)
+ },
+ }
+ }
+ };
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
index e167135556..439e6f7df2 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
using System.Collections.Generic;
+using System.Diagnostics;
using osu.Framework.Audio.Sample;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
@@ -88,10 +89,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
switch (maniaComponent.Component)
{
case ManiaSkinComponents.ColumnBackground:
- return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1);
+ return new LegacyColumnBackground();
case ManiaSkinComponents.HitTarget:
- return new LegacyHitTarget();
+ // Legacy skins sandwich the hit target between the column background and the column light.
+ // To preserve this ordering, it's created manually inside LegacyStageBackground.
+ return Drawable.Empty();
case ManiaSkinComponents.KeyArea:
return new LegacyKeyArea();
@@ -112,7 +115,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
return new LegacyHitExplosion();
case ManiaSkinComponents.StageBackground:
- return new LegacyStageBackground();
+ Debug.Assert(maniaComponent.StageDefinition != null);
+ return new LegacyStageBackground(maniaComponent.StageDefinition.Value);
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 255ce4c064..9aabcc6699 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer;
private readonly DrawablePool hitExplosionPool;
+ private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@@ -65,11 +67,11 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
}
- public override Axes RelativeSizeAxes => Axes.Y;
-
public ColumnType ColumnType { get; set; }
public bool IsSpecial => ColumnType == ColumnType.Special;
@@ -92,6 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI
hitObject.AccentColour.Value = AccentColour;
hitObject.OnNewResult += OnNewResult;
+ DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
+ maniaObject.CheckHittable = hitPolicy.IsHittable;
+
HitObjectContainer.Add(hitObject);
}
@@ -106,6 +111,9 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
+ if (result.IsHit)
+ hitPolicy.HandleHit(judgedObject);
+
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
new file mode 100644
index 0000000000..aef82d4c08
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -0,0 +1,105 @@
+// 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.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// A which flows its contents according to the s in a .
+ /// Content can be added to individual columns via .
+ ///
+ /// The type of content in each column.
+ public class ColumnFlow : CompositeDrawable
+ where TContent : Drawable
+ {
+ ///
+ /// All contents added to this .
+ ///
+ public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList();
+
+ private readonly FillFlowContainer columns;
+ private readonly StageDefinition stageDefinition;
+
+ public ColumnFlow(StageDefinition stageDefinition)
+ {
+ this.stageDefinition = stageDefinition;
+
+ AutoSizeAxes = Axes.X;
+
+ InternalChild = columns = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Horizontal,
+ };
+
+ for (int i = 0; i < stageDefinition.Columns; i++)
+ columns.Add(new Container { RelativeSizeAxes = Axes.Y });
+ }
+
+ private ISkinSource currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ currentSkin = skin;
+
+ skin.SourceChanged += onSkinChanged;
+ onSkinChanged();
+ }
+
+ private void onSkinChanged()
+ {
+ for (int i = 0; i < stageDefinition.Columns; i++)
+ {
+ if (i > 0)
+ {
+ float spacing = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
+ ?.Value ?? Stage.COLUMN_SPACING;
+
+ columns[i].Margin = new MarginPadding { Left = spacing };
+ }
+
+ float? width = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
+ ?.Value;
+
+ if (width == null)
+ // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
+ columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
+ else
+ columns[i].Width = width.Value;
+ }
+ }
+
+ ///
+ /// Sets the content of one of the columns of this .
+ ///
+ /// The index of the column to set the content of.
+ /// The content.
+ public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
+
+ public new MarginPadding Padding
+ {
+ get => base.Padding;
+ set => base.Padding = value;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (currentSkin != null)
+ currentSkin.SourceChanged -= onSkinChanged;
+ }
+ }
+}
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.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
new file mode 100644
index 0000000000..0f9cd48dd8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
@@ -0,0 +1,78 @@
+// 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 osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// Ensures that only the most recent is hittable, affectionately known as "note lock".
+ ///
+ public class OrderedHitPolicy
+ {
+ private readonly HitObjectContainer hitObjectContainer;
+
+ public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
+ {
+ this.hitObjectContainer = hitObjectContainer;
+ }
+
+ ///
+ /// Determines whether a can be hit at a point in time.
+ ///
+ ///
+ /// Only the most recent can be hit, a previous hitobject's window cannot extend past the next one.
+ ///
+ /// The to check.
+ /// The time to check.
+ /// Whether can be hit at the given .
+ public bool IsHittable(DrawableHitObject hitObject, double time)
+ {
+ var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject);
+ return nextObject == null || time < nextObject.HitObject.StartTime;
+ }
+
+ ///
+ /// Handles a being hit to potentially miss all earlier s.
+ ///
+ /// The that was hit.
+ public void HandleHit(DrawableHitObject hitObject)
+ {
+ if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
+ throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
+
+ foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
+ {
+ if (obj.Judged)
+ continue;
+
+ ((DrawableManiaHitObject)obj).MissForcefully();
+ }
+ }
+
+ private IEnumerable enumerateHitObjectsUpTo(double targetTime)
+ {
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.GetEndTime() >= targetTime)
+ yield break;
+
+ yield return obj;
+
+ foreach (var nestedObj in obj.NestedHitObjects)
+ {
+ if (nestedObj.HitObject.GetEndTime() >= targetTime)
+ break;
+
+ yield return nestedObj;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index 36780b0f80..e7a2de266d 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -11,7 +10,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
@@ -31,14 +29,13 @@ namespace osu.Game.Rulesets.Mania.UI
public const float HIT_TARGET_POSITION = 110;
- public IReadOnlyList Columns => columnFlow.Children;
- private readonly FillFlowContainer columnFlow;
+ public IReadOnlyList Columns => columnFlow.Content;
+ private readonly ColumnFlow columnFlow;
private readonly JudgementContainer judgements;
private readonly DrawablePool judgementPool;
private readonly Drawable barLineContainer;
- private readonly Container topLevelContainer;
private readonly Dictionary columnColours = new Dictionary
{
@@ -62,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
+ Container topLevelContainer;
+
InternalChildren = new Drawable[]
{
judgementPool = new DrawablePool(2),
@@ -73,17 +72,13 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground())
{
RelativeSizeAxes = Axes.Both
},
- columnFlow = new FillFlowContainer
+ columnFlow = new ColumnFlow(definition)
{
- Name = "Columns",
RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
- Direction = FillDirection.Horizontal,
- Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
},
new Container
{
@@ -102,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null)
{
RelativeSizeAxes = Axes.Both
},
@@ -121,60 +116,22 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++)
{
var columnType = definition.GetTypeOfColumn(i);
+
var column = new Column(firstColumnIndex + i)
{
+ RelativeSizeAxes = Axes.Both,
+ Width = 1,
ColumnType = columnType,
AccentColour = columnColours[columnType],
Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
};
- AddColumn(column);
+ topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
+ columnFlow.SetContentForColumn(i, column);
+ AddNested(column);
}
}
- private ISkin currentSkin;
-
- [BackgroundDependencyLoader]
- private void load(ISkinSource skin)
- {
- currentSkin = skin;
- skin.SourceChanged += onSkinChanged;
-
- onSkinChanged();
- }
-
- private void onSkinChanged()
- {
- foreach (var col in columnFlow)
- {
- if (col.Index > 0)
- {
- float spacing = currentSkin.GetConfig(
- new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1))
- ?.Value ?? COLUMN_SPACING;
-
- col.Margin = new MarginPadding { Left = spacing };
- }
-
- float? width = currentSkin.GetConfig(
- new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index))
- ?.Value;
-
- if (width == null)
- // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
- col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
- else
- col.Width = width.Value;
- }
- }
-
- public void AddColumn(Column c)
- {
- topLevelContainer.Add(c.TopLevelContainer.CreateProxy());
- columnFlow.Add(c);
- AddNested(c);
- }
-
public override void Add(DrawableHitObject h)
{
var maniaObject = (ManiaHitObject)h.HitObject;
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/Resources/metrics-skin/spinner-background@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png
new file mode 100644
index 0000000000..4f50f638c5
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png
new file mode 100644
index 0000000000..daf28e09cb
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png
new file mode 100644
index 0000000000..6ef1068420
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
index a69646507a..3d100e4b1c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -22,7 +22,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Storyboards;
using osuTK;
-using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audioManager { get; set; }
- private TrackVirtualManual track;
-
protected override bool Autoplay => autoplay;
private bool autoplay;
@@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
- track = (TrackVirtualManual)working.Track;
- return working;
- }
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("enable autoplay", () => autoplay = true);
base.SetUpSteps();
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex);
@@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("have autoplay", () => autoplay = true);
base.SetUpSteps();
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex);
@@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time)
{
- AddStep($"seek to {time}", () => track.Seek(time));
+ AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 47b3926ceb..94d1cb8864 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests
drawableSpinner = new TestDrawableSpinner(spinner, auto)
{
Anchor = Anchor.Centre,
- Depth = depthIndex++
+ Depth = depthIndex++,
+ Scale = new Vector2(0.75f)
};
foreach (var mod in SelectedMods.Value.OfType())
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 69857f8ef9..f7909071ea 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -25,7 +25,6 @@ using osu.Game.Scoring;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
-using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -34,18 +33,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audioManager { get; set; }
- private TrackVirtualManual track;
-
protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
- track = (TrackVirtualManual)working.Track;
- return working;
- }
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
@@ -55,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
base.SetUpSteps();
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
}
@@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0);
- AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
+ AddStep("adjust track rate", () => MusicController.CurrentTrack.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,
@@ -230,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time)
{
- AddStep($"seek to {time}", () => track.Seek(time));
+ AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
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 f3837ea6b1..3639c3616f 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/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 8308c0c576..2946331bc6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
///
- /// Whether this can be hit.
+ /// Whether this can be hit, given a time value.
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
///
public Func CheckHittable;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index f10d11827b..a57bb466c7 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -12,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;
@@ -31,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)
{
@@ -82,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()
{
@@ -101,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 }
});
}
}
@@ -119,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());
}
}
@@ -173,6 +177,12 @@ 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).
///
@@ -222,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()
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
index 9c4b6f774f..0b1232b8db 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
- protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
+ protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
index de3ae27e55..f54e7a9a15 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public override bool AffectsCombo => false;
- protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
+ protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index eaa5d8937a..f527eb2312 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
- new StatisticRow
+ var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
+
+ return new[]
{
- Columns = new[]
+ new StatisticRow
{
- new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
+ Columns = new[]
{
- RelativeSizeAxes = Axes.X,
- Height = 250
- }),
- }
- },
- new StatisticRow
- {
- Columns = new[]
+ new StatisticItem("Timing Distribution",
+ new HitEventTimingDistributionGraph(timedHitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
{
- new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
+ Columns = new[]
{
- RelativeSizeAxes = Axes.X,
- Height = 250
- }),
+ new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
+ {
+ new UnstableRate(timedHitEvents)
+ }))
+ }
}
- }
- };
+ };
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 4cb2cd6539..76b2631894 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -154,8 +154,12 @@ namespace osu.Game.Rulesets.Osu.Replays
// The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position
// We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition.
// TODO: Shouldn't the spinner always spin in the same direction?
- if (h is Spinner)
+ if (h is Spinner spinner)
{
+ // spinners with 0 spins required will auto-complete - don't bother
+ if (spinner.SpinsRequired == 0)
+ return;
+
calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection);
Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position;
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index 0ab3e8825b..d15a0a3203 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
hitCircleSprite = new Sprite
{
Texture = getTextureWithFallback(string.Empty),
- Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
@@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
base.LoadComplete();
state.BindValueChanged(updateState, true);
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
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..e157842fd1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
private DrawableSpinner drawableSpinner;
private Sprite disc;
+ private Sprite metreSprite;
private Container metre;
- private const float background_y_offset = 20;
-
private const float sprite_scale = 1 / 1.6f;
+ private const float final_metre_height = 692 * sprite_scale;
[BackgroundDependencyLoader]
private void load(ISkinSource source, DrawableHitObject drawableObject)
@@ -35,65 +35,82 @@ namespace osu.Game.Rulesets.Osu.Skinning
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ InternalChild = new Container
{
- new Sprite
+ // the old-style spinner relied heavily on absolute screen-space coordinate values.
+ // wrap everything in a container simulating absolute coords to preserve alignment
+ // as there are skins that depend on it.
+ Width = 640,
+ Height = 480,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
{
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Texture = source.GetTexture("spinner-background"),
- Y = background_y_offset,
- Scale = new Vector2(sprite_scale)
- },
- disc = new Sprite
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-circle"),
- Scale = new Vector2(sprite_scale)
- },
- metre = new Container
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Y = background_y_offset,
- Masking = true,
- Child = new Sprite
+ new Sprite
{
- Texture = source.GetTexture("spinner-metre"),
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-background"),
+ Scale = new Vector2(sprite_scale)
},
- Scale = new Vector2(0.625f)
+ disc = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-circle"),
+ Scale = new Vector2(sprite_scale)
+ },
+ metre = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ // this anchor makes no sense, but that's what stable uses.
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ // adjustment for stable (metre has additional offset)
+ Margin = new MarginPadding { Top = 20 },
+ Masking = true,
+ Child = metreSprite = new Sprite
+ {
+ Texture = source.GetTexture("spinner-metre"),
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Scale = new Vector2(0.625f)
+ }
+ }
}
};
}
- private Vector2 metreFinalSize;
-
protected override void LoadComplete()
{
base.LoadComplete();
this.FadeOut();
drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
-
- metreFinalSize = metre.Size = metre.Child.Size;
}
private void updateStateTransforms(ValueChangedEvent state)
{
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()
{
base.Update();
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
- metre.Height = getMetreHeight(drawableSpinner.Progress);
+
+ // careful: need to call this exactly once for all calculations in a frame
+ // as the function has a random factor in it
+ var metreHeight = getMetreHeight(drawableSpinner.Progress);
+
+ // hack to make the metre blink up from below than down from above.
+ // move down the container to be able to apply masking for the metre,
+ // and then move the sprite back up the same amount to keep its position absolute.
+ metre.Y = final_metre_height - metreHeight;
+ metreSprite.Y = -metre.Y;
}
private const int total_bars = 10;
@@ -108,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (RNG.NextBool(((int)progress % 10) / 10f))
barCount++;
- return (float)barCount / total_bars * metreFinalSize.Y;
+ return (float)barCount / total_bars * final_metre_height;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
index 0f586034d5..25ab96445a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
- animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
+ var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
InternalChildren = new[]
{
@@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255),
},
- animationContent.With(d =>
+ LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
- }),
+ }), ballColour),
layerSpec = new Sprite
{
Anchor = Anchor.Centre,
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.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index e896606ee8..b59f3a4344 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.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs
index bfcf268c3d..9b73ccd248 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
private void updateAccentColour()
{
- backgroundLayer.Colour = accentColour;
+ backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
index 8223e3bc01..5ab8e3a8c8 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
@@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning
private void updateAccentColour()
{
- headCircle.AccentColour = accentColour;
- body.Colour = accentColour;
- end.Colour = accentColour;
+ var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
+
+ headCircle.AccentColour = colour;
+ body.Colour = colour;
+ end.Colour = colour;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
index 656728f6e4..b11b64c22c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning
@@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning
[BackgroundDependencyLoader]
private void load()
{
- AccentColour = component == TaikoSkinComponents.CentreHit
- ? new Color4(235, 69, 44, 255)
- : new Color4(67, 142, 172, 255);
+ AccentColour = LegacyColourCompatibility.DisallowZeroAlpha(
+ component == TaikoSkinComponents.CentreHit
+ ? new Color4(235, 69, 44, 255)
+ : new Color4(67, 142, 172, 255));
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 2011842591..dbc32f2c3e 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
- new StatisticRow
+ var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
+
+ return new[]
{
- Columns = new[]
+ new StatisticRow
{
- new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
+ Columns = new[]
{
- RelativeSizeAxes = Axes.X,
- Height = 250
- }),
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
+ {
+ new UnstableRate(timedHitEvents)
+ }))
+ }
}
- }
- };
+ };
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
new file mode 100644
index 0000000000..0f6d956b3c
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Tests.Beatmaps
+{
+ [TestFixture]
+ public class BeatmapDifficultyManagerTest
+ {
+ [Test]
+ public void TestKeyEqualsWithDifferentModInstances()
+ {
+ var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+ var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+
+ Assert.That(key1, Is.EqualTo(key2));
+ }
+
+ [Test]
+ public void TestKeyEqualsWithDifferentModOrder()
+ {
+ var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+ var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
+
+ Assert.That(key1, Is.EqualTo(key2));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 30331e98d2..4a11e1785b 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Texture GetBackground() => throw new NotImplementedException();
- protected override Track GetTrack() => throw new NotImplementedException();
+ protected override Track GetBeatmapTrack() => throw new NotImplementedException();
}
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
new file mode 100644
index 0000000000..9c71466489
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -0,0 +1,70 @@
+// 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 NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Beatmaps.Formats
+{
+ [TestFixture]
+ public class LegacyScoreDecoderTest
+ {
+ [Test]
+ public void TestDecodeManiaReplay()
+ {
+ var decoder = new TestLegacyScoreDecoder();
+
+ using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
+ {
+ var score = decoder.Parse(resourceStream);
+
+ Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID);
+
+ Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
+ Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
+
+ Assert.AreEqual(829_931, score.ScoreInfo.TotalScore);
+ Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
+ Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001));
+ Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
+
+ Assert.That(score.Replay.Frames, Is.Not.Empty);
+ }
+ }
+
+ private class TestLegacyScoreDecoder : LegacyScoreDecoder
+ {
+ private static readonly Dictionary rulesets = new Ruleset[]
+ {
+ new OsuRuleset(),
+ new TaikoRuleset(),
+ new CatchRuleset(),
+ new ManiaRuleset()
+ }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
+
+ protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
+
+ protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ MD5Hash = md5Hash,
+ Ruleset = new OsuRuleset().RulesetInfo,
+ BaseDifficulty = new BeatmapDifficulty()
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 168ec0f09d..bd34eaff63 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -169,17 +169,17 @@ namespace osu.Game.Tests.Editing
[Test]
public void GetSnappedDistanceFromDistance()
{
- assertSnappedDistance(50, 100);
+ assertSnappedDistance(50, 0);
assertSnappedDistance(100, 100);
- assertSnappedDistance(150, 200);
+ assertSnappedDistance(150, 100);
assertSnappedDistance(200, 200);
- assertSnappedDistance(250, 300);
+ assertSnappedDistance(250, 200);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
assertSnappedDistance(50, 0);
- assertSnappedDistance(100, 200);
- assertSnappedDistance(150, 200);
+ assertSnappedDistance(100, 0);
+ assertSnappedDistance(150, 0);
assertSnappedDistance(200, 200);
assertSnappedDistance(250, 200);
@@ -190,8 +190,8 @@ namespace osu.Game.Tests.Editing
});
assertSnappedDistance(50, 0);
- assertSnappedDistance(100, 200);
- assertSnappedDistance(150, 200);
+ assertSnappedDistance(100, 0);
+ assertSnappedDistance(150, 0);
assertSnappedDistance(200, 200);
assertSnappedDistance(250, 200);
assertSnappedDistance(400, 400);
diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs
index cd3669f160..891537c4ad 100644
--- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs
@@ -1,10 +1,8 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
@@ -19,7 +17,14 @@ namespace osu.Game.Tests.Gameplay
{
GameplayClockContainer gcc = null;
- AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0)));
+ AddStep("create container", () =>
+ {
+ var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ working.LoadTrack();
+
+ Add(gcc = new GameplayClockContainer(working, 0));
+ });
+
AddStep("start track", () => gcc.Start());
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
new file mode 100644
index 0000000000..b0baf0385e
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneScoreProcessor : OsuTestScene
+ {
+ [Test]
+ public void TestNoScoreIncreaseFromMiss()
+ {
+ var beatmap = new Beatmap { HitObjects = { new TestHitObject() } };
+
+ var scoreProcessor = new ScoreProcessor();
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ // Apply a miss judgement
+ scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss });
+
+ Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
+ }
+
+ private class TestHitObject : HitObject
+ {
+ public override Judgement CreateJudgement() => new TestJudgement();
+ }
+
+ private class TestJudgement : Judgement
+ {
+ protected override int NumericResultFor(HitResult result) => 100;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index b30870d057..a690eb3b59 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -59,7 +59,10 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () =>
{
- Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0));
+ var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ working.LoadTrack();
+
+ Add(gameplayContainer = new GameplayClockContainer(working, 0));
gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
{
@@ -103,7 +106,7 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod };
- Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0));
+ Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0));
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index 7b2913b817..d15682b1eb 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
}
[Test]
- public void TestApplyDrainRateQueries()
+ public void TestApplyDrainRateQueriesByDrKeyword()
{
const string query = "dr>2 quite specific dr<:6";
var filterCriteria = new FilterCriteria();
@@ -73,6 +73,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
+ [Test]
+ public void TestApplyDrainRateQueriesByHpKeyword()
+ {
+ const string query = "hp>2 quite specific hp<=6";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
+ Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
+ Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
+ Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
+ Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
+ }
+
[Test]
public void TestApplyBPMQueries()
{
diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs
new file mode 100644
index 0000000000..ad6f01881b
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs
@@ -0,0 +1,43 @@
+// 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.Linq;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
+
+namespace osu.Game.Tests.NonVisual.Ranking
+{
+ [TestFixture]
+ public class UnstableRateTest
+ {
+ [Test]
+ public void TestDistributedHits()
+ {
+ var events = Enumerable.Range(-5, 11)
+ .Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null));
+
+ var unstableRate = new UnstableRate(events);
+
+ Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value, 10 * Math.Sqrt(10)));
+ }
+
+ [Test]
+ public void TestMissesAndEmptyWindows()
+ {
+ var events = new[]
+ {
+ new HitEvent(-100, HitResult.Miss, new HitObject(), null, null),
+ new HitEvent(0, HitResult.Great, new HitObject(), null, null),
+ new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null),
+ };
+
+ var unstableRate = new UnstableRate(events);
+
+ Assert.AreEqual(0, unstableRate.Value);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr
new file mode 100644
index 0000000000..da1a7bdd28
Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/mania-replay.osr differ
diff --git a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini
deleted file mode 100644
index 3c0dae6b13..0000000000
--- a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[General]
-Version: latest
-
-[Colours]
-Combo1: 255,255,255,0
\ No newline at end of file
diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
index c408d2f182..aedf26ee75 100644
--- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
@@ -108,15 +108,5 @@ namespace osu.Game.Tests.Skins
using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
}
-
- [Test]
- public void TestDecodeColourWithZeroAlpha()
- {
- var decoder = new LegacySkinDecoder();
-
- using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini"))
- using (var stream = new LineBufferedReader(resStream))
- Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f));
- }
}
}
diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
index 4d3b73fb32..eff430ac25 100644
--- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
+++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
@@ -26,6 +26,7 @@ namespace osu.Game.Tests.Skins
{
var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result;
beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]);
+ beatmap.LoadTrack();
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
index 79275d70a7..6fd5511e5a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
@@ -4,7 +4,6 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Audio.Track;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneCompletionCancellation : OsuPlayerTestScene
{
- private Track track;
-
[Resolved]
private AudioManager audio { get; set; }
@@ -34,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
base.SetUpSteps();
// Ensure track has actually running before attempting to seek
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
}
[Test]
@@ -73,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private void complete()
{
- AddStep("seek to completion", () => track.Seek(5000));
+ AddStep("seek to completion", () => Beatmap.Value.Track.Seek(5000));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
}
private void cancel()
{
- AddStep("rewind to cancel", () => track.Seek(4000));
+ AddStep("rewind to cancel", () => Beatmap.Value.Track.Seek(4000));
AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value);
}
@@ -91,11 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
- track = working.Track;
- return working;
- }
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
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/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 2a119f5199..73c6970482 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Audio.Track;
using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@@ -21,19 +20,13 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private AudioManager audioManager { get; set; }
- private Track track;
-
- protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
- track = working.Track;
- return working;
- }
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
+ new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
public void TestNoJudgementsOnRewind()
{
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
@@ -46,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addSeekStep(double time)
{
- AddStep($"seek to {time}", () => track.Seek(time));
+ AddStep($"seek to {time}", () => Beatmap.Value.Track.Seek(time));
// Allow a few frames of lenience
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
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/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
index 030d420ec0..09b4f9b761 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
@@ -20,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
- TextSize = 40,
Margin = new MarginPadding(20),
};
Add(score);
@@ -30,7 +29,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Margin = new MarginPadding(10),
- TextSize = 40,
};
Add(comboCounter);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
index 7ed7a116b4..841722a8f1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
@@ -1,11 +1,9 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK;
@@ -32,7 +30,10 @@ namespace osu.Game.Tests.Visual.Gameplay
requestCount = 0;
increment = skip_time;
- Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), Array.Empty(), 0)
+ var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+ working.LoadTrack();
+
+ Child = gameplayClockContainer = new GameplayClockContainer(working, 0)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs
index 9f1492a25f..5a2b8d22fd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs
@@ -22,19 +22,32 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneStoryboard : OsuTestScene
{
- private readonly Container storyboardContainer;
+ private Container storyboardContainer;
private DrawableStoryboard storyboard;
- [Cached]
- private MusicController musicController = new MusicController();
+ [Test]
+ public void TestStoryboard()
+ {
+ AddStep("Restart", restart);
+ AddToggleStep("Passing", passing =>
+ {
+ if (storyboard != null) storyboard.Passing = passing;
+ });
+ }
- public TestSceneStoryboard()
+ [Test]
+ public void TestStoryboardMissingVideo()
+ {
+ AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
{
Clock = new FramedClock();
AddRange(new Drawable[]
{
- musicController,
new Container
{
RelativeSizeAxes = Axes.Both,
@@ -58,32 +71,11 @@ namespace osu.Game.Tests.Visual.Gameplay
State = { Value = Visibility.Visible },
}
});
+
+ Beatmap.BindValueChanged(beatmapChanged, true);
}
- [Test]
- public void TestStoryboard()
- {
- AddStep("Restart", restart);
- AddToggleStep("Passing", passing =>
- {
- if (storyboard != null) storyboard.Passing = passing;
- });
- }
-
- [Test]
- public void TestStoryboardMissingVideo()
- {
- AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Beatmap.ValueChanged += beatmapChanged;
- }
-
- private void beatmapChanged(ValueChangedEvent e)
- => loadStoryboard(e.NewValue);
+ private void beatmapChanged(ValueChangedEvent e) => loadStoryboard(e.NewValue);
private void restart()
{
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
index 8f20e38494..5f135febf4 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
@@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Audio.Track;
using osu.Framework.Screens;
+using osu.Framework.Utils;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
@@ -15,11 +15,9 @@ namespace osu.Game.Tests.Visual.Menus
public TestSceneIntroWelcome()
{
- AddUntilStep("wait for load", () => getTrack() != null);
-
- AddAssert("check if menu music loops", () => getTrack().Looping);
+ AddUntilStep("wait for load", () => MusicController.TrackLoaded);
+ AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1));
+ AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping);
}
-
- private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs
index d7f23f5cc0..4b22af38c5 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
@@ -11,14 +10,10 @@ namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneSongTicker : OsuTestScene
{
- [Cached]
- private MusicController musicController = new MusicController();
-
public TestSceneSongTicker()
{
AddRange(new Drawable[]
{
- musicController,
new SongTicker
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index b4985cad9f..2a4486812c 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -4,8 +4,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Overlays;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osuTK.Input;
@@ -15,7 +17,7 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneToolbar : OsuManualInputManagerTestScene
{
- private Toolbar toolbar;
+ private TestToolbar toolbar;
[Resolved]
private RulesetStore rulesets { get; set; }
@@ -23,7 +25,7 @@ namespace osu.Game.Tests.Visual.Menus
[SetUp]
public void SetUp() => Schedule(() =>
{
- Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } };
+ Child = toolbar = new TestToolbar { State = { Value = Visibility.Visible } };
});
[Test]
@@ -72,5 +74,24 @@ namespace osu.Game.Tests.Visual.Menus
AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected));
}
}
+
+ [TestCase(OverlayActivation.All)]
+ [TestCase(OverlayActivation.Disabled)]
+ public void TestRespectsOverlayActivation(OverlayActivation mode)
+ {
+ AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode);
+ AddStep("hide toolbar", () => toolbar.Hide());
+ AddStep("try to show toolbar", () => toolbar.Show());
+
+ if (mode == OverlayActivation.Disabled)
+ AddAssert("toolbar still hidden", () => toolbar.State.Value == Visibility.Hidden);
+ else
+ AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible);
+ }
+
+ public class TestToolbar : Toolbar
+ {
+ public new Bindable OverlayActivationMode => base.OverlayActivationMode as Bindable;
+ }
}
}
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/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs
index 61859c9da3..3924b0333f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -10,11 +12,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
protected override bool UseOnlineAPI => true;
+ [Cached]
+ private MusicController musicController { get; set; } = new MusicController();
+
public TestSceneMultiScreen()
{
Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer();
- AddStep(@"show", () => LoadScreen(multi));
+ AddStep("show", () => LoadScreen(multi));
+ AddUntilStep("wait for loaded", () => multi.IsLoaded);
}
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 8ccaca8630..73a833c15d 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
@@ -46,7 +45,6 @@ namespace osu.Game.Tests.Visual.Navigation
Player player = null;
WorkingBeatmap beatmap() => Game.Beatmap.Value;
- Track track() => beatmap().Track;
PushAndConfirm(() => new TestSongSelect());
@@ -62,30 +60,27 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddUntilStep("wait for fail", () => player.HasFailed);
- AddUntilStep("wait for track stop", () => !track().IsRunning);
- AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
+ AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
+ AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
pushEscape();
- AddUntilStep("wait for track playing", () => track().IsRunning);
- AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
+ AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying);
+ AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
}
[Test]
public void TestMenuMakesMusic()
{
- WorkingBeatmap beatmap() => Game.Beatmap.Value;
- Track track() => beatmap().Track;
-
TestSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect());
- AddUntilStep("wait for no track", () => track() is TrackVirtual);
+ AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
AddStep("return to menu", () => songSelect.Exit());
- AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning);
+ AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying);
}
[Test]
@@ -140,12 +135,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded);
AddStep("Seek close to end", () =>
{
- Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000);
- Game.Beatmap.Value.Track.Completed += () => trackCompleted = true;
+ Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000);
+ Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true;
});
AddUntilStep("Track was completed", () => trackCompleted);
- AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning);
+ AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying);
}
private void pushEscape() =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
index 4cb22bf1fe..fd5c188b94 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
@@ -7,8 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Screens.Select.Details;
@@ -72,6 +74,32 @@ namespace osu.Game.Tests.Visual.Online
};
}
+ [Test]
+ public void TestOnlyFailMetrics()
+ {
+ AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo
+ {
+ Metrics = new BeatmapMetrics
+ {
+ Fails = Enumerable.Range(1, 100).ToArray(),
+ }
+ });
+ AddAssert("graph max values correct",
+ () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100));
+ }
+
+ [Test]
+ public void TestEmptyMetrics()
+ {
+ AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo
+ {
+ Metrics = new BeatmapMetrics()
+ });
+
+ AddAssert("graph max values correct",
+ () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 0));
+ }
+
private class GraphExposingSuccessRate : SuccessRate
{
public new FailRetryGraph Graph => base.Graph;
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/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
index 103308d34d..9662bd65b4 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
- AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null)
+ AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)
{
BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
});
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
index 7ca1fc842f..144f8da2fa 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
@@ -35,6 +35,18 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(new List());
}
+ [Test]
+ public void TestMissesDontShow()
+ {
+ createTest(Enumerable.Range(0, 100).Select(i =>
+ {
+ if (i % 2 == 0)
+ return new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null);
+
+ return new HitEvent(30, HitResult.Miss, new HitCircle(), new HitCircle(), null);
+ }).ToList());
+ }
+
private void createTest(List events) => AddStep("create test", () =>
{
Children = new Drawable[]
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 74808bc2f5..03cb5fa3db 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
@@ -212,6 +213,25 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
}
+ [Test]
+ public void TestDownloadButtonInitiallyDisabled()
+ {
+ TestResultsScreen screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+
+ AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click contracted panel", () =>
+ {
+ var contractedPanel = this.ChildrenOfType().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
+ InputManager.MoveMouseTo(contractedPanel);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value);
+ }
+
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
@@ -255,6 +275,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
score.TotalScore += 10 - i;
+ score.Hash = $"test{i}";
scores.Add(score);
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs
new file mode 100644
index 0000000000..07a0bcc8d8
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs
@@ -0,0 +1,68 @@
+// 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 Humanizer;
+using NUnit.Framework;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Screens.Ranking.Statistics;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public class TestSceneSimpleStatisticTable : OsuTestScene
+ {
+ private Container container;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = new Container
+ {
+ AutoSizeAxes = Axes.Y,
+ Width = 700,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#333"),
+ },
+ container = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding(20)
+ }
+ }
+ };
+ });
+
+ [Test]
+ public void TestEmpty()
+ {
+ AddStep("create with no items",
+ () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty())));
+ }
+
+ [Test]
+ public void TestManyItems(
+ [Values(1, 2, 3, 4, 12)] int itemCount,
+ [Values(1, 3, 5)] int columnCount)
+ {
+ AddStep($"create with {"item".ToQuantity(itemCount)}", () =>
+ {
+ var items = Enumerable.Range(1, itemCount)
+ .Select(i => new SimpleStatisticItem($"Statistic #{i}")
+ {
+ Value = RNG.Next(100)
+ });
+
+ container.Add(new SimpleStatisticTable(columnCount, items));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index e06b3a8a7e..987a4a67fe 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -68,22 +68,22 @@ namespace osu.Game.Tests.Visual.Settings
[Test]
public void TestClearButtonOnBindings()
{
- KeyBindingRow backBindingRow = null;
+ KeyBindingRow multiBindingRow = null;
- AddStep("click back binding row", () =>
+ AddStep("click first row with two bindings", () =>
{
- backBindingRow = panel.ChildrenOfType().ElementAt(10);
- InputManager.MoveMouseTo(backBindingRow);
+ multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1);
+ InputManager.MoveMouseTo(multiBindingRow);
InputManager.Click(MouseButton.Left);
});
clickClearButton();
- AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().First().Text.Text));
+ AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().First().Text.Text));
AddStep("click second binding", () =>
{
- var target = backBindingRow.ChildrenOfType().ElementAt(1);
+ var target = multiBindingRow.ChildrenOfType().ElementAt(1);
InputManager.MoveMouseTo(target);
InputManager.Click(MouseButton.Left);
@@ -91,13 +91,13 @@ namespace osu.Game.Tests.Visual.Settings
clickClearButton();
- AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().ElementAt(1).Text.Text));
+ AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text));
void clickClearButton()
{
AddStep("click clear button", () =>
{
- var clearButton = backBindingRow.ChildrenOfType().Single();
+ var clearButton = multiBindingRow.ChildrenOfType().Single();
InputManager.MoveMouseTo(clearButton);
InputManager.Click(MouseButton.Left);
@@ -108,20 +108,20 @@ namespace osu.Game.Tests.Visual.Settings
[Test]
public void TestClickRowSelectsFirstBinding()
{
- KeyBindingRow backBindingRow = null;
+ KeyBindingRow multiBindingRow = null;
- AddStep("click back binding row", () =>
+ AddStep("click first row with two bindings", () =>
{
- backBindingRow = panel.ChildrenOfType().ElementAt(10);
- InputManager.MoveMouseTo(backBindingRow);
+ multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1);
+ InputManager.MoveMouseTo(multiBindingRow);
InputManager.Click(MouseButton.Left);
});
- AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding);
+ AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding);
AddStep("click second binding", () =>
{
- var target = backBindingRow.ChildrenOfType().ElementAt(1);
+ var target = multiBindingRow.ChildrenOfType().ElementAt(1);
InputManager.MoveMouseTo(target);
InputManager.Click(MouseButton.Left);
@@ -129,12 +129,12 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("click back binding row", () =>
{
- backBindingRow = panel.ChildrenOfType().ElementAt(10);
- InputManager.MoveMouseTo(backBindingRow);
+ multiBindingRow = panel.ChildrenOfType().ElementAt(10);
+ InputManager.MoveMouseTo(multiBindingRow);
InputManager.Click(MouseButton.Left);
});
- AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding);
+ AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding);
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
index dd5ceec739..82b7e65c4f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
@@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private readonly NowPlayingOverlay np;
- [Cached]
- private MusicController musicController = new MusicController();
-
public TestSceneBeatSyncedContainer()
{
Clock = new FramedClock();
@@ -36,7 +33,6 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRange(new Drawable[]
{
- musicController,
new BeatContainer
{
Anchor = Anchor.BottomCentre,
@@ -71,6 +67,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly Box flashLayer;
+ [Resolved]
+ private MusicController musicController { get; set; }
+
public BeatContainer()
{
RelativeSizeAxes = Axes.X;
@@ -165,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (timingPoints.Count == 0) return 0;
if (timingPoints[^1] == current)
- return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength);
+ return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength);
return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index eb4750a597..e54292f7cc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
- dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, Audio, dependencies.Get(), Beatmap.Default));
+ dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), dependencies.Get(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0];
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/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
index 532744a0fc..c14a1ddbf2 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -11,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -20,8 +22,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private MusicController musicController = new MusicController();
- private WorkingBeatmap currentBeatmap;
-
private NowPlayingOverlay nowPlayingOverlay;
private RulesetStore rulesets;
@@ -76,16 +76,21 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}).Wait(), 5);
- AddStep(@"Next track", () => musicController.NextTrack());
- AddStep("Store track", () => currentBeatmap = Beatmap.Value);
+ WorkingBeatmap currentBeatmap = null;
+
+ AddStep("import beatmap with track", () =>
+ {
+ var setWithTrack = manager.Import(TestResources.GetTestBeatmapForImport()).Result;
+ Beatmap.Value = currentBeatmap = manager.GetWorkingBeatmap(setWithTrack.Beatmaps.First());
+ });
AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000));
- AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000);
+ AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack.CurrentTime > 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value);
- AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000);
+ AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack.CurrentTime < 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value);
diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs
index 90c91eb007..7dc5ce1d7f 100644
--- a/osu.Game.Tests/WaveformTestBeatmap.cs
+++ b/osu.Game.Tests/WaveformTestBeatmap.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
- protected override Track GetTrack() => trackStore.Get(firstAudioFile);
+ protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);
private string firstAudioFile
{
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index d767973528..c692bcd5e4 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/Components/TestSceneDateTextBox.cs b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs
new file mode 100644
index 0000000000..33165d385a
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Components;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tournament.Tests.Components
+{
+ public class TestSceneDateTextBox : OsuManualInputManagerTestScene
+ {
+ private DateTextBox textBox;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = textBox = new DateTextBox
+ {
+ Width = 0.3f
+ };
+ });
+
+ [Test]
+ public void TestCommitWithoutSettingBindable()
+ {
+ AddStep("click textbox", () =>
+ {
+ InputManager.MoveMouseTo(textBox);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("unfocus", () =>
+ {
+ InputManager.MoveMouseTo(Vector2.Zero);
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 95f5deb2cc..5d55196dcf 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.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs
index ee7e350970..aee5241e35 100644
--- a/osu.Game.Tournament/Components/DateTextBox.cs
+++ b/osu.Game.Tournament/Components/DateTextBox.cs
@@ -22,11 +22,12 @@ namespace osu.Game.Tournament.Components
}
// hold a reference to the provided bindable so we don't have to in every settings section.
- private Bindable bindable;
+ private Bindable bindable = new Bindable();
public DateTextBox()
{
base.Bindable = new Bindable();
+
((OsuTextBox)Control).OnCommit = (sender, newText) =>
{
try
diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
index f3eecf8afe..efec4cffdd 100644
--- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
@@ -26,6 +26,8 @@ namespace osu.Game.Tournament.Screens.Editors
[Cached]
private LadderEditorInfo editorInfo = new LadderEditorInfo();
+ private WarningBox rightClickMessage;
+
protected override bool DrawLoserPaths => true;
[BackgroundDependencyLoader]
@@ -37,6 +39,16 @@ namespace osu.Game.Tournament.Screens.Editors
Origin = Anchor.TopRight,
Margin = new MarginPadding(5)
});
+
+ AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches"));
+
+ LadderInfo.Matches.CollectionChanged += (_, __) => updateMessage();
+ updateMessage();
+ }
+
+ private void updateMessage()
+ {
+ rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1;
}
public void BeginJoin(TournamentMatch match, bool losers)
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs
index 2e7484542a..695c6d6f3e 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
@@ -127,21 +128,29 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
private class MatchScoreCounter : ScoreCounter
{
+ private OsuSpriteText displayedSpriteText;
+
public MatchScoreCounter()
{
Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
-
- Winning = false;
-
- DisplayedCountSpriteText.Spacing = new Vector2(-6);
}
public bool Winning
{
- set => DisplayedCountSpriteText.Font = value
+ set => updateFont(value);
+ }
+
+ protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
+ {
+ displayedSpriteText = s;
+ displayedSpriteText.Spacing = new Vector2(-6);
+ updateFont(false);
+ });
+
+ private void updateFont(bool winning)
+ => displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true);
- }
}
}
}
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index 7b1a174c1e..bbe4a53d8f 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Tournament
public static readonly Color4 TEXT_COLOUR = Color4Extensions.FromHex("#fff");
private Drawable heightWarning;
private Bindable windowSize;
+ private Bindable windowMode;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig)
@@ -43,6 +44,12 @@ namespace osu.Game.Tournament
heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
}), true);
+ windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode);
+ windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
+ {
+ windowMode.Value = WindowMode.Windowed;
+ }), true);
+
AddRange(new[]
{
new Container
@@ -80,30 +87,7 @@ namespace osu.Game.Tournament
},
}
},
- heightWarning = new Container
- {
- Masking = true,
- CornerRadius = 5,
- Depth = float.MinValue,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = Color4.Red,
- RelativeSizeAxes = Axes.Both,
- },
- new TournamentSpriteText
- {
- Text = "Please make the window wider",
- Font = OsuFont.Torus.With(weight: FontWeight.Bold),
- Colour = Color4.White,
- Padding = new MarginPadding(20)
- }
- }
- },
+ heightWarning = new WarningBox("Please make the window wider"),
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Tournament/WarningBox.cs b/osu.Game.Tournament/WarningBox.cs
new file mode 100644
index 0000000000..814482aea4
--- /dev/null
+++ b/osu.Game.Tournament/WarningBox.cs
@@ -0,0 +1,40 @@
+// 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;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament
+{
+ internal class WarningBox : Container
+ {
+ public WarningBox(string text)
+ {
+ Masking = true;
+ CornerRadius = 5;
+ Depth = float.MinValue;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ AutoSizeAxes = Axes.Both;
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Red,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new TournamentSpriteText
+ {
+ Text = text,
+ Font = OsuFont.Torus.With(weight: FontWeight.Bold),
+ Colour = Color4.White,
+ Padding = new MarginPadding(20)
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
index b80b4e45ed..0100c9b210 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
return existing;
- return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken,
- TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
+ return await Task.Factory.StartNew(() =>
+ {
+ // Computation may have finished in a previous task.
+ if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _))
+ return existing;
+
+ return computeDifficulty(key, beatmapInfo, rulesetInfo);
+ }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
}
///
@@ -245,7 +251,7 @@ namespace osu.Game.Beatmaps
updateScheduler?.Dispose();
}
- private readonly struct DifficultyCacheLookup : IEquatable
+ public readonly struct DifficultyCacheLookup : IEquatable
{
public readonly int BeatmapId;
public readonly int RulesetId;
@@ -261,7 +267,7 @@ namespace osu.Game.Beatmaps
public bool Equals(DifficultyCacheLookup other)
=> BeatmapId == other.BeatmapId
&& RulesetId == other.RulesetId
- && Mods.SequenceEqual(other.Mods);
+ && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym));
public override int GetHashCode()
{
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index b4b341634c..0cadcf5947 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -9,6 +9,7 @@ using System.Linq.Expressions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
@@ -63,16 +64,16 @@ namespace osu.Game.Beatmaps
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager;
- private readonly GameHost host;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
+ private readonly TextureStore textureStore;
+ private readonly ITrackStore trackStore;
- public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
+ public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
{
this.rulesets = rulesets;
this.audioManager = audioManager;
- this.host = host;
DefaultBeatmap = defaultBeatmap;
@@ -83,6 +84,9 @@ namespace osu.Game.Beatmaps
beatmaps.ItemUpdated += removeWorkingCache;
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
+
+ textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
+ trackStore = audioManager.GetTrackStore(Files.Store);
}
protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
@@ -218,7 +222,7 @@ namespace osu.Game.Beatmaps
removeWorkingCache(info);
}
- private readonly WeakList workingCache = new WeakList();
+ private readonly WeakList workingCache = new WeakList();
///
/// Retrieve a instance for the provided
@@ -246,16 +250,13 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
+ if (working != null)
+ return working;
- if (working == null)
- {
- beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
+ beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
- workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store,
- new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager));
- }
+ workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager));
- previous?.TransferTo(working);
return working;
}
}
@@ -459,7 +460,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null;
- protected override Track GetTrack() => null;
+ protected override Track GetBeatmapTrack() => null;
}
}
diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
index 39c5ccab27..92199789ec 100644
--- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
@@ -17,15 +17,18 @@ namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
- protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap
+ private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
private readonly IResourceStore store;
+ private readonly TextureStore textureStore;
+ private readonly ITrackStore trackStore;
- public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, BeatmapInfo beatmapInfo, AudioManager audioManager)
+ public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager)
: base(beatmapInfo, audioManager)
{
this.store = store;
this.textureStore = textureStore;
+ this.trackStore = trackStore;
}
protected override IBeatmap GetBeatmap()
@@ -44,10 +47,6 @@ namespace osu.Game.Beatmaps
private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
- private TextureStore textureStore;
-
- private ITrackStore trackStore;
-
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
protected override Texture GetBackground()
@@ -66,11 +65,11 @@ namespace osu.Game.Beatmaps
}
}
- protected override Track GetTrack()
+ protected override Track GetBeatmapTrack()
{
try
{
- return (trackStore ??= AudioManager.GetTrackStore(store)).Get(getPathForFile(Metadata.AudioFile));
+ return trackStore.Get(getPathForFile(Metadata.AudioFile));
}
catch (Exception e)
{
@@ -79,22 +78,6 @@ namespace osu.Game.Beatmaps
}
}
- public override void RecycleTrack()
- {
- base.RecycleTrack();
-
- trackStore?.Dispose();
- trackStore = null;
- }
-
- public override void TransferTo(WorkingBeatmap other)
- {
- base.TransferTo(other);
-
- if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo))
- owb.textureStore = textureStore;
- }
-
protected override Waveform GetWaveform()
{
try
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/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 8080e94075..af2a2ac250 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -19,7 +20,7 @@ namespace osu.Game.Beatmaps
{
private readonly TextureStore textures;
- public DummyWorkingBeatmap(AudioManager audio, TextureStore textures)
+ public DummyWorkingBeatmap([NotNull] AudioManager audio, TextureStore textures)
: base(new BeatmapInfo
{
Metadata = new BeatmapMetadata
@@ -44,7 +45,7 @@ namespace osu.Game.Beatmaps
protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
- protected override Track GetTrack() => GetVirtualTrack();
+ protected override Track GetBeatmapTrack() => GetVirtualTrack();
private class DummyRulesetInfo : RulesetInfo
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 44ef9bcacc..c15240a4f6 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats
try
{
byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
-
- if (alpha == 0)
- alpha = 255;
-
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha);
}
catch
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index 31975157a0..bcd94d76fd 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -26,11 +26,6 @@ namespace osu.Game.Beatmaps
///
Texture Background { get; }
- ///
- /// Retrieves the audio track for this .
- ///
- Track Track { get; }
-
///
/// Retrieves the for the of this .
///
@@ -59,5 +54,18 @@ namespace osu.Game.Beatmaps
/// The converted .
/// If could not be converted to .
IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null);
+
+ ///
+ /// Load a new audio track instance for this beatmap. This should be called once before accessing .
+ /// The caller of this method is responsible for the lifetime of the track.
+ ///
+ ///
+ /// In a standard game context, the loading of the track is managed solely by MusicController, which will
+ /// automatically load the track of the current global IBindable WorkingBeatmap.
+ /// As such, this method should only be called in very special scenarios, such as external tests or apps which are
+ /// outside of the game context.
+ ///
+ /// A fresh track instance, which will also be available via .
+ Track LoadTrack();
}
}
diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs
index 583e950e49..0e517ea3df 100644
--- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs
+++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs
@@ -38,5 +38,6 @@ namespace osu.Game.Beatmaps.Legacy
Key1 = 1 << 26,
Key3 = 1 << 27,
Key2 = 1 << 28,
+ Mirror = 1 << 30,
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index ac399e37c4..6a161e6e04 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
@@ -40,7 +41,6 @@ namespace osu.Game.Beatmaps
BeatmapSetInfo = beatmapInfo.BeatmapSet;
Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
- track = new RecyclableLazy
-
-
+
+
@@ -80,7 +80,7 @@
-
+