1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 23:52:57 +08:00

Merge branch 'master' into fix-skip-button-clickability

This commit is contained in:
Dean Herbert 2019-07-11 23:10:28 +09:00 committed by GitHub
commit ecbd0f7eff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1494 additions and 467 deletions

View File

@ -11,7 +11,7 @@
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidApplication>True</AndroidApplication> <AndroidApplication>True</AndroidApplication>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<TargetFrameworkVersion>v8.1</TargetFrameworkVersion> <TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk> <AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -63,6 +63,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.704.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.711.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="ppy.osu.lazer" android:installLocation="auto" android:versionName="0.1.0"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="sh.ppy.osulazer" android:installLocation="auto" android:versionName="0.1.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

View File

@ -14,6 +14,11 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis> <AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<MandroidI18n>cjk;mideast;other;rare;west</MandroidI18n>
<AndroidDexTool>d8</AndroidDexTool>
<AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="OsuGameActivity.cs" /> <Compile Include="OsuGameActivity.cs" />
<Compile Include="OsuGameAndroid.cs" /> <Compile Include="OsuGameAndroid.cs" />

View File

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

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
UIApplication.Main(args, null, "AppDelegate"); UIApplication.Main(args, "GameUIApplication", "AppDelegate");
} }
} }
} }

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.1.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

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

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
UIApplication.Main(args, null, "AppDelegate"); UIApplication.Main(args, "GameUIApplication", "AppDelegate");
} }
} }
} }

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.1.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

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

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
UIApplication.Main(args, null, "AppDelegate"); UIApplication.Main(args, "GameUIApplication", "AppDelegate");
} }
} }
} }

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.1.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -13,13 +13,11 @@ using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
public bool AllowFail => false;
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
bool requiresHold = false; bool requiresHold = false;

View File

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

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
UIApplication.Main(args, null, "AppDelegate"); UIApplication.Main(args, "GameUIApplication", "AppDelegate");
} }
} }
} }

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.1.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

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

View File

@ -9,7 +9,7 @@ namespace osu.Game.Tests.iOS
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
UIApplication.Main(args, null, "AppDelegate"); UIApplication.Main(args, "GameUIApplication", "AppDelegate");
} }
} }
} }

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.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Replays; using osu.Game.Replays;
@ -9,7 +10,7 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Tests.NonVisual namespace osu.Game.Tests.NonVisual
{ {
[TestFixture] [TestFixture]
public class FramedReplayinputHandlerTest public class FramedReplayInputHandlerTest
{ {
private Replay replay; private Replay replay;
private TestInputHandler handler; private TestInputHandler handler;
@ -160,10 +161,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestRewindInsideImportantSection() public void TestRewindInsideImportantSection()
{ {
// fast forward to important section fastForwardToPoint(3000);
while (handler.SetFrameFromTime(3000) != null)
{
}
setTime(4000, 4000); setTime(4000, 4000);
confirmCurrentFrame(4); confirmCurrentFrame(4);
@ -205,10 +203,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestRewindOutOfImportantSection() public void TestRewindOutOfImportantSection()
{ {
// fast forward to important section fastForwardToPoint(3500);
while (handler.SetFrameFromTime(3500) != null)
{
}
confirmCurrentFrame(3); confirmCurrentFrame(3);
confirmNextFrame(4); confirmNextFrame(4);
@ -227,6 +222,15 @@ namespace osu.Game.Tests.NonVisual
confirmNextFrame(2); confirmNextFrame(2);
} }
private void fastForwardToPoint(double destination)
{
for (int i = 0; i < 1000; i++)
if (handler.SetFrameFromTime(destination) == null)
return;
throw new TimeoutException("Seek was never fulfilled");
}
private void setTime(double set, double? expect) private void setTime(double set, double? expect)
{ {
Assert.AreEqual(expect, handler.SetFrameFromTime(set)); Assert.AreEqual(expect, handler.SetFrameFromTime(set));
@ -274,6 +278,7 @@ namespace osu.Game.Tests.NonVisual
public TestInputHandler(Replay replay) public TestInputHandler(Replay replay)
: base(replay) : base(replay)
{ {
FrameAccuratePlayback = true;
} }
protected override double AllowedImportantTimeSpan => 1000; protected override double AllowedImportantTimeSpan => 1000;

View File

@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -16,12 +17,13 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestScenePlayerLoader : ManualInputManagerTestScene public class TestScenePlayerLoader : ManualInputManagerTestScene
{ {
private PlayerLoader loader; private TestPlayerLoader loader;
private OsuScreenStack stack; private OsuScreenStack stack;
[SetUp] [SetUp]
@ -31,19 +33,29 @@ namespace osu.Game.Tests.Visual.Gameplay
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
}); });
[Test]
public void TestBlockLoadViaMouseMovement()
{
AddStep("load dummy beatmap", () => stack.Push(loader = new TestPlayerLoader(() => new TestPlayer(false, false))));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddRepeatStep("move mouse", () => InputManager.MoveMouseTo(loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) * RNG.NextSingle()), 20);
AddAssert("loader still active", () => loader.IsCurrentScreen());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
}
[Test] [Test]
public void TestLoadContinuation() public void TestLoadContinuation()
{ {
Player player = null; Player player = null;
SlowLoadPlayer slowPlayer = null; SlowLoadPlayer slowPlayer = null;
AddStep("load dummy beatmap", () => stack.Push(loader = new PlayerLoader(() => player = new TestPlayer(false, false)))); AddStep("load dummy beatmap", () => stack.Push(loader = new TestPlayerLoader(() => player = new TestPlayer(false, false))));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
AddStep("load slow dummy beatmap", () => AddStep("load slow dummy beatmap", () =>
{ {
stack.Push(loader = new PlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); stack.Push(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
}); });
@ -61,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load player", () => AddStep("load player", () =>
{ {
Mods.Value = new[] { gameMod = new TestMod() }; Mods.Value = new[] { gameMod = new TestMod() };
stack.Push(loader = new PlayerLoader(() => player = new TestPlayer())); stack.Push(loader = new TestPlayerLoader(() => player = new TestPlayer()));
}); });
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
@ -85,6 +97,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player mods applied", () => playerMod2.Applied); AddAssert("player mods applied", () => playerMod2.Applied);
} }
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;
public TestPlayerLoader(Func<Player> createPlayer)
: base(createPlayer)
{
}
}
private class TestMod : Mod, IApplicableToScoreProcessor private class TestMod : Mod, IApplicableToScoreProcessor
{ {
public override string Name => string.Empty; public override string Name => string.Empty;

View File

@ -108,6 +108,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
StarDifficulty = 9.99, StarDifficulty = 9.99,
Version = @"TEST", Version = @"TEST",
Length = 456000,
Ruleset = maniaRuleset, Ruleset = maniaRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
@ -118,7 +119,6 @@ namespace osu.Game.Tests.Visual.Online
}, },
OnlineInfo = new BeatmapOnlineInfo OnlineInfo = new BeatmapOnlineInfo
{ {
Length = 456000,
CircleCount = 111, CircleCount = 111,
SliderCount = 12, SliderCount = 12,
PlayCount = 222, PlayCount = 222,
@ -181,6 +181,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
StarDifficulty = 5.67, StarDifficulty = 5.67,
Version = @"ANOTHER TEST", Version = @"ANOTHER TEST",
Length = 123000,
Ruleset = taikoRuleset, Ruleset = taikoRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
@ -191,7 +192,6 @@ namespace osu.Game.Tests.Visual.Online
}, },
OnlineInfo = new BeatmapOnlineInfo OnlineInfo = new BeatmapOnlineInfo
{ {
Length = 123000,
CircleCount = 123, CircleCount = 123,
SliderCount = 45, SliderCount = 45,
PlayCount = 567, PlayCount = 567,

View File

@ -24,6 +24,7 @@ namespace osu.Game.Tests.Visual.Online
typeof(ChangelogListing), typeof(ChangelogListing),
typeof(ChangelogSingleBuild), typeof(ChangelogSingleBuild),
typeof(ChangelogBuild), typeof(ChangelogBuild),
typeof(Comments),
}; };
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel); channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue.ToString(); channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue;
AddStep("Add random private channel", addRandomPrivateChannel); AddStep("Add random private channel", addRandomPrivateChannel);
AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2); AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2);

View File

