Merge branch 'master' into realm-key-binding-store

This commit is contained in:
Dean Herbert 2021-06-10 13:58:08 +09:00
commit 9044a20120
340 changed files with 5939 additions and 2341 deletions

View File

@ -10,14 +10,6 @@ trim_trailing_whitespace = true
#Roslyn naming styles #Roslyn naming styles
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members #camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_style.camelcase.capitalization = camel_case

View File

@ -1,7 +0,0 @@
---
name: Feature Request
about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
**Proposal designs of the feature:**

View File

@ -1,5 +1,12 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues - name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here. about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.

View File

@ -97,7 +97,7 @@ Before committing your code, please run a code formatter. This can be achieved b
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself. We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`, which is [only supported on Windows](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice. JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing ## Contributing

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
} }
} }

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.EmptyFreeform.Replays; using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.EmptyFreeform.Mods namespace osu.Game.Rulesets.EmptyFreeform.Mods
{ {
public class EmptyFreeformModAutoplay : ModAutoplay<EmptyFreeformHitObject> public class EmptyFreeformModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods namespace osu.Game.Rulesets.Pippidon.Mods
{ {
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject> public class PippidonModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
} }
} }

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
} }
} }

View File

@ -3,7 +3,6 @@
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.EmptyScrolling.Replays; using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
@ -11,7 +10,7 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.EmptyScrolling.Mods namespace osu.Game.Rulesets.EmptyScrolling.Mods
{ {
public class EmptyScrollingModAutoplay : ModAutoplay<EmptyScrollingHitObject> public class EmptyScrollingModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods namespace osu.Game.Rulesets.Pippidon.Mods
{ {
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject> public class PippidonModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
} }
} }

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.524.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.609.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -20,7 +20,8 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {

View File

@ -57,7 +57,7 @@ namespace osu.Desktop
private string getStableInstallPath() private string getStableInstallPath()
{ {
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")); static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
string stableInstallPath; string stableInstallPath;

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="nunit" Version="3.13.2" /> <PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup> </ItemGroup>

View File

@ -4,10 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneCatchModHidden : ModTestScene public class TestSceneCatchModHidden : ModTestScene
{ {
[BackgroundDependencyLoader]
private void load()
{
LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
}
[Test] [Test]
public void TestJuiceStream() public void TestJuiceStream()
{ {

View File

@ -1,8 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
@ -10,5 +18,22 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
{ {
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
{
if (withModifiedSkin)
{
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit());
CreateTest(null);
}
AddAssert("legacy HUD combo counter hidden", () =>
{
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
});
}
} }
} }

View File

@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true)); AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit())); AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () => AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour); catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
} }
[Test] [Test]

View File

@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = circleSize CircleSize = circleSize
}; };
SetContents(() => SetContents(_ =>
{ {
var droppedObjectContainer = new Container<CaughtObject> var droppedObjectContainer = new Container<CaughtObject>
{ {

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
scoreProcessor = new ScoreProcessor(); scoreProcessor = new ScoreProcessor();
SetContents(() => new CatchComboDisplay SetContents(_ => new CatchComboDisplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -19,22 +19,22 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
base.LoadComplete(); base.LoadComplete();
AddStep("show pear", () => SetContents(() => createDrawableFruit(0))); AddStep("show pear", () => SetContents(_ => createDrawableFruit(0)));
AddStep("show grape", () => SetContents(() => createDrawableFruit(1))); AddStep("show grape", () => SetContents(_ => createDrawableFruit(1)));
AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2))); AddStep("show pineapple / apple", () => SetContents(_ => createDrawableFruit(2)));
AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3))); AddStep("show raspberry / orange", () => SetContents(_ => createDrawableFruit(3)));
AddStep("show banana", () => SetContents(createDrawableBanana)); AddStep("show banana", () => SetContents(_ => createDrawableBanana()));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show droplet", () => SetContents(_ => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); AddStep("show tiny droplet", () => SetContents(_ => createDrawableTinyDroplet()));
AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true))); AddStep("show hyperdash pear", () => SetContents(_ => createDrawableFruit(0, true)));
AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true))); AddStep("show hyperdash grape", () => SetContents(_ => createDrawableFruit(1, true)));
AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true))); AddStep("show hyperdash pineapple / apple", () => SetContents(_ => createDrawableFruit(2, true)));
AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true))); AddStep("show hyperdash raspberry / orange", () => SetContents(_ => createDrawableFruit(3, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true)));
} }
private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) => private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override void LoadComplete() protected override void LoadComplete()
{ {
AddStep("fruit changes visual and hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit AddStep("fruit changes visual and hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
{ {
IndexInBeatmapBindable = { BindTarget = indexInBeatmap }, IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash }, HyperDashBindable = { BindTarget = hyperDash },
})))); }))));
AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet AddStep("droplet changes hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
{ {
HyperDashBindable = { BindTarget = hyperDash }, HyperDashBindable = { BindTarget = hyperDash },
})))); }))));

View File

@ -32,28 +32,28 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(true, false)] [TestCase(true, false)]
[TestCase(false, true)] [TestCase(false, true)]
[TestCase(false, false)] [TestCase(false, false)]
public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin); ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin) public void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
base.TestBeatmapComboColoursOverride(useBeatmapSkin); ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin); ConfigureTest(useBeatmapSkin, false, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
} }
@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)] [TestCase(false, true)]
[TestCase(true, false)] [TestCase(true, false)]
[TestCase(false, false)] [TestCase(false, false)]
public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour); ConfigureTest(useBeatmapSkin, useBeatmapColour, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
} }
@ -72,10 +72,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)] [TestCase(false, true)]
[TestCase(true, false)] [TestCase(true, false)]
[TestCase(false, false)] [TestCase(false, false)]
public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour); ConfigureTest(useBeatmapSkin, useBeatmapColour, true);
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
} }
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)] [TestCase(false)]
public void TestBeatmapHyperDashColours(bool useBeatmapSkin) public void TestBeatmapHyperDashColours(bool useBeatmapSkin)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, true, true); ConfigureTest(useBeatmapSkin, true, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR); AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)] [TestCase(false)]
public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin) public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin)
{ {
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, false, true); ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR); AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -161,13 +161,13 @@ namespace osu.Game.Rulesets.Catch
switch (result) switch (result)
{ {
case HitResult.LargeTickHit: case HitResult.LargeTickHit:
return "large droplet"; return "Large droplet";
case HitResult.SmallTickHit: case HitResult.SmallTickHit:
return "small droplet"; return "Small droplet";
case HitResult.LargeBonus: case HitResult.LargeBonus:
return "banana"; return "Banana";
} }
return base.GetDisplayNameForHitResult(result); return base.GetDisplayNameForHitResult(result);

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
} }
} }
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[] return new Skill[]
{ {
new Movement(mods, halfCatcherWidth), new Movement(mods, halfCatcherWidth, clockRate),
}; };
} }

View File

@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
/// </summary> /// </summary>
public readonly double StrainTime; public readonly double StrainTime;
public readonly double ClockRate;
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate) : base(hitObject, lastObject, clockRate)
{ {
@ -37,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime); StrainTime = Math.Max(40, DeltaTime);
ClockRate = clockRate;
} }
} }
} }

View File

@ -28,10 +28,21 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved; private float lastDistanceMoved;
private double lastStrainTime; private double lastStrainTime;
public Movement(Mod[] mods, float halfCatcherWidth) /// <summary>
/// The speed multiplier applied to the player's catcher.
/// </summary>
private readonly double catcherSpeedMultiplier;
public Movement(Mod[] mods, float halfCatcherWidth, double clockRate)
: base(mods) : base(mods)
{ {
HalfCatcherWidth = halfCatcherWidth; HalfCatcherWidth = halfCatcherWidth;
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
// but also the speed of the player's catcher, which has an impact on difficulty
// TODO: Support variable clockrates caused by mods such as ModTimeRamp
// (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing)
catcherSpeedMultiplier = clockRate;
} }
protected override double StrainValueOf(DifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
@ -48,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value; float distanceMoved = playerPosition - lastPlayerPosition.Value;
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate); double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime); double sqrtStrain = Math.Sqrt(weightedStrainTime);
@ -81,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition; playerPosition = catchCurrent.NormalizedPosition;
} }
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
} }
lastPlayerPosition = playerPosition; lastPlayerPosition = playerPosition;

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModAutoplay : ModAutoplay<CatchHitObject> public class CatchModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Mods
} }
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ => ApplyNormalVisibilityState(hitObject, state);
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
@ -9,21 +8,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
/// <summary> /// <summary>
/// Represents a <see cref="Fruit"/> caught by the catcher. /// Represents a <see cref="Fruit"/> caught by the catcher.
/// </summary> /// </summary>
public class CaughtFruit : CaughtObject, IHasFruitState public class CaughtFruit : CaughtObject
{ {
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public CaughtFruit() public CaughtFruit()
: base(CatchSkinComponents.Fruit, _ => new FruitPiece()) : base(CatchSkinComponents.Fruit, _ => new FruitPiece())
{ {
} }
public override void CopyStateFrom(IHasCatchObjectState objectState)
{
base.CopyStateFrom(objectState);
var fruitState = (IHasFruitState)objectState;
VisualRepresentation.Value = fruitState.VisualRepresentation.Value;
}
} }
} }

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public PalpableCatchHitObject HitObject { get; private set; } public PalpableCatchHitObject HitObject { get; private set; }
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>(); public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>(); public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
public Vector2 DisplaySize => Size * Scale; public Vector2 DisplaySize => Size * Scale;
@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Rotation = objectState.DisplayRotation; Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value; AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value; HyperDash.Value = objectState.HyperDash.Value;
IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
} }
protected override void FreeAfterUse() protected override void FreeAfterUse()

View File

@ -3,17 +3,14 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState public class DrawableFruit : DrawablePalpableCatchHitObject
{ {
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public DrawableFruit() public DrawableFruit()
: this(null) : this(null)
{ {
@ -27,11 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
IndexInBeatmap.BindValueChanged(change =>
{
VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
}, true);
ScalingContainer.Child = new SkinnableDrawable( ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Fruit), new CatchSkinComponent(CatchSkinComponents.Fruit),
_ => new FruitPiece()); _ => new FruitPiece());
@ -44,12 +36,4 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
} }
} }
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
}
} }

View File

@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Bindable<bool> HyperDash { get; } Bindable<bool> HyperDash { get; }
Bindable<int> IndexInBeatmap { get; }
Vector2 DisplaySize { get; } Vector2 DisplaySize { get; }
float DisplayRotation { get; } float DisplayRotation { get; }

View File

@ -1,15 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Provides a visual state of a <see cref="Fruit"/>.
/// </summary>
public interface IHasFruitState : IHasCatchObjectState
{
Bindable<FruitVisualRepresentation> VisualRepresentation { get; }
}
}

View File

@ -9,5 +9,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public class Fruit : PalpableCatchHitObject public class Fruit : PalpableCatchHitObject
{ {
public override Judgement CreateJudgement() => new CatchJudgement(); public override Judgement CreateJudgement() => new CatchJudgement();
public static FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
} }
} }

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Catch.Objects
{
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchHitWindows : HitWindows
{
public override bool IsHitResultAllowed(HitResult result)
{
switch (result)
{
case HitResult.Great:
case HitResult.Miss:
return true;
}
return false;
}
}
}

View File

@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(); public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>(); public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
[Resolved] [Resolved]
protected IHasCatchObjectState ObjectState { get; private set; } protected IHasCatchObjectState ObjectState { get; private set; }
@ -37,6 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
AccentColour.BindTo(ObjectState.AccentColour); AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash); HyperDash.BindTo(ObjectState.HyperDash);
IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap);
HyperDash.BindValueChanged(hyper => HyperDash.BindValueChanged(hyper =>
{ {

View File

@ -3,7 +3,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Skinning.Default namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
@ -39,8 +39,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
base.LoadComplete(); base.LoadComplete();
var fruitState = (IHasFruitState)ObjectState; IndexInBeatmap.BindValueChanged(index =>
VisualRepresentation.BindTo(fruitState.VisualRepresentation); {
VisualRepresentation.Value = Fruit.GetVisualRepresentation(index.NewValue);
}, true);
} }
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default namespace osu.Game.Rulesets.Catch.Skinning.Default

View File

@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -22,38 +24,46 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (component is HUDSkinComponent hudComponent) if (component is SkinnableTargetComponent targetComponent)
{ {
switch (hudComponent.Component) switch (targetComponent.Target)
{
case SkinnableTarget.MainHUDComponents:
var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
if (providesComboCounter && components != null)
{ {
case HUDSkinComponents.ComboCounter:
// catch may provide its own combo counter; hide the default. // catch may provide its own combo counter; hide the default.
return providesComboCounter ? Drawable.Empty() : null; // todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>())
legacyComboCounter.HiddenByRulesetImplementation = false;
}
return components;
} }
} }
if (!(component is CatchSkinComponent catchSkinComponent)) if (component is CatchSkinComponent catchSkinComponent)
return null; {
switch (catchSkinComponent.Component) switch (catchSkinComponent.Component)
{ {
case CatchSkinComponents.Fruit: case CatchSkinComponents.Fruit:
if (GetTexture("fruit-pear") != null) if (GetTexture("fruit-pear") != null)
return new LegacyFruitPiece(); return new LegacyFruitPiece();
break; return null;
case CatchSkinComponents.Banana: case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null) if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece(); return new LegacyBananaPiece();
break; return null;
case CatchSkinComponents.Droplet: case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null) if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece(); return new LegacyDropletPiece();
break; return null;
case CatchSkinComponents.CatcherIdle: case CatchSkinComponents.CatcherIdle:
return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
@ -71,10 +81,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
if (providesComboCounter) if (providesComboCounter)
return new LegacyCatchComboCounter(Source); return new LegacyCatchComboCounter(Source);
break; return null;
}
} }
return null; return Source.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -3,7 +3,7 @@
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class LegacyBananaPiece : LegacyCatchHitObjectPiece public class LegacyBananaPiece : LegacyCatchHitObjectPiece
{ {

View File

@ -13,12 +13,13 @@ using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public abstract class LegacyCatchHitObjectPiece : PoolableDrawable public abstract class LegacyCatchHitObjectPiece : PoolableDrawable
{ {
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(); public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>(); public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
private readonly Sprite colouredSprite; private readonly Sprite colouredSprite;
private readonly Sprite overlaySprite; private readonly Sprite overlaySprite;
@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
AccentColour.BindTo(ObjectState.AccentColour); AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash); HyperDash.BindTo(ObjectState.HyperDash);
IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap);
hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ?? hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??

View File

@ -4,7 +4,7 @@
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class LegacyDropletPiece : LegacyCatchHitObjectPiece public class LegacyDropletPiece : LegacyCatchHitObjectPiece
{ {

View File

@ -1,23 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
internal class LegacyFruitPiece : LegacyCatchHitObjectPiece internal class LegacyFruitPiece : LegacyCatchHitObjectPiece
{ {
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
var fruitState = (IHasFruitState)ObjectState; IndexInBeatmap.BindValueChanged(index =>
VisualRepresentation.BindTo(fruitState.VisualRepresentation); {
setTexture(Fruit.GetVisualRepresentation(index.NewValue));
VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); }, true);
} }
private void setTexture(FruitVisualRepresentation visualRepresentation) private void setTexture(FruitVisualRepresentation visualRepresentation)

View File

@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
ComboCounter?.UpdateCombo(currentCombo); ComboCounter?.UpdateCombo(currentCombo);
} }

View File

@ -126,8 +126,7 @@ namespace osu.Game.Rulesets.Catch.UI
private float hyperDashTargetPosition; private float hyperDashTargetPosition;
private Bindable<bool> hitLighting; private Bindable<bool> hitLighting;
private readonly DrawablePool<HitExplosion> hitExplosionPool; private readonly HitExplosionContainer hitExplosionContainer;
private readonly Container<HitExplosion> hitExplosionContainer;
private readonly DrawablePool<CaughtFruit> caughtFruitPool; private readonly DrawablePool<CaughtFruit> caughtFruitPool;
private readonly DrawablePool<CaughtBanana> caughtBananaPool; private readonly DrawablePool<CaughtBanana> caughtBananaPool;
@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
hitExplosionPool = new DrawablePool<HitExplosion>(10),
caughtFruitPool = new DrawablePool<CaughtFruit>(50), caughtFruitPool = new DrawablePool<CaughtFruit>(50),
caughtBananaPool = new DrawablePool<CaughtBanana>(100), caughtBananaPool = new DrawablePool<CaughtBanana>(100),
// less capacity is needed compared to fruit because droplet is not stacked // less capacity is needed compared to fruit because droplet is not stacked
@ -173,7 +171,7 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Alpha = 0, Alpha = 0,
}, },
hitExplosionContainer = new Container<HitExplosion> hitExplosionContainer = new HitExplosionContainer
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
@ -297,7 +295,6 @@ namespace osu.Game.Rulesets.Catch.UI
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject); droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
} }
/// <summary> /// <summary>
@ -399,9 +396,9 @@ namespace osu.Game.Rulesets.Catch.UI
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing; private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
hyperDashColour = hyperDashColour =
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
@ -508,15 +505,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position; return position;
} }
private void addLighting(CatchHitObject hitObject, float x, Color4 colour) private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
{ hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.HitObject = hitObject;
hitExplosion.X = x;
hitExplosion.Scale = new Vector2(hitObject.Scale);
hitExplosion.ObjectColour = colour;
hitExplosionContainer.Add(hitExplosion);
}
private CaughtObject getCaughtObject(PalpableCatchHitObject source) private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{ {

View File

@ -5,31 +5,16 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public class HitExplosion : PoolableDrawable public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
{ {
private Color4 objectColour;
public CatchHitObject HitObject;
public Color4 ObjectColour
{
get => objectColour;
set
{
if (objectColour == value) return;
objectColour = value;
onColourChanged();
}
}
private readonly CircularContainer largeFaint; private readonly CircularContainer largeFaint;
private readonly CircularContainer smallFaint; private readonly CircularContainer smallFaint;
private readonly CircularContainer directionalGlow1; private readonly CircularContainer directionalGlow1;
@ -83,9 +68,19 @@ namespace osu.Game.Rulesets.Catch.UI
}; };
} }
protected override void PrepareForUse() protected override void OnApply(HitExplosionEntry entry)
{ {
base.PrepareForUse(); X = entry.Position;
Scale = new Vector2(entry.Scale);
setColour(entry.ObjectColour);
using (BeginAbsoluteSequence(entry.LifetimeStart))
applyTransforms(entry.RNGSeed);
}
private void applyTransforms(int randomSeed)
{
ClearTransforms(true);
const double duration = 400; const double duration = 400;
@ -96,14 +91,13 @@ namespace osu.Game.Rulesets.Catch.UI
.FadeOut(duration * 2); .FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45 const float angle_variangle = 15; // should be less than 45
directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
Expire(true);
} }
private void onColourChanged() private void setColour(Color4 objectColour)
{ {
const float roundness = 100; const float roundness = 100;

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosionContainer : PooledDrawableWithLifetimeContainer<HitExplosionEntry, HitExplosion>
{
protected override bool RemoveRewoundEntry => true;
private readonly DrawablePool<HitExplosion> pool;
public HitExplosionContainer()
{
AddInternal(pool = new DrawablePool<HitExplosion>(10));
}
protected override HitExplosion GetDrawable(HitExplosionEntry entry) => pool.Get(d => d.Apply(entry));
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Performance;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosionEntry : LifetimeEntry
{
public readonly float Position;
public readonly float Scale;
public readonly Color4 ObjectColour;
public readonly int RNGSeed;
public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed)
{
LifetimeStart = startTime;
Position = position;
Scale = scale;
ObjectColour = objectColour;
RNGSeed = rngSeed;
}
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
SetContents(() => new FillFlowContainer SetContents(_ => new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new FillFlowContainer SetContents(_ => new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new FillFlowContainer SetContents(_ => new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
if (hitWindows.IsHitResultAllowed(result)) if (hitWindows.IsHitResultAllowed(result))
{ {
AddStep("Show " + result.GetDescription(), () => SetContents(() => AddStep("Show " + result.GetDescription(), () => SetContents(_ =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()) new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{ {
Type = result Type = result

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => SetContents(_ =>
{ {
var pool = new DrawablePool<PoolableHitExplosion>(5); var pool = new DrawablePool<PoolableHitExplosion>(5);
hitExplosionPools.Add(pool); hitExplosionPools.Add(pool);

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new FillFlowContainer SetContents(_ => new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition { Columns = 2 } new StageDefinition { Columns = 2 }
}; };
SetContents(() => new ManiaPlayfield(stageDefinitions)); SetContents(_ => new ManiaPlayfield(stageDefinitions));
}); });
} }
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition { Columns = 2 } new StageDefinition { Columns = 2 }
}; };
SetContents(() => new ManiaPlayfield(stageDefinitions)); SetContents(_ => new ManiaPlayfield(stageDefinitions));
}); });
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => SetContents(_ =>
{ {
ManiaAction normalAction = ManiaAction.Key1; ManiaAction normalAction = ManiaAction.Key1;
ManiaAction specialAction = ManiaAction.Special1; ManiaAction specialAction = ManiaAction.Special1;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }), SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
_ => new DefaultStageBackground()) _ => new DefaultStageBackground())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null) SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public class TestSceneManiaHitObjectSamples : HitObjectSampleTest public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
{ {
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
protected override IResourceStore<byte[]> Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples))); protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
/// <summary> /// <summary>
/// Tests that when a normal sample bank is used, the normal hitsound will be looked up. /// Tests that when a normal sample bank is used, the normal hitsound will be looked up.

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input; protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{ {
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
}; };

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -12,7 +11,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject> public class ManiaModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -17,8 +18,11 @@ namespace osu.Game.Rulesets.Mania.Mods
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
Seed.Value ??= RNG.Next();
var rng = new Random((int)Seed.Value);
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => RNG.Next()).ToList(); var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]); beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
} }

View File

@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private void sourceChanged() private void sourceChanged()
{ {
isLegacySkin = new Lazy<bool>(() => Source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null); isLegacySkin = new Lazy<bool>(() => FindProvider(s => s.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
hasKeyTexture = new Lazy<bool>(() => Source.GetAnimation( hasKeyTexture = new Lazy<bool>(() => FindProvider(s => s.GetAnimation(
this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value s.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
?? "mania-key1", true, true) != null); ?? "mania-key1", true, true) != null) != null);
} }
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break; break;
} }
return null; return Source.GetDrawableComponent(component);
} }
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)

View File

@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
Direction.BindValueChanged(onDirectionChanged, true); Direction.BindValueChanged(onDirectionChanged, true);
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
UpdateHitPosition(); UpdateHitPosition();
} }

View File

@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 0
[TimingPoints]
0,300,4,1,2,100,1,0
[HitObjects]
444,320,1000,5,0,0:0:0:0:

View File

@ -39,18 +39,28 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestLegacySmoothCursorTrail() public void TestLegacySmoothCursorTrail()
{ {
createTest(() => new LegacySkinContainer(false) createTest(() =>
{ {
Child = new LegacyCursorTrail() var skinContainer = new LegacySkinContainer(false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
return skinContainer;
}); });
} }
[Test] [Test]
public void TestLegacyDisjointCursorTrail() public void TestLegacyDisjointCursorTrail()
{ {
createTest(() => new LegacySkinContainer(true) createTest(() =>
{ {
Child = new LegacyCursorTrail() var skinContainer = new LegacySkinContainer(true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
return skinContainer;
}); });
} }
@ -102,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null; public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public event Action SourceChanged public event Action SourceChanged
{ {
add { } add { }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
int poolIndex = 0; int poolIndex = 0;
SetContents(() => SetContents(_ =>
{ {
DrawablePool<TestDrawableOsuJudgement> pool; DrawablePool<TestDrawableOsuJudgement> pool;

View File

@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null) private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
{ {
SetContents(() => SetContents(_ =>
{ {
var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo); var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo);
var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null); var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null);
@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => null; public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public ISample GetSample(ISampleInfo sampleInfo) => null; public ISample GetSample(ISampleInfo sampleInfo) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {

View File

@ -23,18 +23,18 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestVariousHitCircles() public void TestVariousHitCircles()
{ {
AddStep("Miss Big Single", () => SetContents(() => testSingle(2))); AddStep("Miss Big Single", () => SetContents(_ => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(() => testSingle(5))); AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5)));
AddStep("Miss Small Single", () => SetContents(() => testSingle(7))); AddStep("Miss Small Single", () => SetContents(_ => testSingle(7)));
AddStep("Hit Big Single", () => SetContents(() => testSingle(2, true))); AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
AddStep("Hit Medium Single", () => SetContents(() => testSingle(5, true))); AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
AddStep("Hit Small Single", () => SetContents(() => testSingle(7, true))); AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
AddStep("Miss Big Stream", () => SetContents(() => testStream(2))); AddStep("Miss Big Stream", () => SetContents(_ => testStream(2)));
AddStep("Miss Medium Stream", () => SetContents(() => testStream(5))); AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5)));
AddStep("Miss Small Stream", () => SetContents(() => testStream(7))); AddStep("Miss Small Stream", () => SetContents(_ => testStream(7)));
AddStep("Hit Big Stream", () => SetContents(() => testStream(2, true))); AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
AddStep("Hit Medium Stream", () => SetContents(() => testStream(5, true))); AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
AddStep("Hit Small Stream", () => SetContents(() => testStream(7, true))); AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
} }
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneHitCircleKiai : TestSceneHitCircle
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -30,28 +30,28 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(true, false)] [TestCase(true, false)]
[TestCase(false, true)] [TestCase(false, true)]
[TestCase(false, false)] [TestCase(false, false)]
public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
{ {
TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true));
base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin); ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin) public void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
{ {
TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true));
base.TestBeatmapComboColoursOverride(useBeatmapSkin); ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
{ {
TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true); PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true));
base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin); ConfigureTest(useBeatmapSkin, false, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
} }
@ -59,10 +59,10 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(false, true)] [TestCase(false, true)]
[TestCase(true, false)] [TestCase(true, false)]
[TestCase(false, false)] [TestCase(false, false)]
public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
{ {
TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, false); PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, false));
base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour); ConfigureTest(useBeatmapSkin, useBeatmapColour, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
} }
@ -70,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(false, true)] [TestCase(false, true)]
[TestCase(true, false)] [TestCase(true, false)]
[TestCase(false, false)] [TestCase(false, false)]
public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
{ {
TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, false); PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, false));
base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour); ConfigureTest(useBeatmapSkin, useBeatmapColour, true);
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
} }

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Reflection;
using NUnit.Framework;
using osu.Framework.IO.Stores;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneOsuHitObjectSamples : HitObjectSampleTest
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneOsuHitObjectSamples)));
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
SetupSkins(expectedSample, expectedSample);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSample);
}
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{
SetupSkins(string.Empty, expectedSample);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertUserLookup(expectedSample);
}
[TestCase("normal-hitnormal2")]
public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample)
{
SetupSkins(string.Empty, unwantedSample);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertNoLookup(unwantedSample);
}
}
}

View File

@ -166,6 +166,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default; public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null; public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public event Action SourceChanged; public event Action SourceChanged;

View File

@ -30,54 +30,54 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestVariousSliders() public void TestVariousSliders()
{ {
AddStep("Big Single", () => SetContents(() => testSimpleBig())); AddStep("Big Single", () => SetContents(_ => testSimpleBig()));
AddStep("Medium Single", () => SetContents(() => testSimpleMedium())); AddStep("Medium Single", () => SetContents(_ => testSimpleMedium()));
AddStep("Small Single", () => SetContents(() => testSimpleSmall())); AddStep("Small Single", () => SetContents(_ => testSimpleSmall()));
AddStep("Big 1 Repeat", () => SetContents(() => testSimpleBig(1))); AddStep("Big 1 Repeat", () => SetContents(_ => testSimpleBig(1)));
AddStep("Medium 1 Repeat", () => SetContents(() => testSimpleMedium(1))); AddStep("Medium 1 Repeat", () => SetContents(_ => testSimpleMedium(1)));
AddStep("Small 1 Repeat", () => SetContents(() => testSimpleSmall(1))); AddStep("Small 1 Repeat", () => SetContents(_ => testSimpleSmall(1)));
AddStep("Big 2 Repeats", () => SetContents(() => testSimpleBig(2))); AddStep("Big 2 Repeats", () => SetContents(_ => testSimpleBig(2)));
AddStep("Medium 2 Repeats", () => SetContents(() => testSimpleMedium(2))); AddStep("Medium 2 Repeats", () => SetContents(_ => testSimpleMedium(2)));
AddStep("Small 2 Repeats", () => SetContents(() => testSimpleSmall(2))); AddStep("Small 2 Repeats", () => SetContents(_ => testSimpleSmall(2)));
AddStep("Slow Slider", () => SetContents(testSlowSpeed)); // slow long sliders take ages already so no repeat steps AddStep("Slow Slider", () => SetContents(_ => testSlowSpeed())); // slow long sliders take ages already so no repeat steps
AddStep("Slow Short Slider", () => SetContents(() => testShortSlowSpeed())); AddStep("Slow Short Slider", () => SetContents(_ => testShortSlowSpeed()));
AddStep("Slow Short Slider 1 Repeats", () => SetContents(() => testShortSlowSpeed(1))); AddStep("Slow Short Slider 1 Repeats", () => SetContents(_ => testShortSlowSpeed(1)));
AddStep("Slow Short Slider 2 Repeats", () => SetContents(() => testShortSlowSpeed(2))); AddStep("Slow Short Slider 2 Repeats", () => SetContents(_ => testShortSlowSpeed(2)));
AddStep("Fast Slider", () => SetContents(() => testHighSpeed())); AddStep("Fast Slider", () => SetContents(_ => testHighSpeed()));
AddStep("Fast Slider 1 Repeat", () => SetContents(() => testHighSpeed(1))); AddStep("Fast Slider 1 Repeat", () => SetContents(_ => testHighSpeed(1)));
AddStep("Fast Slider 2 Repeats", () => SetContents(() => testHighSpeed(2))); AddStep("Fast Slider 2 Repeats", () => SetContents(_ => testHighSpeed(2)));
AddStep("Fast Short Slider", () => SetContents(() => testShortHighSpeed())); AddStep("Fast Short Slider", () => SetContents(_ => testShortHighSpeed()));
AddStep("Fast Short Slider 1 Repeat", () => SetContents(() => testShortHighSpeed(1))); AddStep("Fast Short Slider 1 Repeat", () => SetContents(_ => testShortHighSpeed(1)));
AddStep("Fast Short Slider 2 Repeats", () => SetContents(() => testShortHighSpeed(2))); AddStep("Fast Short Slider 2 Repeats", () => SetContents(_ => testShortHighSpeed(2)));
AddStep("Fast Short Slider 6 Repeats", () => SetContents(() => testShortHighSpeed(6))); AddStep("Fast Short Slider 6 Repeats", () => SetContents(_ => testShortHighSpeed(6)));
AddStep("Perfect Curve", () => SetContents(() => testPerfect())); AddStep("Perfect Curve", () => SetContents(_ => testPerfect()));
AddStep("Perfect Curve 1 Repeat", () => SetContents(() => testPerfect(1))); AddStep("Perfect Curve 1 Repeat", () => SetContents(_ => testPerfect(1)));
AddStep("Perfect Curve 2 Repeats", () => SetContents(() => testPerfect(2))); AddStep("Perfect Curve 2 Repeats", () => SetContents(_ => testPerfect(2)));
AddStep("Linear Slider", () => SetContents(() => testLinear())); AddStep("Linear Slider", () => SetContents(_ => testLinear()));
AddStep("Linear Slider 1 Repeat", () => SetContents(() => testLinear(1))); AddStep("Linear Slider 1 Repeat", () => SetContents(_ => testLinear(1)));
AddStep("Linear Slider 2 Repeats", () => SetContents(() => testLinear(2))); AddStep("Linear Slider 2 Repeats", () => SetContents(_ => testLinear(2)));
AddStep("Bezier Slider", () => SetContents(() => testBezier())); AddStep("Bezier Slider", () => SetContents(_ => testBezier()));
AddStep("Bezier Slider 1 Repeat", () => SetContents(() => testBezier(1))); AddStep("Bezier Slider 1 Repeat", () => SetContents(_ => testBezier(1)));
AddStep("Bezier Slider 2 Repeats", () => SetContents(() => testBezier(2))); AddStep("Bezier Slider 2 Repeats", () => SetContents(_ => testBezier(2)));
AddStep("Linear Overlapping", () => SetContents(() => testLinearOverlapping())); AddStep("Linear Overlapping", () => SetContents(_ => testLinearOverlapping()));
AddStep("Linear Overlapping 1 Repeat", () => SetContents(() => testLinearOverlapping(1))); AddStep("Linear Overlapping 1 Repeat", () => SetContents(_ => testLinearOverlapping(1)));
AddStep("Linear Overlapping 2 Repeats", () => SetContents(() => testLinearOverlapping(2))); AddStep("Linear Overlapping 2 Repeats", () => SetContents(_ => testLinearOverlapping(2)));
AddStep("Catmull Slider", () => SetContents(() => testCatmull())); AddStep("Catmull Slider", () => SetContents(_ => testCatmull()));
AddStep("Catmull Slider 1 Repeat", () => SetContents(() => testCatmull(1))); AddStep("Catmull Slider 1 Repeat", () => SetContents(_ => testCatmull(1)));
AddStep("Catmull Slider 2 Repeats", () => SetContents(() => testCatmull(2))); AddStep("Catmull Slider 2 Repeats", () => SetContents(_ => testCatmull(2)));
AddStep("Big Single, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset())); AddStep("Big Single, Large StackOffset", () => SetContents(_ => testSimpleBigLargeStackOffset()));
AddStep("Big 1 Repeat, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset(1))); AddStep("Big 1 Repeat, Large StackOffset", () => SetContents(_ => testSimpleBigLargeStackOffset(1)));
AddStep("Distance Overflow", () => SetContents(() => testDistanceOverflow())); AddStep("Distance Overflow", () => SetContents(_ => testDistanceOverflow()));
AddStep("Distance Overflow 1 Repeat", () => SetContents(() => testDistanceOverflow(1))); AddStep("Distance Overflow 1 Repeat", () => SetContents(_ => testDistanceOverflow(1)));
} }
[Test] [Test]

View File

@ -29,15 +29,15 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestVariousSpinners(bool autoplay) public void TestVariousSpinners(bool autoplay)
{ {
string term = autoplay ? "Hit" : "Miss"; string term = autoplay ? "Hit" : "Miss";
AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay))); AddStep($"{term} Big", () => SetContents(_ => testSingle(2, autoplay)));
AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay))); AddStep($"{term} Medium", () => SetContents(_ => testSingle(5, autoplay)));
AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay))); AddStep($"{term} Small", () => SetContents(_ => testSingle(7, autoplay)));
} }
[Test] [Test]
public void TestSpinningSamplePitchShift() public void TestSpinningSamplePitchShift()
{ {
AddStep("Add spinner", () => SetContents(() => testSingle(5, true, 4000))); AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000)));
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8); AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8); AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(true)] [TestCase(true)]
public void TestLongSpinner(bool autoplay) public void TestLongSpinner(bool autoplay)
{ {
AddStep("Very long spinner", () => SetContents(() => testSingle(5, autoplay, 4000))); AddStep("Very long spinner", () => SetContents(_ => testSingle(5, autoplay, 4000)));
AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult); AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0)); AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0));
} }
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(true)] [TestCase(true)]
public void TestSuperShortSpinner(bool autoplay) public void TestSuperShortSpinner(bool autoplay)
{ {
AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200))); AddStep("Very short spinner", () => SetContents(_ => testSingle(5, autoplay, 200)));
AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult); AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1); AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
} }
} }
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{ {
new Aim(mods), new Aim(mods),
new Speed(mods) new Speed(mods)

View File

@ -6,14 +6,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay<OsuHitObject> public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();

View File

@ -0,0 +1,274 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Mod that randomises the positions of the <see cref="HitObject"/>s
/// </summary>
public class OsuModRandom : ModRandom, IApplicableToBeatmap
{
public override string Description => "It never gets boring!";
public override bool Ranked => false;
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
private const float playfield_edge_ratio = 0.375f;
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random rng;
public void ApplyToBeatmap(IBeatmap beatmap)
{
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
var hitObjects = osuBeatmap.HitObjects;
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
RandomObjectInfo previous = null;
float rateOfChangeMultiplier = 0;
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
var current = new RandomObjectInfo(hitObject);
// rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams
if (i % 3 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (hitObject is Spinner)
{
previous = null;
continue;
}
applyRandomisation(rateOfChangeMultiplier, previous, current);
hitObject.Position = current.PositionRandomised;
// update end position as it may have changed as a result of the position update.
current.EndPositionRandomised = current.PositionRandomised;
if (hitObject is Slider slider)
moveSliderIntoPlayfield(slider, current);
previous = current;
}
}
/// <summary>
/// Returns the final position of the hit object
/// </summary>
/// <returns>Final position of the hit object</returns>
private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current)
{
if (previous == null)
{
var playfieldSize = OsuPlayfield.BASE_SIZE;
current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y);
return;
}
float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal;
current.AngleRad = (float)randomAngleRad + previous.AngleRad;
if (current.AngleRad < 0)
current.AngleRad += 2 * (float)Math.PI;
var posRelativeToPrev = new Vector2(
distanceToPrev * (float)Math.Cos(current.AngleRad),
distanceToPrev * (float)Math.Sin(current.AngleRad)
);
posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
var position = previous.EndPositionRandomised + posRelativeToPrev;
// Move hit objects back into the playfield if they are outside of it,
// which would sometimes happen during big jumps otherwise.
position.X = MathHelper.Clamp(position.X, 0, OsuPlayfield.BASE_SIZE.X);
position.Y = MathHelper.Clamp(position.Y, 0, OsuPlayfield.BASE_SIZE.Y);
current.PositionRandomised = position;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo)
{
var minMargin = getMinSliderMargin(slider);
slider.Position = new Vector2(
Math.Clamp(slider.Position.X, minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right),
Math.Clamp(slider.Position.Y, minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom)
);
currentObjectInfo.PositionRandomised = slider.Position;
currentObjectInfo.EndPositionRandomised = slider.EndPosition;
shiftNestedObjects(slider, currentObjectInfo.PositionRandomised - currentObjectInfo.PositionOriginal);
}
/// <summary>
/// Calculates the min. distances from the <see cref="Slider"/>'s position to the playfield border for the slider to be fully inside of the playfield.
/// </summary>
private MarginPadding getMinSliderMargin(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
var minMargin = new MarginPadding();
foreach (var pos in pathPositions)
{
minMargin.Left = Math.Max(minMargin.Left, -pos.X);
minMargin.Right = Math.Max(minMargin.Right, pos.X);
minMargin.Top = Math.Max(minMargin.Top, -pos.Y);
minMargin.Bottom = Math.Max(minMargin.Bottom, pos.Y);
}
minMargin.Left = Math.Min(minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right);
minMargin.Top = Math.Min(minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom);
return minMargin;
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Determines the position of the current hit object relative to the previous one.
/// </summary>
/// <returns>The position of the current hit object relative to the previous one</returns>
private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
{
var relativeRotationDistance = 0f;
if (prevPosChanged.X < playfield_middle.X)
{
relativeRotationDistance = Math.Max(
(border_distance_x - prevPosChanged.X) / border_distance_x,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
relativeRotationDistance
);
}
if (prevPosChanged.Y < playfield_middle.Y)
{
relativeRotationDistance = Math.Max(
(border_distance_y - prevPosChanged.Y) / border_distance_y,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
relativeRotationDistance
);
}
return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
}
/// <summary>
/// Rotates vector "initial" towards vector "destinantion"
/// </summary>
/// <param name="initial">Vector to rotate to "destination"</param>
/// <param name="destination">Vector "initial" should be rotated to</param>
/// <param name="relativeDistance">The angle the vector should be rotated relative to the difference between the angles of the the two vectors.</param>
/// <returns>Resulting vector</returns>
private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
{
var initialAngleRad = Math.Atan2(initial.Y, initial.X);
var destAngleRad = Math.Atan2(destination.Y, destination.X);
var diff = destAngleRad - initialAngleRad;
while (diff < -Math.PI) diff += 2 * Math.PI;
while (diff > Math.PI) diff -= 2 * Math.PI;
var finalAngleRad = initialAngleRad + relativeDistance * diff;
return new Vector2(
initial.Length * (float)Math.Cos(finalAngleRad),
initial.Length * (float)Math.Sin(finalAngleRad)
);
}
private class RandomObjectInfo
{
public float AngleRad { get; set; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionRandomised { get; set; }
public Vector2 EndPositionOriginal { get; }
public Vector2 EndPositionRandomised { get; set; }
public RandomObjectInfo(OsuHitObject hitObject)
{
PositionRandomised = PositionOriginal = hitObject.Position;
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
AngleRad = 0;
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Pooling;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
@ -12,34 +13,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s. /// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointConnection : PoolableDrawable public class FollowPointConnection : PoolableDrawableWithLifetime<FollowPointLifetimeEntry>
{ {
// Todo: These shouldn't be constants // Todo: These shouldn't be constants
public const int SPACING = 32; public const int SPACING = 32;
public const double PREEMPT = 800; public const double PREEMPT = 800;
public FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool; public DrawablePool<FollowPoint> Pool;
protected override void PrepareForUse() protected override void OnApply(FollowPointLifetimeEntry entry)
{ {
base.PrepareForUse(); base.OnApply(entry);
Entry.Invalidated += onEntryInvalidated;
entry.Invalidated += onEntryInvalidated;
refreshPoints(); refreshPoints();
} }
protected override void FreeAfterUse() protected override void OnFree(FollowPointLifetimeEntry entry)
{ {
base.FreeAfterUse(); base.OnFree(entry);
Entry.Invalidated -= onEntryInvalidated;
entry.Invalidated -= onEntryInvalidated;
// Return points to the pool. // Return points to the pool.
ClearInternal(false); ClearInternal(false);
Entry = null;
} }
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints); private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
@ -48,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
ClearInternal(false); ClearInternal(false);
OsuHitObject start = Entry.Start; var entry = Entry;
OsuHitObject end = Entry.End; if (entry?.End == null) return;
OsuHitObject start = entry.Start;
OsuHitObject end = entry.End;
double startTime = start.GetEndTime(); double startTime = start.GetEndTime();
@ -87,14 +86,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
fp.FadeIn(end.TimeFadeIn); fp.FadeIn(end.TimeFadeIn);
fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn); fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn).Expire();
finalTransformEndTime = fadeOutTime + end.TimeFadeIn; finalTransformEndTime = fp.LifetimeEnd;
} }
} }
// todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. entry.LifetimeEnd = finalTransformEndTime;
Entry.LifetimeEnd = finalTransformEndTime;
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
public class FollowPointLifetimeEntry : LifetimeEntry public class FollowPointLifetimeEntry : LifetimeEntry
{ {
public event Action Invalidated; public event Action? Invalidated;
public readonly OsuHitObject Start; public readonly OsuHitObject Start;
public FollowPointLifetimeEntry(OsuHitObject start) public FollowPointLifetimeEntry(OsuHitObject start)
@ -22,9 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
bindEvents(); bindEvents();
} }
private OsuHitObject end; private OsuHitObject? end;
public OsuHitObject End public OsuHitObject? End
{ {
get => end; get => end;
set set
@ -55,12 +57,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
} }
public void UnbindEvents() public void UnbindEvents()
{
if (Start != null)
{ {
Start.DefaultsApplied -= onDefaultsApplied; Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged; Start.PositionBindable.ValueChanged -= onPositionChanged;
}
if (End != null) if (End != null)
{ {

View File

@ -6,43 +6,32 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
/// <summary> /// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s. /// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointRenderer : CompositeDrawable public class FollowPointRenderer : PooledDrawableWithLifetimeContainer<FollowPointLifetimeEntry, FollowPointConnection>
{ {
public override bool RemoveCompletedTransforms => false; public new IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
private DrawablePool<FollowPointConnection> connectionPool; private DrawablePool<FollowPointConnection> connectionPool;
private DrawablePool<FollowPoint> pointPool; private DrawablePool<FollowPoint> pointPool;
private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>(); private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>(); private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
public FollowPointRenderer()
{
lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
lifetimeManager.EntryBecameDead += onEntryBecameDead;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionPool = new DrawablePoolNoLifetime<FollowPointConnection>(1, 200), connectionPool = new DrawablePool<FollowPointConnection>(1, 200),
pointPool = new DrawablePoolNoLifetime<FollowPoint>(50, 1000) pointPool = new DrawablePool<FollowPoint>(50, 1000)
}; };
} }
@ -107,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
previousEntry.End = newEntry.Start; previousEntry.End = newEntry.Start;
} }
lifetimeManager.AddEntry(newEntry); Add(newEntry);
} }
private void removeEntry(OsuHitObject hitObject) private void removeEntry(OsuHitObject hitObject)
@ -118,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
entry.UnbindEvents(); entry.UnbindEvents();
lifetimeEntries.RemoveAt(index); lifetimeEntries.RemoveAt(index);
lifetimeManager.RemoveEntry(entry); Remove(entry);
if (index > 0) if (index > 0)
{ {
@ -131,30 +120,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
} }
} }
protected override bool CheckChildrenLife() protected override FollowPointConnection GetDrawable(FollowPointLifetimeEntry entry)
{ {
bool anyAliveChanged = base.CheckChildrenLife(); var connection = connectionPool.Get();
anyAliveChanged |= lifetimeManager.Update(Time.Current); connection.Pool = pointPool;
return anyAliveChanged; connection.Apply(entry);
} return connection;
private void onEntryBecameAlive(LifetimeEntry entry)
{
var connection = connectionPool.Get(c =>
{
c.Entry = (FollowPointLifetimeEntry)entry;
c.Pool = pointPool;
});
connectionsInUse[entry] = connection;
AddInternal(connection);
}
private void onEntryBecameDead(LifetimeEntry entry)
{
RemoveInternal(connectionsInUse[entry]);
connectionsInUse.Remove(entry);
} }
private void onStartTimeChanged(OsuHitObject hitObject) private void onStartTimeChanged(OsuHitObject hitObject)
@ -171,16 +142,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
entry.UnbindEvents(); entry.UnbindEvents();
lifetimeEntries.Clear(); lifetimeEntries.Clear();
} }
private class DrawablePoolNoLifetime<T> : DrawablePool<T>
where T : PoolableDrawable, new()
{
public override bool RemoveWhenNotAlive => false;
public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
}
}
} }
} }

View File

@ -182,8 +182,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation. // todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut(); this.Delay(800).FadeOut();
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
switch (state) switch (state)
{ {
case ArmedState.Idle: case ArmedState.Idle:

View File

@ -97,8 +97,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateHitStateTransforms(state); base.UpdateHitStateTransforms(state);
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
switch (state) switch (state)
{ {
case ArmedState.Idle: case ArmedState.Idle:
@ -154,7 +152,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (Math.Abs(aimRotation - Arrow.Rotation) > 180) while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360; aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
if (!hasRotation) // The clock may be paused in a scenario like the editor.
if (!hasRotation || !Clock.IsRunning)
{ {
Arrow.Rotation = aimRotation; Arrow.Rotation = aimRotation;
hasRotation = true; hasRotation = true;

View File

@ -7,13 +7,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking, IHasMainCirclePiece public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, IHasMainCirclePiece
{ {
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@ -86,8 +87,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Debug.Assert(HitObject.HitWindows != null); Debug.Assert(HitObject.HitWindows != null);
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
switch (state) switch (state)
{ {
case ArmedState.Idle: case ArmedState.Idle:
@ -111,7 +110,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
} }
public void UpdateSnakingPosition(Vector2 start, Vector2 end) => protected override void OnApply()
Position = HitObject.RepeatIndex % 2 == 0 ? end : start; {
base.OnApply();
if (Slider != null)
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
}
} }
} }

View File

@ -18,9 +18,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
updateColour(); updateColour();
} }

View File

@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
{ {
new OsuModTarget(), new OsuModTarget(),
new OsuModDifficultyAdjust(), new OsuModDifficultyAdjust(),
new OsuModClassic() new OsuModClassic(),
new OsuModRandom(),
}; };
case ModType.Automation: case ModType.Automation:

View File

@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"), Texture = textures.Get(@"Gameplay/osu/disc"),
}, },
new KiaiFlash
{
RelativeSizeAxes = Axes.Both,
},
triangles = new TrianglesPiece triangles = new TrianglesPiece
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -128,5 +128,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner != null)
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
} }
} }

View File

@ -1,17 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public interface IMainCirclePiece
{
/// <summary>
/// Begins animating this <see cref="IMainCirclePiece"/>.
/// </summary>
/// <param name="state">The <see cref="ArmedState"/> of the related <see cref="DrawableHitCircle"/>.</param>
void Animate(ArmedState state);
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class KiaiFlash : BeatSyncedContainer
{
private const double fade_length = 80;
private const float flash_opacity = 0.25f;
public KiaiFlash()
{
EarlyActivationMilliseconds = 80;
Blending = BlendingParameters.Additive;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
Alpha = 0f,
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
Child
.FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint)
.Then()
.FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine);
}
}
}

Some files were not shown because too many files have changed in this diff Show More