@ -3,19 +3,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -30,11 +29,9 @@ namespace osu.Game.Tests.Visual.Online
typeof(ScoreTableRowBackground), typeof(ScoreTableRowBackground),
}; };
private readonly Box background;
public TestSceneScoresContainer() public TestSceneScoresContainer()
{ {
ScoresContainer scoresContainer; TestScoresContainer scoresContainer;
Child = new Container Child = new Container
{ {
@ -44,108 +41,137 @@ namespace osu.Game.Tests.Visual.Online
Width = 0.8f, Width = 0.8f,
Children = new Drawable[] Children = new Drawable[]
{ {
background = new Box { RelativeSizeAxes = Axes.Both }, new Box
scoresContainer = new ScoresContainer(), {
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
scoresContainer = new TestScoresContainer(),
} }
}; };
var scores = new List<ScoreInfo> var allScores = new APILegacyScores
{ {
new ScoreInfo Scores = new List<APILegacyScoreInfo>
{ {
User = new User new APILegacyScoreInfo
{ {
Id = 6602580, User = new User
Username = @"waaiiru",
Country = new Country
{ {
FullName = @"Spain", Id = 6602580,
FlagName = @"ES", Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
}, },
}, Mods = new Mod[]
Mods = new Mod[]
{
new OsuModDoubleTime(),
new OsuModHidden(),
new OsuModFlashlight(),
new OsuModHardRock(),
},
Rank = ScoreRank.XH,
PP = 200,
MaxCombo = 1234,
TotalScore = 1234567890,
Accuracy = 1,
},
new ScoreInfo
{
User = new User
{
Id = 4608074,
Username = @"Skycries",
Country = new Country
{ {
FullName = @"Brazil", new OsuModDoubleTime(),
FlagName = @"BR", new OsuModHidden(),
new OsuModFlashlight(),
new OsuModHardRock(),
}, },
Rank = ScoreRank.XH,
PP = 200,
MaxCombo = 1234,
TotalScore = 1234567890,
Accuracy = 1,
}, },
Mods = new Mod[] new APILegacyScoreInfo
{ {
new OsuModDoubleTime(), User = new User
new OsuModHidden(),
new OsuModFlashlight(),
},
Rank = ScoreRank.S,
PP = 190,
MaxCombo = 1234,
TotalScore = 1234789,
Accuracy = 0.9997,
},
new ScoreInfo
{
User = new User
{
Id = 1014222,
Username = @"eLy",
Country = new Country
{ {
FullName = @"Japan", Id = 4608074,
FlagName = @"JP", Username = @"Skycries",
Country = new Country
{
FullName = @"Brazil",
FlagName = @"BR",
},
}, },
}, Mods = new Mod[]
Mods = new Mod[]
{
new OsuModDoubleTime(),
new OsuModHidden(),
},
Rank = ScoreRank.B,
PP = 180,
MaxCombo = 1234,
TotalScore = 12345678,
Accuracy = 0.9854,
},
new ScoreInfo
{
User = new User
{
Id = 1541390,
Username = @"Toukai",
Country = new Country
{ {
FullName = @"Canada", new OsuModDoubleTime(),
FlagName = @"CA", new OsuModHidden(),
new OsuModFlashlight(),
}, },
Rank = ScoreRank.S,
PP = 190,
MaxCombo = 1234,
TotalScore = 1234789,
Accuracy = 0.9997,
}, },
Mods = new Mod[] new APILegacyScoreInfo
{ {
new OsuModDoubleTime(), User = new User
{
Id = 1014222,
Username = @"eLy",
Country = new Country
{
FullName = @"Japan",
FlagName = @"JP",
},
},
Mods = new Mod[]
{
new OsuModDoubleTime(),
new OsuModHidden(),
},
Rank = ScoreRank.B,
PP = 180,
MaxCombo = 1234,
TotalScore = 12345678,
Accuracy = 0.9854,
}, },
Rank = ScoreRank.C, new APILegacyScoreInfo
PP = 170, {
MaxCombo = 1234, User = new User
TotalScore = 1234567, {
Accuracy = 0.8765, Id = 1541390,
}, Username = @"Toukai",
new ScoreInfo Country = new Country
{
FullName = @"Canada",
FlagName = @"CA",
},
},
Mods = new Mod[]
{
new OsuModDoubleTime(),
},
Rank = ScoreRank.C,
PP = 170,
MaxCombo = 1234,
TotalScore = 1234567,
Accuracy = 0.8765,
},
new APILegacyScoreInfo
{
User = new User
{
Id = 7151382,
Username = @"Mayuri Hana",
Country = new Country
{
FullName = @"Thailand",
FlagName = @"TH",
},
},
Rank = ScoreRank.D,
PP = 160,
MaxCombo = 1234,
TotalScore = 123456,
Accuracy = 0.6543,
},
}
};
var myBestScore = new APILegacyUserTopScoreInfo
{
Score = new APILegacyScoreInfo
{ {
User = new User User = new User
{ {
@ -163,9 +189,42 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 123456, TotalScore = 123456,
Accuracy = 0.6543, Accuracy = 0.6543,
}, },
Position = 1337,
}; };
foreach (var s in scores) var oneScore = new APILegacyScores
{
Scores = new List<APILegacyScoreInfo>
{
new APILegacyScoreInfo
{
User = new User
{
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
},
Mods = new Mod[]
{
new OsuModDoubleTime(),
new OsuModHidden(),
new OsuModFlashlight(),
new OsuModHardRock(),
},
Rank = ScoreRank.XH,
PP = 200,
MaxCombo = 1234,
TotalScore = 1234567890,
Accuracy = 1,
}
}
};
foreach (var s in allScores.Scores)
{ {
s.Statistics.Add(HitResult.Great, RNG.Next(2000)); s.Statistics.Add(HitResult.Great, RNG.Next(2000));
s.Statistics.Add(HitResult.Good, RNG.Next(2000)); s.Statistics.Add(HitResult.Good, RNG.Next(2000));
@ -173,15 +232,26 @@ namespace osu.Game.Tests.Visual.Online
s.Statistics.Add(HitResult.Miss, RNG.Next(2000)); s.Statistics.Add(HitResult.Miss, RNG.Next(2000));
} }
AddStep("Load all scores", () => scoresContainer.Scores = scores); AddStep("Load all scores", () =>
{
allScores.UserScore = null;
scoresContainer.Scores = allScores;
});
AddStep("Load null scores", () => scoresContainer.Scores = null); AddStep("Load null scores", () => scoresContainer.Scores = null);
AddStep("Load only one score", () => scoresContainer.Scores = new[] { scores.First() }); AddStep("Load only one score", () => scoresContainer.Scores = oneScore);
AddStep("Load scores with my best", () =>
{
allScores.UserScore = myBestScore;
scoresContainer.Scores = allScores;
});
} }
[BackgroundDependencyLoader] private class TestScoresContainer : ScoresContainer
private void load(OsuColour colours)
{ {
background.Colour = colours.Gray2; public new APILegacyScores Scores
{
set => base.Scores = value;
}
} }
} }
} }

View File

@ -4,12 +4,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users; using osu.Game.Users;
@ -27,8 +24,6 @@ namespace osu.Game.Tests.Visual.SongSelect
typeof(RetrievalFailurePlaceholder), typeof(RetrievalFailurePlaceholder),
}; };
private RulesetStore rulesets;
private readonly FailableLeaderboard leaderboard; private readonly FailableLeaderboard leaderboard;
public TestSceneLeaderboard() public TestSceneLeaderboard()
@ -47,13 +42,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn)); AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn));
AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable)); AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable));
AddStep(@"Real beatmap", realBeatmap); foreach (BeatmapSetOnlineStatus status in Enum.GetValues(typeof(BeatmapSetOnlineStatus)))
} AddStep($"{status} beatmap", () => showBeatmapWithStatus(status));
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
} }
private void newScores() private void newScores()
@ -245,34 +235,12 @@ namespace osu.Game.Tests.Visual.SongSelect
leaderboard.Scores = scores; leaderboard.Scores = scores;
} }
private void realBeatmap() private void showBeatmapWithStatus(BeatmapSetOnlineStatus status)
{ {
leaderboard.Beatmap = new BeatmapInfo leaderboard.Beatmap = new BeatmapInfo
{ {
StarDifficulty = 1.36,
Version = @"BASIC",
OnlineBeatmapID = 1113057, OnlineBeatmapID = 1113057,
Ruleset = rulesets.GetRuleset(0), Status = status,
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 4,
DrainRate = 6.5f,
OverallDifficulty = 6.5f,
ApproachRate = 5,
},
OnlineInfo = new BeatmapOnlineInfo
{
Length = 115000,
CircleCount = 265,
SliderCount = 71,
PlayCount = 47906,
PassCount = 19899,
},
Metrics = new BeatmapMetrics
{
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
}; };
} }

View File

@ -133,6 +133,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep(@"Sort by Artist", delegate { songSelect.FilterControl.Sort = SortMode.Artist; }); AddStep(@"Sort by Artist", delegate { songSelect.FilterControl.Sort = SortMode.Artist; });
AddStep(@"Sort by Title", delegate { songSelect.FilterControl.Sort = SortMode.Title; }); AddStep(@"Sort by Title", delegate { songSelect.FilterControl.Sort = SortMode.Title; });
AddStep(@"Sort by Author", delegate { songSelect.FilterControl.Sort = SortMode.Author; }); AddStep(@"Sort by Author", delegate { songSelect.FilterControl.Sort = SortMode.Author; });
AddStep(@"Sort by DateAdded", delegate { songSelect.FilterControl.Sort = SortMode.DateAdded; });
AddStep(@"Sort by BPM", delegate { songSelect.FilterControl.Sort = SortMode.BPM; });
AddStep(@"Sort by Length", delegate { songSelect.FilterControl.Sort = SortMode.Length; });
AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; }); AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; });
} }
@ -265,16 +268,21 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
int beatmapId = setId * 10 + i; int beatmapId = setId * 10 + i;
int length = RNG.Next(30000, 200000);
double bpm = RNG.NextSingle(80, 200);
beatmaps.Add(new BeatmapInfo beatmaps.Add(new BeatmapInfo
{ {
Ruleset = getRuleset(), Ruleset = getRuleset(),
OnlineBeatmapID = beatmapId, OnlineBeatmapID = beatmapId,
Path = "normal.osu", Path = "normal.osu",
Version = $"{beatmapId}", Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
BPM = bpm,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
OverallDifficulty = 3.5f, OverallDifficulty = 3.5f,
} },
}); });
} }
@ -286,10 +294,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
// Create random metadata, then we can check if sorting works based on these // Create random metadata, then we can check if sorting works based on these
Artist = "Some Artist " + RNG.Next(0, 9), Artist = "Some Artist " + RNG.Next(0, 9),
Title = $"Some Song (set id {setId})", Title = $"Some Song (set id {setId}, max bpm {beatmaps.Max(b => b.BPM):0.#})",
AuthorString = "Some Guy " + RNG.Next(0, 9), AuthorString = "Some Guy " + RNG.Next(0, 9),
}, },
Beatmaps = beatmaps Beatmaps = beatmaps,
DateAdded = DateTimeOffset.UtcNow,
}; };
} }
} }

View File

@ -14,8 +14,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneBackButton : OsuTestScene public class TestSceneBackButton : OsuTestScene
{ {
private readonly BackButton button;
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(TwoLayerButton) typeof(TwoLayerButton)
@ -23,6 +21,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestSceneBackButton() public TestSceneBackButton()
{ {
BackButton button;
Child = new Container Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -40,11 +40,12 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Action = () => button.Hide(),
} }
} }
}; };
button.Action = () => button.Hide();
AddStep("show button", () => button.Show()); AddStep("show button", () => button.Show());
AddStep("hide button", () => button.Hide()); AddStep("hide button", () => button.Hide());
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -23,11 +24,12 @@ namespace osu.Game.Tests.Visual.UserInterface
typeof(Button) typeof(Button)
}; };
public TestSceneButtonSystem() private OsuLogo logo;
{ private ButtonSystem buttons;
OsuLogo logo;
ButtonSystem buttons;
[SetUp]
public void SetUp() => Schedule(() =>
{
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -36,13 +38,47 @@ namespace osu.Game.Tests.Visual.UserInterface
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
buttons = new ButtonSystem(), buttons = new ButtonSystem(),
logo = new OsuLogo { RelativePositionAxes = Axes.Both } logo = new OsuLogo
{
RelativePositionAxes = Axes.Both,
Position = new Vector2(0.5f)
}
}; };
buttons.SetOsuLogo(logo); buttons.SetOsuLogo(logo);
});
[Test]
public void TestAllStates()
{
foreach (var s in Enum.GetValues(typeof(ButtonSystemState)).OfType<ButtonSystemState>().Skip(1)) foreach (var s in Enum.GetValues(typeof(ButtonSystemState)).OfType<ButtonSystemState>().Skip(1))
AddStep($"State to {s}", () => buttons.State = s); AddStep($"State to {s}", () => buttons.State = s);
AddStep("Enter mode", performEnterMode);
AddStep("Return to menu", () =>
{
buttons.State = ButtonSystemState.Play;
buttons.FadeIn(MainMenu.FADE_IN_DURATION, Easing.OutQuint);
buttons.MoveTo(new Vector2(0), MainMenu.FADE_IN_DURATION, Easing.OutQuint);
logo.FadeColour(Color4.White, 100, Easing.OutQuint);
logo.FadeIn(100, Easing.OutQuint);
});
}
[Test]
public void TestSmoothExit()
{
AddStep("Enter mode", performEnterMode);
}
private void performEnterMode()
{
buttons.State = ButtonSystemState.EnteringMode;
buttons.FadeOut(MainMenu.FADE_OUT_DURATION, Easing.InSine);
buttons.MoveTo(new Vector2(-800, 0), MainMenu.FADE_OUT_DURATION, Easing.InSine);
logo.FadeOut(300, Easing.InSine)
.ScaleTo(0.2f, 300, Easing.InSine);
} }
} }
} }

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
}; };
breadcrumbs.Current.ValueChanged += screen => titleText.Text = $"Changed to {screen.NewValue.ToString()}"; breadcrumbs.Current.ValueChanged += screen => titleText.Text = $"Changed to {screen.NewValue}";
breadcrumbs.Current.TriggerChange(); breadcrumbs.Current.TriggerChange();
waitForCurrent(); waitForCurrent();

View File

@ -3,7 +3,7 @@
<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="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -5,9 +5,9 @@
</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="15.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -159,7 +159,7 @@ namespace osu.Game.Tournament.Components
} }
var bpm = beatmap.BeatmapSet.OnlineInfo.BPM; var bpm = beatmap.BeatmapSet.OnlineInfo.BPM;
var length = beatmap.OnlineInfo.Length; var length = beatmap.Length;
string hardRockExtra = ""; string hardRockExtra = "";
string srExtra = ""; string srExtra = "";
@ -180,7 +180,7 @@ namespace osu.Game.Tournament.Components
panelContents.Children = new Drawable[] panelContents.Children = new Drawable[]
{ {
new DiffPiece(("Length", TimeSpan.FromSeconds(length).ToString(@"mm\:ss"))) new DiffPiece(("Length", TimeSpan.FromMilliseconds(length).ToString(@"mm\:ss")))
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,

View File

@ -51,6 +51,16 @@ namespace osu.Game.Beatmaps
[NotMapped] [NotMapped]
public BeatmapOnlineInfo OnlineInfo { get; set; } public BeatmapOnlineInfo OnlineInfo { get; set; }
/// <summary>
/// The playable length in milliseconds of this beatmap.
/// </summary>
public double Length { get; set; }
/// <summary>
/// The most common BPM of this beatmap.
/// </summary>
public double BPM { get; set; }
public string Path { get; set; } public string Path { get; set; }
[JsonProperty("file_sha2")] [JsonProperty("file_sha2")]

View File

@ -23,6 +23,7 @@ using osu.Game.IO.Archives;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -82,6 +83,8 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize); new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
if (archive != null) if (archive != null)
@ -176,20 +179,22 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap; return DefaultBeatmap;
var cached = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
if (cached != null) if (working == null)
return cached; {
if (beatmapInfo.Metadata == null)
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata;
if (beatmapInfo.Metadata == null) workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store,
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager));
}
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager); previous?.TransferTo(working);
return working;
previous?.TransferTo(working); }
workingCache.Add(working);
return working;
} }
/// <summary> /// <summary>
@ -297,6 +302,8 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo.Ruleset = ruleset; beatmap.BeatmapInfo.Ruleset = ruleset;
// TODO: this should be done in a better place once we actually need to dynamically update it. // TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
beatmap.BeatmapInfo.Length = calculateLength(beatmap);
beatmap.BeatmapInfo.BPM = beatmap.ControlPointInfo.BPMMode;
beatmapInfos.Add(beatmap.BeatmapInfo); beatmapInfos.Add(beatmap.BeatmapInfo);
} }
@ -305,6 +312,19 @@ namespace osu.Game.Beatmaps
return beatmapInfos; return beatmapInfos;
} }
private double calculateLength(IBeatmap b)
{
if (!b.HitObjects.Any())
return 0;
var lastObject = b.HitObjects.Last();
double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime;
double startTime = b.HitObjects.First().StartTime;
return endTime - startTime;
}
/// <summary> /// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary> /// </summary>

View File

@ -8,11 +8,6 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public class BeatmapOnlineInfo public class BeatmapOnlineInfo
{ {
/// <summary>
/// The length in milliseconds of this beatmap's song.
/// </summary>
public double Length { get; set; }
/// <summary> /// <summary>
/// The amount of circles in this beatmap. /// The amount of circles in this beatmap.
/// </summary> /// </summary>

View File

@ -35,8 +35,21 @@ namespace osu.Game.Beatmaps
[NotMapped] [NotMapped]
public BeatmapSetMetrics Metrics { get; set; } public BeatmapSetMetrics Metrics { get; set; }
/// <summary>
/// The maximum star difficulty of all beatmaps in this set.
/// </summary>
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0; public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0;
/// <summary>
/// The maximum playable length in milliseconds of all beatmaps in this set.
/// </summary>
public double MaxLength => Beatmaps?.Max(b => b.Length) ?? 0;
/// <summary>
/// The maximum BPM of all beatmaps in this set.
/// </summary>
public double MaxBPM => Beatmaps?.Max(b => b.BPM) ?? 0;
[NotMapped] [NotMapped]
public bool DeletePending { get; set; } public bool DeletePending { get; set; }

View File

@ -35,15 +35,15 @@ namespace osu.Game.Beatmaps.Drawables
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad) protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay); => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
protected override double TransformDuration => 400;
protected override Drawable CreateDrawable(BeatmapInfo model) protected override Drawable CreateDrawable(BeatmapInfo model)
{ {
Drawable drawable = getDrawableForModel(model); var drawable = getDrawableForModel(model);
drawable.RelativeSizeAxes = Axes.Both; drawable.RelativeSizeAxes = Axes.Both;
drawable.Anchor = Anchor.Centre; drawable.Anchor = Anchor.Centre;
drawable.Origin = Anchor.Centre; drawable.Origin = Anchor.Centre;
drawable.FillMode = FillMode.Fill; drawable.FillMode = FillMode.Fill;
drawable.OnLoadComplete += d => d.FadeInFromZero(400);
return drawable; return drawable;
} }

View File

@ -77,6 +77,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01); Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
Set(OsuSetting.ShowInterface, true); Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.KeyOverlay, false);
Set(OsuSetting.FloatingComments, false); Set(OsuSetting.FloatingComments, false);
@ -131,6 +132,7 @@ namespace osu.Game.Configuration
KeyOverlay, KeyOverlay,
FloatingComments, FloatingComments,
ShowInterface, ShowInterface,
ShowHealthDisplayWhenCantFail,
MouseDisableButtons, MouseDisableButtons,
MouseDisableWheel, MouseDisableWheel,
AudioOffset, AudioOffset,

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
@ -114,7 +114,8 @@ namespace osu.Game.Database
lock (imported) lock (imported)
{ {
imported.Add(model); if (model != null)
imported.Add(model);
current++; current++;
notification.Text = $"Imported {current} of {paths.Length} {HumanisedModelName}s"; notification.Text = $"Imported {current} of {paths.Length} {HumanisedModelName}s";
@ -140,7 +141,7 @@ namespace osu.Game.Database
{ {
notification.CompletionText = imported.Count == 1 notification.CompletionText = imported.Count == 1
? $"Imported {imported.First()}!" ? $"Imported {imported.First()}!"
: $"Imported {current} {HumanisedModelName}s!"; : $"Imported {imported.Count} {HumanisedModelName}s!";
if (imported.Count > 0 && PresentImport != null) if (imported.Count > 0 && PresentImport != null)
{ {
@ -176,7 +177,7 @@ namespace osu.Game.Database
// TODO: Add a check to prevent files from storage to be deleted. // TODO: Add a check to prevent files from storage to be deleted.
try try
{ {
if (import != null && File.Exists(path)) if (import != null && File.Exists(path) && ShouldDeleteArchive(path))
File.Delete(path); File.Delete(path);
} }
catch (Exception e) catch (Exception e)
@ -207,7 +208,7 @@ namespace osu.Game.Database
{ {
model = CreateModel(archive); model = CreateModel(archive);
if (model == null) return null; if (model == null) return Task.FromResult<TModel>(null);
model.Hash = computeHash(archive); model.Hash = computeHash(archive);
} }
@ -498,6 +499,18 @@ namespace osu.Game.Database
/// </summary> /// </summary>
protected virtual string ImportFromStablePath => null; protected virtual string ImportFromStablePath => null;
/// <summary>
/// Select paths to import from stable. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
/// </summary>
protected virtual IEnumerable<string> GetStableImportPaths(Storage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath);
/// <summary>
/// Whether this specified path should be removed after successful import.
/// </summary>
/// <param name="path">The path for consideration. May be a file or a directory.</param>
/// <returns>Whether to perform deletion.</returns>
protected virtual bool ShouldDeleteArchive(string path) => false;
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>
@ -518,7 +531,7 @@ namespace osu.Game.Database
return Task.CompletedTask; return Task.CompletedTask;
} }
return Task.Run(async () => await Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray())); return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray()));
} }
#endregion #endregion

View File

@ -41,6 +41,9 @@ namespace osu.Game.Database
{ {
// required to initialise native SQLite libraries on some platforms. // required to initialise native SQLite libraries on some platforms.
SQLitePCL.Batteries_V2.Init(); SQLitePCL.Batteries_V2.Init();
// https://github.com/aspnet/EntityFrameworkCore/issues/9994#issuecomment-508588678
SQLitePCL.raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/);
} }
/// <summary> /// <summary>

View File

@ -37,6 +37,8 @@ namespace osu.Game.Graphics.UserInterface
text.Colour = AccentColour; text.Colour = AccentColour;
icon.Colour = AccentColour; icon.Colour = AccentColour;
} }
updateFade();
} }
} }
@ -48,39 +50,6 @@ namespace osu.Game.Graphics.UserInterface
private const float transition_length = 500; private const float transition_length = 500;
private void fadeIn()
{
box.FadeIn(transition_length, Easing.OutQuint);
text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
}
private void fadeOut()
{
box.FadeOut(transition_length, Easing.OutQuint);
text.FadeColour(AccentColour, transition_length, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
{
fadeIn();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (!Current.Value)
fadeOut();
base.OnHoverLost(e);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (accentColour == null)
AccentColour = colours.Blue;
}
public OsuTabControlCheckbox() public OsuTabControlCheckbox()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -115,19 +84,34 @@ namespace osu.Game.Graphics.UserInterface
} }
}; };
Current.ValueChanged += selected => Current.ValueChanged += selected => { icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; };
{ }
if (selected.NewValue)
{ [BackgroundDependencyLoader]
fadeIn(); private void load(OsuColour colours)
icon.Icon = FontAwesome.Regular.CheckCircle; {
} if (accentColour == null)
else AccentColour = colours.Blue;
{ }
fadeOut();
icon.Icon = FontAwesome.Regular.Circle; protected override bool OnHover(HoverEvent e)
} {
}; updateFade();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (!Current.Value)
updateFade();
base.OnHoverLost(e);
}
private void updateFade()
{
box.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint);
} }
} }
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F4, GlobalAction.ToggleMute), new KeyBinding(InputKey.F4, GlobalAction.ToggleMute),
new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.MouseButton1, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Space, GlobalAction.Select),
new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select),

View File

@ -0,0 +1,504 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20190708070844_AddBPMAndLengthColumns")]
partial class AddBPMAndLengthColumns
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset>("DateAdded");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Key")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<int?>("SkinInfoID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("SkinInfoID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int?>("ScoreInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("ScoreInfoID");
b.ToTable("ScoreFileInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("Accuracy")
.HasColumnType("DECIMAL(1,4)");
b.Property<int>("BeatmapInfoID");
b.Property<int>("Combo");
b.Property<DateTimeOffset>("Date");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int>("MaxCombo");
b.Property<string>("ModsJson")
.HasColumnName("Mods");
b.Property<long?>("OnlineScoreID");
b.Property<double?>("PP");
b.Property<int>("Rank");
b.Property<int>("RulesetID");
b.Property<string>("StatisticsJson")
.HasColumnName("Statistics");
b.Property<long>("TotalScore");
b.Property<long?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
.HasColumnName("User");
b.HasKey("ID");
b.HasIndex("BeatmapInfoID");
b.HasIndex("OnlineScoreID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("ScoreInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<string>("Name");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Settings")
.HasForeignKey("SkinInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Scoring.ScoreInfo")
.WithMany("Files")
.HasForeignKey("ScoreInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
.WithMany("Scores")
.HasForeignKey("BeatmapInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class AddBPMAndLengthColumns : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "BPM",
table: "BeatmapInfo",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<double>(
name: "Length",
table: "BeatmapInfo",
nullable: false,
defaultValue: 0.0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BPM",
table: "BeatmapInfo");
migrationBuilder.DropColumn(
name: "Length",
table: "BeatmapInfo");
}
}
}

View File

@ -45,6 +45,8 @@ namespace osu.Game.Migrations
b.Property<int>("AudioLeadIn"); b.Property<int>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID"); b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor"); b.Property<int>("BeatDivisor");
@ -61,6 +63,8 @@ namespace osu.Game.Migrations
b.Property<bool>("Hidden"); b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks"); b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash"); b.Property<string>("MD5Hash");

View File

@ -5,8 +5,10 @@ using System;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using System.Text;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
@ -15,8 +17,9 @@ namespace osu.Game.Online.API.Requests
private readonly BeatmapInfo beatmap; private readonly BeatmapInfo beatmap;
private readonly BeatmapLeaderboardScope scope; private readonly BeatmapLeaderboardScope scope;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly IEnumerable<Mod> mods;
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global) public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<Mod> mods = null)
{ {
if (!beatmap.OnlineBeatmapID.HasValue) if (!beatmap.OnlineBeatmapID.HasValue)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}.");
@ -27,6 +30,7 @@ namespace osu.Game.Online.API.Requests
this.beatmap = beatmap; this.beatmap = beatmap;
this.scope = scope; this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
this.mods = mods ?? Array.Empty<Mod>();
Success += onSuccess; Success += onSuccess;
} }
@ -38,19 +42,29 @@ namespace osu.Game.Online.API.Requests
score.Beatmap = beatmap; score.Beatmap = beatmap;
score.Ruleset = ruleset; score.Ruleset = ruleset;
} }
var userScore = r.UserScore;
if (userScore != null)
{
userScore.Score.Beatmap = beatmap;
userScore.Score.Ruleset = ruleset;
}
} }
protected override WebRequest CreateWebRequest() protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores{createQueryParameters()}";
private string createQueryParameters()
{ {
var req = base.CreateWebRequest(); StringBuilder query = new StringBuilder(@"?");
req.Timeout = 30000; query.Append($@"type={scope.ToString().ToLowerInvariant()}");
req.AddParameter(@"type", scope.ToString().ToLowerInvariant()); query.Append($@"&mode={ruleset.ShortName}");
req.AddParameter(@"mode", ruleset.ShortName);
return req; foreach (var mod in mods)
query.Append($@"&mods[]={mod.Acronym}");
return query.ToString();
} }
protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores";
} }
} }

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 Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -71,6 +72,7 @@ namespace osu.Game.Online.API.Requests.Responses
StarDifficulty = starDifficulty, StarDifficulty = starDifficulty,
OnlineBeatmapID = OnlineBeatmapID, OnlineBeatmapID = OnlineBeatmapID,
Version = version, Version = version,
Length = TimeSpan.FromSeconds(length).TotalMilliseconds,
Status = Status, Status = Status,
BeatmapSet = set, BeatmapSet = set,
Metrics = metrics, Metrics = metrics,
@ -85,7 +87,6 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
PlayCount = playCount, PlayCount = playCount,
PassCount = passCount, PassCount = passCount,
Length = length,
CircleCount = circleCount, CircleCount = circleCount,
SliderCount = sliderCount, SliderCount = sliderCount,
}, },

View File

@ -33,6 +33,8 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("versions")] [JsonProperty("versions")]
public VersionNavigation Versions { get; set; } public VersionNavigation Versions { get; set; }
public string Url => $"https://osu.ppy.sh/home/changelog/{UpdateStream.Name}/{Version}";
public class VersionNavigation public class VersionNavigation
{ {
[JsonProperty("next")] [JsonProperty("next")]

View File

@ -10,5 +10,17 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
[JsonProperty(@"scores")] [JsonProperty(@"scores")]
public List<APILegacyScoreInfo> Scores; public List<APILegacyScoreInfo> Scores;
[JsonProperty(@"userScore")]
public APILegacyUserTopScoreInfo UserScore;
}
public class APILegacyUserTopScoreInfo
{
[JsonProperty(@"position")]
public int Position;
[JsonProperty(@"score")]
public APILegacyScoreInfo Score;
} }
} }

View File

@ -11,7 +11,7 @@ using osu.Game.Online.API;
namespace osu.Game.Online namespace osu.Game.Online
{ {
/// <summary> /// <summary>
/// A component which tracks a beatmap through potential download/import/deletion. /// A component which tracks a <see cref="TModel"/> through potential download/import/deletion.
/// </summary> /// </summary>
public abstract class DownloadTrackingComposite<TModel, TModelManager> : CompositeDrawable public abstract class DownloadTrackingComposite<TModel, TModelManager> : CompositeDrawable
where TModel : class, IEquatable<TModel> where TModel : class, IEquatable<TModel>
@ -22,7 +22,7 @@ namespace osu.Game.Online
private TModelManager manager; private TModelManager manager;
/// <summary> /// <summary>
/// Holds the current download state of the beatmap, whether is has already been downloaded, is in progress, or is not downloaded. /// Holds the current download state of the <see cref="TModel"/>, whether is has already been downloaded, is in progress, or is not downloaded.
/// </summary> /// </summary>
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>(); protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();

View File

@ -203,8 +203,13 @@ namespace osu.Game.Online.Leaderboards
public void APIStateChanged(IAPIProvider api, APIState state) public void APIStateChanged(IAPIProvider api, APIState state)
{ {
if (state == APIState.Online) switch (state)
UpdateScores(); {
case APIState.Online:
case APIState.Offline:
UpdateScores();
break;
}
} }
protected void UpdateScores() protected void UpdateScores()
@ -231,12 +236,6 @@ namespace osu.Game.Online.Leaderboards
if (getScoresRequest == null) if (getScoresRequest == null)
return; return;
if (api?.IsLoggedIn != true)
{
PlaceholderState = PlaceholderState.NotLoggedIn;
return;
}
getScoresRequest.Failure += e => Schedule(() => getScoresRequest.Failure += e => Schedule(() =>
{ {
if (e is OperationCanceledException) if (e is OperationCanceledException)

View File

@ -282,11 +282,9 @@ namespace osu.Game
performFromMainMenu(() => performFromMainMenu(() =>
{ {
Ruleset.Value = databasedScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
Mods.Value = databasedScoreInfo.Mods;
menuScreen.Push(new PlayerLoader(() => new ReplayPlayer(databasedScore))); menuScreen.Push(new ReplayPlayerLoader(databasedScore));
}, $"watch {databasedScoreInfo}", bypassScreenAllowChecks: true); }, $"watch {databasedScoreInfo}", bypassScreenAllowChecks: true);
} }
@ -387,6 +385,7 @@ namespace osu.Game
BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
ScoreManager.PostNotification = n => notifications?.Post(n); ScoreManager.PostNotification = n => notifications?.Post(n);
ScoreManager.GetStableStorage = GetStorageForStableInstall;
ScoreManager.PresentImport = items => PresentScore(items.First()); ScoreManager.PresentImport = items => PresentScore(items.First());
Container logoContainer; Container logoContainer;

View File

@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet
} }
else else
{ {
length.Value = TimeSpan.FromSeconds(beatmap.OnlineInfo.Length).ToString(@"m\:ss"); length.Value = TimeSpan.FromMilliseconds(beatmap.Length).ToString(@"m\:ss");
circleCount.Value = beatmap.OnlineInfo.CircleCount.ToString(); circleCount.Value = beatmap.OnlineInfo.CircleCount.ToString();
sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToString(); sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToString();
} }

View File

@ -23,10 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private Color4 backgroundHoveredColour; private Color4 backgroundHoveredColour;
private readonly Box background; private readonly Box background;
private readonly TopScoreUserSection userSection;
private readonly TopScoreStatisticsSection statisticsSection;
public DrawableTopScore() public DrawableTopScore(ScoreInfo score, int position = 1)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -61,16 +59,19 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
new Drawable[] new Drawable[]
{ {
userSection = new TopScoreUserSection new TopScoreUserSection
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Score = score,
ScorePosition = position,
}, },
null, null,
statisticsSection = new TopScoreStatisticsSection new TopScoreStatisticsSection
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Score = score,
} }
}, },
}, },
@ -91,18 +92,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
background.Colour = backgroundIdleColour; background.Colour = backgroundIdleColour;
} }
/// <summary>
/// Sets the score to be displayed.
/// </summary>
public ScoreInfo Score
{
set
{
userSection.Score = value;
statisticsSection.Score = value;
}
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
background.FadeColour(backgroundHoveredColour, fade_duration, Easing.OutQuint); background.FadeColour(backgroundHoveredColour, fade_duration, Easing.OutQuint);

View File

@ -59,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Content = null; Content = null;
backgroundFlow.Clear(); backgroundFlow.Clear();
if (value == null || !value.Any()) if (value?.Any() != true)
return; return;
for (int i = 0; i < value.Count; i++) for (int i = 0; i < value.Count; i++)

View File

@ -5,15 +5,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.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK;
using System.Linq;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osuTK;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapSet.Scores namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
@ -24,18 +23,65 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly Box background; private readonly Box background;
private readonly ScoreTable scoreTable; private readonly ScoreTable scoreTable;
private readonly FillFlowContainer topScoresContainer;
private readonly DrawableTopScore topScore;
private readonly LoadingAnimation loadingAnimation; private readonly LoadingAnimation loadingAnimation;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
private GetScoresRequest getScoresRequest;
private BeatmapInfo beatmap;
public BeatmapInfo Beatmap
{
get => beatmap;
set
{
if (beatmap == value)
return;
beatmap = value;
getScores(beatmap);
}
}
protected APILegacyScores Scores
{
set
{
Schedule(() =>
{
loading = false;
topScoresContainer.Clear();
if (value?.Scores.Any() != true)
{
scoreTable.Scores = null;
scoreTable.Hide();
return;
}
scoreTable.Scores = value.Scores;
scoreTable.Show();
var topScore = value.Scores.First();
var userScore = value.UserScore;
topScoresContainer.Add(new DrawableTopScore(topScore));
if (userScore != null && userScore.Score.OnlineScoreID != topScore.OnlineScoreID)
topScoresContainer.Add(new DrawableTopScore(userScore.Score, userScore.Position));
});
}
}
public ScoresContainer() public ScoresContainer()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
background = new Box background = new Box
@ -54,7 +100,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Margin = new MarginPadding { Vertical = spacing }, Margin = new MarginPadding { Vertical = spacing },
Children = new Drawable[] Children = new Drawable[]
{ {
topScore = new DrawableTopScore(), topScoresContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
scoreTable = new ScoreTable scoreTable = new ScoreTable
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -65,7 +117,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
loadingAnimation = new LoadingAnimation loadingAnimation = new LoadingAnimation
{ {
Alpha = 0, Alpha = 0,
Margin = new MarginPadding(20) Margin = new MarginPadding(20),
}, },
}; };
} }
@ -74,7 +126,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
background.Colour = colours.Gray2; background.Colour = colours.Gray2;
updateDisplay();
} }
private bool loading private bool loading
@ -82,62 +133,23 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
set => loadingAnimation.FadeTo(value ? 1 : 0, fade_duration); set => loadingAnimation.FadeTo(value ? 1 : 0, fade_duration);
} }
private GetScoresRequest getScoresRequest; private void getScores(BeatmapInfo beatmap)
private IReadOnlyList<ScoreInfo> scores;
public IReadOnlyList<ScoreInfo> Scores
{
get => scores;
set
{
getScoresRequest?.Cancel();
scores = value;
updateDisplay();
}
}
private BeatmapInfo beatmap;
public BeatmapInfo Beatmap
{
get => beatmap;
set
{
beatmap = value;
Scores = null;
if (beatmap?.OnlineBeatmapID.HasValue != true)
return;
loading = true;
getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset);
getScoresRequest.Success += r => Schedule(() => Scores = r.Scores);
api.Queue(getScoresRequest);
}
}
private void updateDisplay()
{
loading = false;
scoreTable.Scores = scores?.Count > 1 ? scores : new List<ScoreInfo>();
scoreTable.FadeTo(scores?.Count > 1 ? 1 : 0);
if (scores?.Any() == true)
{
topScore.Score = scores.FirstOrDefault();
topScore.Show();
}
else
topScore.Hide();
}
protected override void Dispose(bool isDisposing)
{ {
getScoresRequest?.Cancel(); getScoresRequest?.Cancel();
getScoresRequest = null;
Scores = null;
if (beatmap?.OnlineBeatmapID.HasValue != true)
{
loading = false;
return;
}
getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset);
getScoresRequest.Success += scores => Scores = scores;
api.Queue(getScoresRequest);
loading = true;
} }
} }
} }

View File

@ -39,19 +39,28 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Spacing = new Vector2(10, 0), Spacing = new Vector2(10, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
rankText = new OsuSpriteText new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "#1", AutoSizeAxes = Axes.Both,
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold, italics: true) Direction = FillDirection.Vertical,
}, Children = new Drawable[]
rank = new UpdateableRank(ScoreRank.D) {
{ rankText = new OsuSpriteText
Anchor = Anchor.Centre, {
Origin = Anchor.Centre, Anchor = Anchor.Centre,
Size = new Vector2(40), Origin = Anchor.Centre,
FillMode = FillMode.Fit, Font = OsuFont.GetFont(size: 24, weight: FontWeight.Bold, italics: true)
},
rank = new UpdateableRank(ScoreRank.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(40),
FillMode = FillMode.Fit,
},
}
}, },
avatar = new UpdateableAvatar(hideImmediately: true) avatar = new UpdateableAvatar(hideImmediately: true)
{ {
@ -109,6 +118,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
rankText.Colour = colours.Yellow; rankText.Colour = colours.Yellow;
} }
public int ScorePosition
{
set => rankText.Text = $"#{value}";
}
/// <summary> /// <summary>
/// Sets the score to be displayed. /// Sets the score to be displayed.
/// </summary> /// </summary>

View File

@ -21,12 +21,9 @@ namespace osu.Game.Overlays
{ {
public class BeatmapSetOverlay : FullscreenOverlay public class BeatmapSetOverlay : FullscreenOverlay
{ {
private const int fade_duration = 300;
public const float X_PADDING = 40; public const float X_PADDING = 40;
public const float TOP_PADDING = 25; public const float TOP_PADDING = 25;
public const float RIGHT_WIDTH = 275; public const float RIGHT_WIDTH = 275;
protected readonly Header Header; protected readonly Header Header;
private RulesetStore rulesets; private RulesetStore rulesets;
@ -40,7 +37,7 @@ namespace osu.Game.Overlays
{ {
OsuScrollContainer scroll; OsuScrollContainer scroll;
Info info; Info info;
ScoresContainer scores; ScoresContainer scoreContainer;
Children = new Drawable[] Children = new Drawable[]
{ {
@ -62,7 +59,7 @@ namespace osu.Game.Overlays
{ {
Header = new Header(), Header = new Header(),
info = new Info(), info = new Info(),
scores = new ScoresContainer(), scoreContainer = new ScoresContainer(),
}, },
}, },
}, },
@ -74,7 +71,7 @@ namespace osu.Game.Overlays
Header.Picker.Beatmap.ValueChanged += b => Header.Picker.Beatmap.ValueChanged += b =>
{ {
info.Beatmap = b.NewValue; info.Beatmap = b.NewValue;
scores.Beatmap = b.NewValue; scoreContainer.Beatmap = b.NewValue;
scroll.ScrollToStart(); scroll.ScrollToStart();
}; };
@ -101,6 +98,7 @@ namespace osu.Game.Overlays
public void FetchAndShowBeatmap(int beatmapId) public void FetchAndShowBeatmap(int beatmapId)
{ {
beatmapSet.Value = null; beatmapSet.Value = null;
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
req.Success += res => req.Success += res =>
{ {
@ -108,15 +106,18 @@ namespace osu.Game.Overlays
Header.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); Header.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId);
}; };
API.Queue(req); API.Queue(req);
Show(); Show();
} }
public void FetchAndShowBeatmapSet(int beatmapSetId) public void FetchAndShowBeatmapSet(int beatmapSetId)
{ {
beatmapSet.Value = null; beatmapSet.Value = null;
var req = new GetBeatmapSetRequest(beatmapSetId); var req = new GetBeatmapSetRequest(beatmapSetId);
req.Success += res => beatmapSet.Value = res.ToBeatmapSet(rulesets); req.Success += res => beatmapSet.Value = res.ToBeatmapSet(rulesets);
API.Queue(req); API.Queue(req);
Show(); Show();
} }

View File

@ -58,7 +58,11 @@ namespace osu.Game.Overlays.Changelog
} }
if (build != null) if (build != null)
Child = new ChangelogBuildWithNavigation(build) { SelectBuild = SelectBuild }; Children = new Drawable[]
{
new ChangelogBuildWithNavigation(build) { SelectBuild = SelectBuild },
new Comments(build)
};
} }
public class ChangelogBuildWithNavigation : ChangelogBuild public class ChangelogBuildWithNavigation : ChangelogBuild

View File

@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK.Graphics;
namespace osu.Game.Overlays.Changelog
{
public class Comments : CompositeDrawable
{
private readonly APIChangelogBuild build;
public Comments(APIChangelogBuild build)
{
this.build = build;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding
{
Horizontal = 50,
Vertical = 20,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
LinkFlowContainer text;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreyVioletDarker
},
},
text = new LinkFlowContainer(t =>
{
t.Colour = colours.PinkLighter;
t.Font = OsuFont.Default.With(size: 14);
})
{
Padding = new MarginPadding(20),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
text.AddParagraph("Got feedback?", t =>
{
t.Colour = Color4.White;
t.Font = OsuFont.Default.With(italics: true, size: 20);
t.Padding = new MarginPadding { Bottom = 20 };
});
text.AddParagraph("We would love to hear what you think of this update! ");
text.AddIcon(FontAwesome.Regular.GrinHearts);
text.AddParagraph("Please visit the ");
text.AddLink("web version", $"{build.Url}#comments");
text.AddText(" of this changelog to leave any comments.");
}
}
}

View File

@ -55,6 +55,8 @@ namespace osu.Game.Overlays
private Container dragContainer; private Container dragContainer;
private Container playerContainer; private Container playerContainer;
public bool IsUserPaused { get; private set; }
[Resolved] [Resolved]
private Bindable<WorkingBeatmap> beatmap { get; set; } private Bindable<WorkingBeatmap> beatmap { get; set; }
@ -157,7 +159,7 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(1.4f), Scale = new Vector2(1.4f),
IconScale = new Vector2(1.4f), IconScale = new Vector2(1.4f),
Action = play, Action = togglePause,
Icon = FontAwesome.Regular.PlayCircle, Icon = FontAwesome.Regular.PlayCircle,
}, },
nextButton = new MusicIconButton nextButton = new MusicIconButton
@ -276,7 +278,7 @@ namespace osu.Game.Overlays
} }
} }
private void play() private void togglePause()
{ {
var track = current?.Track; var track = current?.Track;
@ -288,9 +290,15 @@ namespace osu.Game.Overlays
} }
if (track.IsRunning) if (track.IsRunning)
{
IsUserPaused = true;
track.Stop(); track.Stop();
}
else else
{
track.Start(); track.Start();
IsUserPaused = false;
}
} }
private void prev() private void prev()

View File

@ -35,6 +35,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Bindable = config.GetBindable<bool>(OsuSetting.ShowInterface) Bindable = config.GetBindable<bool>(OsuSetting.ShowInterface)
}, },
new SettingsCheckbox new SettingsCheckbox
{
LabelText = "Show health display even when you can't fail",
Bindable = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
},
new SettingsCheckbox
{ {
LabelText = "Always show key overlay", LabelText = "Always show key overlay",
Bindable = config.GetBindable<bool>(OsuSetting.KeyOverlay) Bindable = config.GetBindable<bool>(OsuSetting.KeyOverlay)

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Overlays.Settings.Sections.Maintenance namespace osu.Game.Overlays.Settings.Sections.Maintenance
@ -16,14 +17,16 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
protected override string Header => "General"; protected override string Header => "General";
private TriangleButton importBeatmapsButton; private TriangleButton importBeatmapsButton;
private TriangleButton importScoresButton;
private TriangleButton importSkinsButton; private TriangleButton importSkinsButton;
private TriangleButton deleteSkinsButton;
private TriangleButton deleteBeatmapsButton; private TriangleButton deleteBeatmapsButton;
private TriangleButton deleteScoresButton;
private TriangleButton deleteSkinsButton;
private TriangleButton restoreButton; private TriangleButton restoreButton;
private TriangleButton undeleteButton; private TriangleButton undeleteButton;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay) private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay)
{ {
if (beatmaps.SupportsImportFromStable) if (beatmaps.SupportsImportFromStable)
{ {
@ -51,6 +54,32 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
} }
}); });
if (scores.SupportsImportFromStable)
{
Add(importScoresButton = new SettingsButton
{
Text = "Import scores from stable",
Action = () =>
{
importScoresButton.Enabled.Value = false;
scores.ImportFromStableAsync().ContinueWith(t => Schedule(() => importScoresButton.Enabled.Value = true));
}
});
}
Add(deleteScoresButton = new DangerousSettingsButton
{
Text = "Delete ALL scores",
Action = () =>
{
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{
deleteScoresButton.Enabled.Value = false;
Task.Run(() => scores.Delete(scores.GetAllUsableScores())).ContinueWith(t => Schedule(() => deleteScoresButton.Enabled.Value = true));
}));
}
});
if (skins.SupportsImportFromStable) if (skins.SupportsImportFromStable)
{ {
Add(importSkinsButton = new SettingsButton Add(importSkinsButton = new SettingsButton

View File

@ -0,0 +1,18 @@
// 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.Screens.Play;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// An interface for mods that apply changes to the <see cref="HUDOverlay"/>.
/// </summary>
public interface IApplicableToHUD : IApplicableMod
{
/// <summary>
/// Provide a <see cref="HUDOverlay"/>. Called once on initialisation of a play instance.
/// </summary>
void ApplyToHUD(HUDOverlay overlay);
}
}

View File

@ -0,0 +1,29 @@
// 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;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModBlockFail : Mod, IApplicableFailOverride, IApplicableToHUD, IReadFromConfig
{
private Bindable<bool> showHealthBar;
/// <summary>
/// We never fail, 'yo.
/// </summary>
public bool AllowFail => false;
public void ReadFromConfig(OsuConfigManager config)
{
showHealthBar = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail);
}
public void ApplyToHUD(HUDOverlay overlay)
{
overlay.ShowHealthbar.BindTo(showHealthBar);
}
}
}

View File

@ -7,7 +7,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModNoFail : Mod, IApplicableFailOverride public abstract class ModNoFail : ModBlockFail
{ {
public override string Name => "No Fail"; public override string Name => "No Fail";
public override string Acronym => "NF"; public override string Acronym => "NF";
@ -17,10 +17,5 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 0.5; public override double ScoreMultiplier => 0.5;
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModAutoplay) };
/// <summary>
/// We never fail, 'yo.
/// </summary>
public bool AllowFail => false;
} }
} }

View File

@ -7,7 +7,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModRelax : Mod public abstract class ModRelax : ModBlockFail
{ {
public override string Name => "Relax"; public override string Name => "Relax";
public override string Acronym => "RX"; public override string Acronym => "RX";

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Replays
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
/// Disabling this can make replay playback smoother (useful for autoplay, currently). /// Disabling this can make replay playback smoother (useful for autoplay, currently).
/// </summary> /// </summary>
public bool FrameAccuratePlayback = true; public bool FrameAccuratePlayback = false;
protected bool HasFrames => Frames.Count > 0; protected bool HasFrames => Frames.Count > 0;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -24,7 +25,7 @@ namespace osu.Game.Scoring
protected override string[] HashableFileTypes => new[] { ".osr" }; protected override string[] HashableFileTypes => new[] { ".osr" };
protected override string ImportFromStablePath => "Replays"; protected override string ImportFromStablePath => Path.Combine("Data", "r");
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps; private readonly Func<BeatmapManager> beatmaps;
@ -55,6 +56,9 @@ namespace osu.Game.Scoring
} }
} }
protected override IEnumerable<string> GetStableImportPaths(Storage stableStorage)
=> stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.InvariantCultureIgnoreCase) ?? false));
public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);
public List<ScoreInfo> GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); public List<ScoreInfo> GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
@ -65,6 +69,6 @@ namespace osu.Game.Scoring
protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score);
protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items) => items.Any(s => s.OnlineScoreID == model.OnlineScoreID); protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items) => items.Any(s => s.OnlineScoreID == model.OnlineScoreID && s.Files.Any());
} }
} }

View File

@ -224,30 +224,32 @@ namespace osu.Game.Screens.Edit
public override void OnResuming(IScreen last) public override void OnResuming(IScreen last)
{ {
Beatmap.Value.Track?.Stop();
base.OnResuming(last); base.OnResuming(last);
Beatmap.Value.Track?.Stop();
} }
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
Background.FadeColour(Color4.DarkGray, 500); Background.FadeColour(Color4.DarkGray, 500);
Beatmap.Value.Track?.Stop(); resetTrack();
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
Background.FadeColour(Color4.White, 500); Background.FadeColour(Color4.White, 500);
resetTrack();
if (Beatmap.Value.Track != null)
{
Beatmap.Value.Track.Tempo.Value = 1;
Beatmap.Value.Track.Start();
}
return base.OnExiting(next); return base.OnExiting(next);
} }
private void resetTrack()
{
Beatmap.Value.Track?.ResetSpeedAdjustments();
Beatmap.Value.Track?.Stop();
}
private void exportBeatmap() => host.OpenFileExternally(Beatmap.Value.Save()); private void exportBeatmap() => host.OpenFileExternally(Beatmap.Value.Save());
private void onModeChanged(ValueChangedEvent<EditorScreenMode> e) private void onModeChanged(ValueChangedEvent<EditorScreenMode> e)

View File

@ -332,7 +332,7 @@ namespace osu.Game.Screens.Menu
break; break;
case ButtonSystemState.EnteringMode: case ButtonSystemState.EnteringMode:
logoTrackingContainer.StartTracking(logo, 0, Easing.In); logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.Initial ? MainMenu.FADE_OUT_DURATION : 0, Easing.InSine);
break; break;
} }
} }

View File

@ -33,13 +33,18 @@ namespace osu.Game.Screens.Menu
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack(); protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack();
private readonly BindableDouble exitingVolumeFade = new BindableDouble(1);
[Resolved]
private AudioManager audio { get; set; }
private Bindable<bool> menuVoice; private Bindable<bool> menuVoice;
private Bindable<bool> menuMusic; private Bindable<bool> menuMusic;
private Track track; private Track track;
private WorkingBeatmap introBeatmap; private WorkingBeatmap introBeatmap;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game) private void load(OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game)
{ {
menuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice); menuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice);
menuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic); menuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
@ -161,7 +166,8 @@ namespace osu.Game.Screens.Menu
else else
fadeOutTime = 500; fadeOutTime = 500;
Scheduler.AddDelayed(this.Exit, fadeOutTime); audio.AddAdjustment(AdjustableProperty.Volume, exitingVolumeFade);
this.TransformBindableTo(exitingVolumeFade, 0, fadeOutTime).OnComplete(_ => this.Exit());
//don't want to fade out completely else we will stop running updates. //don't want to fade out completely else we will stop running updates.
Game.FadeTo(0.01f, fadeOutTime); Game.FadeTo(0.01f, fadeOutTime);

View File

@ -23,7 +23,9 @@ namespace osu.Game.Screens.Menu
{ {
public class MainMenu : OsuScreen public class MainMenu : OsuScreen
{ {
private ButtonSystem buttons; public const float FADE_IN_DURATION = 300;
public const float FADE_OUT_DURATION = 400;
public override bool HideOverlaysOnEnter => buttons == null || buttons.State == ButtonSystemState.Initial; public override bool HideOverlaysOnEnter => buttons == null || buttons.State == ButtonSystemState.Initial;
@ -35,9 +37,14 @@ namespace osu.Game.Screens.Menu
private MenuSideFlashes sideFlashes; private MenuSideFlashes sideFlashes;
private ButtonSystem buttons;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
[Resolved(canBeNull: true)]
private MusicController music { get; set; }
private BackgroundScreenDefault background; private BackgroundScreenDefault background;
protected override BackgroundScreen CreateBackground() => background; protected override BackgroundScreen CreateBackground() => background;
@ -141,12 +148,10 @@ namespace osu.Game.Screens.Menu
{ {
buttons.State = ButtonSystemState.TopLevel; buttons.State = ButtonSystemState.TopLevel;
const float length = 300; this.FadeIn(FADE_IN_DURATION, Easing.OutQuint);
this.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint);
this.FadeIn(length, Easing.OutQuint); sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint);
this.MoveTo(new Vector2(0, 0), length, Easing.OutQuint);
sideFlashes.Delay(length).FadeIn(64, Easing.InQuint);
} }
} }
@ -171,12 +176,10 @@ namespace osu.Game.Screens.Menu
{ {
base.OnSuspending(next); base.OnSuspending(next);
const float length = 400;
buttons.State = ButtonSystemState.EnteringMode; buttons.State = ButtonSystemState.EnteringMode;
this.FadeOut(length, Easing.InSine); this.FadeOut(FADE_OUT_DURATION, Easing.InSine);
this.MoveTo(new Vector2(-800, 0), length, Easing.InSine); this.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine);
sideFlashes.FadeOut(64, Easing.OutQuint); sideFlashes.FadeOut(64, Easing.OutQuint);
} }
@ -189,6 +192,9 @@ namespace osu.Game.Screens.Menu
//we may have consumed our preloaded instance, so let's make another. //we may have consumed our preloaded instance, so let's make another.
preloadSongSelect(); preloadSongSelect();
if (Beatmap.Value.Track != null && music?.IsUserPaused != true)
Beatmap.Value.Track.Start();
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)

View File

@ -23,7 +23,8 @@ namespace osu.Game.Screens.Play
{ {
public class HUDOverlay : Container public class HUDOverlay : Container
{ {
private const int duration = 100; private const int duration = 250;
private const Easing easing = Easing.OutQuint;
public readonly KeyCounterDisplay KeyCounter; public readonly KeyCounterDisplay KeyCounter;
public readonly RollingCounter<int> ComboCounter; public readonly RollingCounter<int> ComboCounter;
@ -35,6 +36,8 @@ namespace osu.Game.Screens.Play
public readonly HoldForMenuButton HoldToQuit; public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay; public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
public Bindable<bool> ShowHealthbar = new Bindable<bool>(true);
private readonly ScoreProcessor scoreProcessor; private readonly ScoreProcessor scoreProcessor;
private readonly DrawableRuleset drawableRuleset; private readonly DrawableRuleset drawableRuleset;
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
@ -47,6 +50,8 @@ namespace osu.Game.Screens.Play
public Action<double> RequestSeek; public Action<double> RequestSeek;
private readonly Container topScoreContainer;
public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods) public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
{ {
this.scoreProcessor = scoreProcessor; this.scoreProcessor = scoreProcessor;
@ -62,11 +67,10 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
new Container topScoreContainer = new Container
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Y = 30,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
AutoSizeDuration = 200, AutoSizeDuration = 200,
AutoSizeEasing = Easing.Out, AutoSizeEasing = Easing.Out,
@ -113,8 +117,21 @@ namespace osu.Game.Screens.Play
ModDisplay.Current.Value = mods; ModDisplay.Current.Value = mods;
showHud = config.GetBindable<bool>(OsuSetting.ShowInterface); showHud = config.GetBindable<bool>(OsuSetting.ShowInterface);
showHud.ValueChanged += visible => visibilityContainer.FadeTo(visible.NewValue ? 1 : 0, duration); showHud.BindValueChanged(visible => visibilityContainer.FadeTo(visible.NewValue ? 1 : 0, duration, easing), true);
showHud.TriggerChange();
ShowHealthbar.BindValueChanged(healthBar =>
{
if (healthBar.NewValue)
{
HealthDisplay.FadeIn(duration, easing);
topScoreContainer.MoveToY(30, duration, easing);
}
else
{
HealthDisplay.FadeOut(duration, easing);
topScoreContainer.MoveToY(0, duration, easing);
}
}, true);
if (!showHud.Value && !hasShownNotificationOnce) if (!showHud.Value && !hasShownNotificationOnce)
{ {

View File

@ -498,6 +498,9 @@ namespace osu.Game.Screens.Play
GameplayClockContainer.Restart(); GameplayClockContainer.Restart();
GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint);
foreach (var mod in Mods.Value.OfType<IApplicableToHUD>())
mod.ApplyToHUD(HUDOverlay);
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)

View File

@ -18,6 +18,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -53,6 +54,8 @@ namespace osu.Game.Screens.Play
private InputManager inputManager; private InputManager inputManager;
private IdleTracker idleTracker;
public PlayerLoader(Func<Player> createPlayer) public PlayerLoader(Func<Player> createPlayer)
{ {
this.createPlayer = createPlayer; this.createPlayer = createPlayer;
@ -93,7 +96,8 @@ namespace osu.Game.Screens.Play
VisualSettings = new VisualSettings(), VisualSettings = new VisualSettings(),
new InputSettings() new InputSettings()
} }
} },
idleTracker = new IdleTracker(750)
}); });
loadNewPlayer(); loadNewPlayer();
@ -193,7 +197,7 @@ namespace osu.Game.Screens.Play
// Here because IsHovered will not update unless we do so. // Here because IsHovered will not update unless we do so.
public override bool HandlePositionalInput => true; public override bool HandlePositionalInput => true;
private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && GetContainingInputManager()?.DraggedDrawable == null; private bool readyForPush => player.LoadState == LoadState.Ready && (IsHovered || idleTracker.IsIdle.Value) && inputManager?.DraggedDrawable == null;
private void pushWhenLoaded() private void pushWhenLoaded()
{ {

View File

@ -86,11 +86,7 @@ namespace osu.Game.Screens.Play
} }
}, true); }, true);
if (replayAvailability == ReplayAvailability.NotAvailable) button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable;
{
button.Enabled.Value = false;
button.Alpha = 0.6f;
}
} }
private enum ReplayAvailability private enum ReplayAvailability

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Game.Scoring;
namespace osu.Game.Screens.Play
{
public class ReplayPlayerLoader : PlayerLoader
{
private readonly ScoreInfo scoreInfo;
public ReplayPlayerLoader(Score score)
: base(() => new ReplayPlayer(score))
{
if (score.Replay == null)
throw new ArgumentNullException(nameof(score.Replay), $"{nameof(score)} must have a non-null {nameof(score.Replay)}.");
scoreInfo = score.ScoreInfo;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = base.CreateChildDependencies(parent);
// these will be reverted thanks to PlayerLoader's lease.
Mods.Value = scoreInfo.Mods;
Ruleset.Value = scoreInfo.Ruleset;
return dependencies;
}
}
}

View File

@ -41,6 +41,8 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
OnFilter = (tab, mods) => OnFilter = (tab, mods) =>
{ {
Leaderboard.FilterMods = mods;
switch (tab) switch (tab)
{ {
case BeatmapDetailTab.Details: case BeatmapDetailTab.Details:

View File

@ -18,8 +18,6 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
@ -289,14 +287,11 @@ namespace osu.Game.Screens.Select
if (b?.HitObjects?.Any() == true) if (b?.HitObjects?.Any() == true)
{ {
HitObject lastObject = b.HitObjects.LastOrDefault();
double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0;
labels.Add(new InfoLabel(new BeatmapStatistic labels.Add(new InfoLabel(new BeatmapStatistic
{ {
Name = "Length", Name = "Length",
Icon = FontAwesome.Regular.Clock, Icon = FontAwesome.Regular.Clock,
Content = TimeSpan.FromMilliseconds(endTime - b.HitObjects.First().StartTime).ToString(@"m\:ss"), Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"),
})); }));
labels.Add(new InfoLabel(new BeatmapStatistic labels.Add(new InfoLabel(new BeatmapStatistic

View File

@ -48,6 +48,12 @@ namespace osu.Game.Screens.Select.Carousel
case SortMode.DateAdded: case SortMode.DateAdded:
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
case SortMode.BPM:
return BeatmapSet.MaxBPM.CompareTo(otherSet.BeatmapSet.MaxBPM);
case SortMode.Length:
return BeatmapSet.MaxLength.CompareTo(otherSet.BeatmapSet.MaxLength);
case SortMode.Difficulty: case SortMode.Difficulty:
return BeatmapSet.MaxStarDifficulty.CompareTo(otherSet.BeatmapSet.MaxStarDifficulty); return BeatmapSet.MaxStarDifficulty.CompareTo(otherSet.BeatmapSet.MaxStarDifficulty);
} }

View File

@ -1,18 +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.Screens;
namespace osu.Game.Screens.Select
{
public class EditSongSelect : SongSelect
{
protected override bool ShowFooter => false;
protected override bool OnStart()
{
this.Exit();
return true;
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select
public ImportFromStablePopup(Action importFromStable) public ImportFromStablePopup(Action importFromStable)
{ {
HeaderText = @"You have no beatmaps!"; HeaderText = @"You have no beatmaps!";
BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps (and skins)?"; BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins and scores?";
Icon = FontAwesome.Solid.Plane; Icon = FontAwesome.Solid.Plane;

View File

@ -11,6 +11,7 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Screens.Select.Leaderboards namespace osu.Game.Screens.Select.Leaderboards
@ -36,12 +37,34 @@ namespace osu.Game.Screens.Select.Leaderboards
} }
} }
private bool filterMods;
/// <summary>
/// Whether to apply the game's currently selected mods as a filter when retrieving scores.
/// </summary>
public bool FilterMods
{
get => filterMods;
set
{
if (value == filterMods)
return;
filterMods = value;
UpdateScores();
}
}
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
@ -49,32 +72,66 @@ namespace osu.Game.Screens.Select.Leaderboards
private void load() private void load()
{ {
ruleset.ValueChanged += _ => UpdateScores(); ruleset.ValueChanged += _ => UpdateScores();
mods.ValueChanged += _ =>
{
if (filterMods)
UpdateScores();
};
} }
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{ {
if (Scope == BeatmapLeaderboardScope.Local) if (Scope == BeatmapLeaderboardScope.Local)
{ {
Scores = scoreManager var scores = scoreManager
.QueryScores(s => !s.DeletePending && s.Beatmap.ID == Beatmap.ID && s.Ruleset.ID == ruleset.Value.ID) .QueryScores(s => !s.DeletePending && s.Beatmap.ID == Beatmap.ID && s.Ruleset.ID == ruleset.Value.ID);
.OrderByDescending(s => s.TotalScore).ToArray();
if (filterMods && !mods.Value.Any())
{
// we need to filter out all scores that have any mods to get all local nomod scores
scores = scores.Where(s => !s.Mods.Any());
}
else if (filterMods)
{
// otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters)
// we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself
var selectedMods = mods.Value.Select(m => m.Acronym);
scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
}
Scores = scores.OrderByDescending(s => s.TotalScore).ToArray();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
return null; return null;
} }
if (Beatmap?.OnlineBeatmapID == null) if (api?.IsLoggedIn != true)
{
PlaceholderState = PlaceholderState.NotLoggedIn;
return null;
}
if (Beatmap?.OnlineBeatmapID == null || Beatmap?.Status <= BeatmapSetOnlineStatus.Pending)
{ {
PlaceholderState = PlaceholderState.Unavailable; PlaceholderState = PlaceholderState.Unavailable;
return null; return null;
} }
if (Scope != BeatmapLeaderboardScope.Global && !api.LocalUser.Value.IsSupporter) if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods))
{ {
PlaceholderState = PlaceholderState.NotSupporter; PlaceholderState = PlaceholderState.NotSupporter;
return null; return null;
} }
var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope); IReadOnlyList<Mod> requestMods = null;
if (filterMods && !mods.Value.Any())
// add nomod for the request
requestMods = new Mod[] { new ModNoMod() };
else if (filterMods)
requestMods = mods.Value;
var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods);
req.Success += r => scoresCallback?.Invoke(r.Scores); req.Success += r => scoresCallback?.Invoke(r.Scores);

View File

@ -35,6 +35,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Scoring;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -86,6 +87,9 @@ namespace osu.Game.Screens.Select
private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>(); private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>();
[Resolved(canBeNull: true)]
private MusicController music { get; set; }
[Cached] [Cached]
[Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))] [Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))]
private readonly Bindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); // Bound to the game's mods, but is not reset on exiting private readonly Bindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); // Bound to the game's mods, but is not reset on exiting
@ -215,7 +219,7 @@ namespace osu.Game.Screens.Select
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins) private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores)
{ {
mods.BindTo(Mods); mods.BindTo(Mods);
@ -252,7 +256,7 @@ namespace osu.Game.Screens.Select
if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable) if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable)
dialogOverlay.Push(new ImportFromStablePopup(() => dialogOverlay.Push(new ImportFromStablePopup(() =>
{ {
Task.Run(beatmaps.ImportFromStableAsync); Task.Run(beatmaps.ImportFromStableAsync).ContinueWith(_ => scores.ImportFromStableAsync(), TaskContinuationOptions.OnlyOnRanToCompletion);
Task.Run(skins.ImportFromStableAsync); Task.Run(skins.ImportFromStableAsync);
})); }));
}); });
@ -569,7 +573,7 @@ namespace osu.Game.Screens.Select
{ {
Track track = Beatmap.Value.Track; Track track = Beatmap.Value.Track;
if (!track.IsRunning || restart) if ((!track.IsRunning || restart) && music?.IsUserPaused != true)
{ {
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
track.Restart(); track.Restart();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading; using System.Threading;
@ -54,6 +55,8 @@ namespace osu.Game.Skinning
}; };
} }
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osk";
/// <summary> /// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>. /// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
/// </summary> /// </summary>

View File

@ -15,7 +15,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.704.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.711.0" />
<PackageReference Include="SharpCompress" Version="0.23.0" /> <PackageReference Include="SharpCompress" Version="0.23.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -105,8 +105,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.704.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.711.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.704.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.711.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />