1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 15:57:24 +08:00

Merge remote-tracking branch 'nekodex/master' into drawnode-changes

This commit is contained in:
smoogipoo 2019-03-07 18:15:12 +09:00
commit e430b8a640
107 changed files with 2424 additions and 605 deletions

1
.gitignore vendored
View File

@ -14,6 +14,7 @@
tools/** tools/**
build/tools/** build/tools/**
fastlane/report.xml
# Build results # Build results
bin/[Dd]ebug/ bin/[Dd]ebug/

6
Gemfile Normal file
View File

@ -0,0 +1,6 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

173
Gemfile.lock Normal file
View File

@ -0,0 +1,173 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.0)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
atomos (0.1.3)
babosa (1.0.2)
claide (1.0.2)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.1)
emoji_regex (1.0.1)
excon (0.62.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.5)
fastlane (2.117.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.45.0, < 1.0.0)
faraday (~> 0.9)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.9)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.21.2, < 0.24.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
mini_magick (~> 4.5.1)
multi_json
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.2.2, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 1.6.2, < 2.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.6.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-clean_testflight_testers (0.2.0)
fastlane-plugin-souyuz (0.8.1)
souyuz (>= 0.8.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
google-api-client (0.23.9)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.0)
google-cloud-env (~> 1.0)
google-cloud-env (1.0.5)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
google-api-client (~> 0.23)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
googleauth (0.6.7)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
json (2.2.0)
jwt (2.1.0)
memoist (0.16.0)
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mini_magick (4.5.1)
mini_portile2 (2.4.0)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
nokogiri (1.10.1)
mini_portile2 (~> 2.4.0)
os (1.0.0)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.2.2)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.5)
CFPropertyList
naturally
slack-notifier (2.3.2)
souyuz (0.8.1)
fastlane (>= 2.29.0)
highline (~> 1.7)
nokogiri (~> 1.7)
terminal-notifier (1.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.6.1)
tty-screen (0.6.5)
tty-spinner (0.9.0)
tty-cursor (~> 0.6.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.4.1)
word_wrap (1.0.0)
xcodeproj (1.8.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
fastlane
fastlane-plugin-clean_testflight_testers
fastlane-plugin-souyuz
fastlane-plugin-xamarin
BUNDLED WITH
2.0.1

View File

@ -1,22 +1,24 @@
# osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) # osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename "osu!lazer". Pew pew. Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename "osu!lazer". Pew pew.
# Status ## Status
This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable osu! client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table. This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable osu! client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table.
We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below. We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below.
# Requirements ## Requirements
- A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed. - A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed.
- When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2017+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). - When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2017+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- Note that there are **[additional requirements for Windows 7 and Windows 8.1](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** which you may need to manually install if your operating system is not up-to-date. - Note that there are **[additional requirements for Windows 7 and Windows 8.1](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** which you may need to manually install if your operating system is not up-to-date.
# Running osu! ## Running osu!
## Releases ### Releases
If you are not interested in developing the game, please head over to the [releases](https://github.com/ppy/osu/releases) to download a precompiled build with automatic updating enabled. If you are not interested in developing the game, please head over to the [releases](https://github.com/ppy/osu/releases) to download a precompiled build with automatic updating enabled.
@ -26,7 +28,7 @@ If you are not interested in developing the game, please head over to the [relea
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Downloading the source code ### Downloading the source code
Clone the repository **including submodules**: Clone the repository **including submodules**:
@ -41,7 +43,7 @@ To update the source code to the latest commit, run the following command inside
git pull git pull
``` ```
## Building ### Building
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided below. Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided below.
@ -57,7 +59,7 @@ If you are not interested in debugging osu!, you can add `-c Release` to gain pe
If the build fails, try to restore nuget packages with `dotnet restore`. If the build fails, try to restore nuget packages with `dotnet restore`.
### A note for Linux users #### A note for Linux users
On Linux, the environment variable `LD_LIBRARY_PATH` must point to the build directory, located at `osu.Desktop/bin/Debug/$NETCORE_VERSION`. On Linux, the environment variable `LD_LIBRARY_PATH` must point to the build directory, located at `osu.Desktop/bin/Debug/$NETCORE_VERSION`.
@ -69,15 +71,15 @@ For example, you can run osu! with the following command:
LD_LIBRARY_PATH="$(pwd)/osu.Desktop/bin/Debug/netcoreapp2.2" dotnet run --project osu.Desktop LD_LIBRARY_PATH="$(pwd)/osu.Desktop/bin/Debug/netcoreapp2.2" dotnet run --project osu.Desktop
``` ```
## Testing with resource/framework modifications ### Testing with resource/framework modifications
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be achieved by running some commands as documented on the [osu-resources](https://github.com/ppy/osu-resources/wiki/Testing-local-resources-checkout-with-other-projects) and [osu-framework](https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects) wiki pages. Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be achieved by running some commands as documented on the [osu-resources](https://github.com/ppy/osu-resources/wiki/Testing-local-resources-checkout-with-other-projects) and [osu-framework](https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects) wiki pages.
## Code analysis ### Code analysis
Code analysis can be run with `powershell ./build.ps1` or `build.sh`. This is currently only supported under windows due to [resharper cli shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install resharper or use rider to get inline support in your IDE of choice. Code analysis can be run with `powershell ./build.ps1` or `build.sh`. This is currently only supported under windows due to [resharper cli shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install resharper or use rider to get inline support in your IDE of choice.
# Contributing ## Contributing
We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted. We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted.
@ -87,7 +89,7 @@ Contributions can be made via pull requests to this repository. We hope to credi
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. I welcome all feedback so we can make contributing to this project as pain-free as possible. Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. I welcome all feedback so we can make contributing to this project as pain-free as possible.
# Licence ## Licence
The osu! client code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. The osu! client code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.

2
fastlane/Appfile Normal file
View File

@ -0,0 +1,2 @@
app_identifier("sh.ppy.osulazer") # The bundle identifier of your app
apple_id("apple-dev@ppy.sh") # Your Apple email address

65
fastlane/Fastfile Normal file
View File

@ -0,0 +1,65 @@
update_fastlane
default_platform(:ios)
platform :ios do
lane :testflight_prune_dry do
clean_testflight_testers(days_of_inactivity:45, dry_run: true)
end
# Specify a custom number for what's "inactive"
lane :testflight_prune do
clean_testflight_testers(days_of_inactivity: 45) # 120 days, so about 4 months
end
lane :update_version do |options|
options[:plist_path] = '../osu.iOS/Info.plist'
app_version(options)
end
desc 'Deploy to testflight'
lane :beta do |options|
update_version(options)
provision(
type: 'appstore'
)
build(
build_configuration: 'Release',
build_platform: 'iPhone'
)
client = HTTPClient.new
changelog = client.get_content 'https://gist.githubusercontent.com/peppy/ab89c29dcc0dce95f39eb218e8fad197/raw'
changelog.gsub!('$BUILD_ID', options[:build])
pilot(
wait_processing_interval: 900,
changelog: changelog,
ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa'
)
end
desc 'Compile the project'
lane :build do
nuget_restore(
project_path: 'osu.iOS.sln'
)
souyuz(
platform: "ios",
build_target: "osu_iOS",
plist_path: "../osu.iOS/Info.plist"
)
end
desc 'Install provisioning profiles using match'
lane :provision do |options|
if Helper.is_ci?
options[:readonly] = true
end
match(options)
end
end

1
fastlane/Matchfile Normal file
View File

@ -0,0 +1 @@
git_url('https://github.com/peppy/apple-certificates')

7
fastlane/Pluginfile Normal file
View File

@ -0,0 +1,7 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-clean_testflight_testers'
gem 'fastlane-plugin-souyuz'
gem 'fastlane-plugin-xamarin'

54
fastlane/README.md Normal file
View File

@ -0,0 +1,54 @@
fastlane documentation
================
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
# Available Actions
## iOS
### ios testflight_prune_dry
```
fastlane ios testflight_prune_dry
```
### ios testflight_prune
```
fastlane ios testflight_prune
```
### ios update_version
```
fastlane ios update_version
```
### ios beta
```
fastlane ios beta
```
Deploy to testflight
### ios build
```
fastlane ios build
```
Compile the project
### ios provision
```
fastlane ios provision
```
Install provisioning profiles using match
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@ -1,9 +1,11 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
@ -17,13 +19,30 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override IBeatmap CreateBeatmap(Ruleset ruleset) protected override IBeatmap CreateBeatmap(Ruleset ruleset)
{ {
var beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo } }; var beatmap = new Beatmap
{
BeatmapInfo =
{
Ruleset = ruleset.RulesetInfo,
BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f }
}
};
// Should produce a hperdash
beatmap.HitObjects.Add(new Fruit { StartTime = 816, X = 308 / 512f, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 1008, X = 56 / 512f, });
for (int i = 0; i < 512; i++) for (int i = 0; i < 512; i++)
if (i % 5 < 3) if (i % 5 < 3)
beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = i * 100, NewCombo = i % 8 == 0 }); beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = 2000 + i * 100, NewCombo = i % 8 == 0 });
return beatmap; return beatmap;
} }
protected override void AddCheckSteps(Func<Player> player)
{
base.AddCheckSteps(player);
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
}
} }
} }

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
CatchHitObject nextObject = objectWithDroplets[i + 1]; CatchHitObject nextObject = objectWithDroplets[i + 1];
int thisDirection = nextObject.X > currentObject.X ? 1 : -1; int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
double timeToNext = nextObject.StartTime - currentObject.StartTime; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext); float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext);
if (distanceToHyper < 0) if (distanceToHyper < 0)

View File

@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Mania.Tests
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache) private void load(RulesetConfigCache configCache)
{ {
var config = (ManiaConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
config.BindWith(ManiaSetting.ScrollDirection, direction); config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
} }
} }
} }

View File

@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
public int TargetColumns; public int TargetColumns;
public bool Dual;
public readonly bool IsForCurrentRuleset; public readonly bool IsForCurrentRuleset;
// Internal for testing purposes // Internal for testing purposes
@ -45,7 +46,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
if (IsForCurrentRuleset) if (IsForCurrentRuleset)
{
TargetColumns = (int)Math.Max(1, roundedCircleSize); TargetColumns = (int)Math.Max(1, roundedCircleSize);
if (TargetColumns >= 10)
{
TargetColumns = TargetColumns / 2;
Dual = true;
}
}
else else
{ {
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count; float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
@ -70,7 +78,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return base.ConvertBeatmap(original); return base.ConvertBeatmap(original);
} }
protected override Beatmap<ManiaHitObject> CreateBeatmap() => beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }); protected override Beatmap<ManiaHitObject> CreateBeatmap()
{
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
return beatmap;
}
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap) protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
{ {

View File

@ -8,9 +8,9 @@ using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Configuration namespace osu.Game.Rulesets.Mania.Configuration
{ {
public class ManiaConfigManager : RulesetConfigManager<ManiaSetting> public class ManiaRulesetConfigManager : RulesetConfigManager<ManiaRulesetSetting>
{ {
public ManiaConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) public ManiaRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant) : base(settings, ruleset, variant)
{ {
} }
@ -19,17 +19,17 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
base.InitialiseDefaults(); base.InitialiseDefaults();
Set(ManiaSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0); Set(ManiaRulesetSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down); Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
} }
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{ {
new TrackedSetting<double>(ManiaSetting.ScrollTime, v => new SettingDescription(v, "Scroll Time", $"{v}ms")) new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime, v => new SettingDescription(v, "Scroll Time", $"{v}ms"))
}; };
} }
public enum ManiaSetting public enum ManiaRulesetSetting
{ {
ScrollTime, ScrollTime,
ScrollDirection ScrollDirection

View File

@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Mania
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaConfigManager(settings, RulesetInfo); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this); public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);

View File

@ -22,19 +22,19 @@ namespace osu.Game.Rulesets.Mania
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var config = (ManiaConfigManager)Config; var config = (ManiaRulesetConfigManager)Config;
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsEnumDropdown<ManiaScrollingDirection> new SettingsEnumDropdown<ManiaScrollingDirection>
{ {
LabelText = "Scrolling direction", LabelText = "Scrolling direction",
Bindable = config.GetBindable<ManiaScrollingDirection>(ManiaSetting.ScrollDirection) Bindable = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<double, TimeSlider> new SettingsSlider<double, TimeSlider>
{ {
LabelText = "Scroll speed", LabelText = "Scroll speed",
Bindable = config.GetBindable<double>(ManiaSetting.ScrollTime) Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime)
}, },
}; };
} }

View File

@ -1,15 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToBeatmap<ManiaHitObject> public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter
{ {
public override string Name => "Dual Stages"; public override string Name => "Dual Stages";
public override string Acronym => "DS"; public override string Acronym => "DS";
@ -29,24 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
if (isForCurrentRuleset) if (isForCurrentRuleset)
return; return;
mbc.TargetColumns *= 2; mbc.Dual = true;
}
public void ApplyToBeatmap(Beatmap<ManiaHitObject> beatmap)
{
if (isForCurrentRuleset)
return;
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newDefinitions = new List<StageDefinition>();
foreach (var existing in maniaBeatmap.Stages)
{
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
}
maniaBeatmap.Stages = newDefinitions;
} }
public PlayfieldType PlayfieldType => PlayfieldType.Dual; public PlayfieldType PlayfieldType => PlayfieldType.Dual;

View File

@ -15,7 +15,6 @@ using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines; public IEnumerable<BarLine> BarLines;
protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
@ -75,10 +74,10 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
BarLines.ForEach(Playfield.Add); BarLines.ForEach(Playfield.Add);
Config.BindWith(ManiaSetting.ScrollDirection, configDirection); Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaSetting.ScrollTime, TimeRange); Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange);
} }
/// <summary> /// <summary>
@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
public override int Variant => (int)(Mods.OfType<IPlayfieldTypeMod>().FirstOrDefault()?.PlayfieldType ?? PlayfieldType.Single) + Beatmap.TotalColumns; public override int Variant => (int)(Beatmap.Stages.Count == 1 ? PlayfieldType.Single : PlayfieldType.Dual) + Beatmap.TotalColumns;
public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
namespace osu.Game.Rulesets.Osu.Configuration
{
public class OsuRulesetConfigManager : RulesetConfigManager<OsuRulesetSetting>
{
public OsuRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{
}
protected override void InitialiseDefaults()
{
base.InitialiseDefaults();
Set(OsuRulesetSetting.SnakingInSliders, true);
Set(OsuRulesetSetting.SnakingOutSliders, true);
}
}
public enum OsuRulesetSetting
{
SnakingInSliders,
SnakingOutSliders
}
}

View File

@ -10,8 +10,8 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -33,6 +33,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable<float> scaleBindable = new Bindable<float>(); private readonly IBindable<float> scaleBindable = new Bindable<float>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>(); private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
public DrawableSlider(Slider s) public DrawableSlider(Slider s)
: base(s) : base(s)
{ {
@ -94,10 +97,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load()
{ {
config.BindWith(OsuSetting.SnakingInSliders, Body.SnakingIn); config?.BindWith(OsuRulesetSetting.SnakingInSliders, Body.SnakingIn);
config.BindWith(OsuSetting.SnakingOutSliders, Body.SnakingOut); config?.BindWith(OsuRulesetSetting.SnakingOutSliders, Body.SnakingOut);
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
scaleBindable.BindValueChanged(scale => scaleBindable.BindValueChanged(scale =>

View File

@ -16,8 +16,11 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Difficulty; using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -144,12 +147,14 @@ namespace osu.Game.Rulesets.Osu
public override string ShortName => "osu"; public override string ShortName => "osu";
public override RulesetSettingsSubsection CreateSettings() => new OsuSettings(this); public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
public override int? LegacyID => 0; public override int? LegacyID => 0;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
public OsuRuleset(RulesetInfo rulesetInfo = null) public OsuRuleset(RulesetInfo rulesetInfo = null)
: base(rulesetInfo) : base(rulesetInfo)
{ {

View File

@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
@ -20,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuRulesetContainer : RulesetContainer<OsuPlayfield, OsuHitObject> public class OsuRulesetContainer : RulesetContainer<OsuPlayfield, OsuHitObject>
{ {
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {

View File

@ -3,34 +3,36 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu.Configuration;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuSettings : RulesetSettingsSubsection public class OsuSettingsSubsection : RulesetSettingsSubsection
{ {
protected override string Header => "osu!"; protected override string Header => "osu!";
public OsuSettings(Ruleset ruleset) public OsuSettingsSubsection(Ruleset ruleset)
: base(ruleset) : base(ruleset)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load()
{ {
var config = (OsuRulesetConfigManager)Config;
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Snaking in sliders", LabelText = "Snaking in sliders",
Bindable = config.GetBindable<bool>(OsuSetting.SnakingInSliders) Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Snaking out sliders", LabelText = "Snaking out sliders",
Bindable = config.GetBindable<bool>(OsuSetting.SnakingOutSliders) Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
}, },
}; };
} }

View File

@ -333,6 +333,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First());
Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
} }
SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);

View File

@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps.IO
int fireCount = 0; int fireCount = 0;
// ReSharper disable once AccessToModifiedClosure // ReSharper disable once AccessToModifiedClosure
manager.ItemAdded += (_, __, ___) => fireCount++; manager.ItemAdded += (_, __) => fireCount++;
manager.ItemRemoved += _ => fireCount++; manager.ItemRemoved += _ => fireCount++;
var imported = LoadOszIntoOsu(osu); var imported = LoadOszIntoOsu(osu);

View File

@ -13,4 +13,4 @@ SampleSet: Normal
255,193,2170,1,0,0:0:0:0:hit_1.wav 255,193,2170,1,0,0:0:0:0:hit_1.wav
256,191,2638,5,0,0:0:0:0:hit_2.wav 256,191,2638,5,0,0:0:0:0:hit_2.wav
255,193,3107,1,0,0:0:0:0: 255,193,3107,1,0,0:0:0:0:
256,191,3576,1,0,0:0:0:0:hit_1.wav 256,191,3576,1,0,0:0:0:70:hit_1.wav

View File

@ -0,0 +1,414 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseBackgroundScreenBeatmap : ManualInputManagerTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ScreenWithBeatmapBackground),
typeof(PlayerLoader),
typeof(Player),
typeof(UserDimContainer),
typeof(OsuScreen)
};
private DummySongSelect songSelect;
private DimAccessiblePlayerLoader playerLoader;
private DimAccessiblePlayer player;
private DatabaseContextFactory factory;
private BeatmapManager manager;
private RulesetStore rulesets;
private ScreenStackCacheContainer screenStackContainer;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
factory = new DatabaseContextFactory(LocalStorage);
factory.ResetDatabase();
using (var usage = factory.Get())
usage.Migrate();
factory.ResetDatabase();
using (var usage = factory.Get())
usage.Migrate();
Dependencies.Cache(rulesets = new RulesetStore(factory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
Beatmap.SetDefault();
}
[SetUp]
public virtual void SetUp()
{
Schedule(() =>
{
manager.Delete(manager.GetAllUsableBeatmapSets());
var temp = TestResources.GetTestBeatmapForImport();
manager.Import(temp);
Child = screenStackContainer = new ScreenStackCacheContainer { RelativeSizeAxes = Axes.Both };
screenStackContainer.ScreenStack.Push(songSelect = new DummySongSelect());
});
}
/// <summary>
/// Check if <see cref="PlayerLoader"/> properly triggers background dim previews when a user hovers over the visual settings panel.
/// </summary>
[Test]
public void PlayerLoaderSettingsHoverTest()
{
setupUserSettings();
AddStep("Start player loader", () => songSelect.Push(playerLoader = new DimAccessiblePlayerLoader(player = new DimAccessiblePlayer())));
AddUntilStep(() => playerLoader?.IsLoaded ?? false, "Wait for Player Loader to load");
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
AddStep("Trigger background preview", () =>
{
InputManager.MoveMouseTo(playerLoader.ScreenPos);
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
});
waitForDim();
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
waitForDim();
AddAssert("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
}
/// <summary>
/// In the case of a user triggering the dim preview the instant player gets loaded, then moving the cursor off of the visual settings:
/// The OnHover of PlayerLoader will trigger, which could potentially trigger an undim unless checked for in PlayerLoader.
/// We need to check that in this scenario, the dim is still properly applied after entering player.
/// </summary>
[Test]
public void PlayerLoaderTransitionTest()
{
performFullSetup();
AddStep("Trigger hover event", () => playerLoader.TriggerOnHover());
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
waitForDim();
AddAssert("Screen is dimmed and unblurred", () => songSelect.IsBackgroundDimmed() && songSelect.IsBackgroundUnblurred());
}
/// <summary>
/// Make sure the background is fully invisible (Alpha == 0) when the background should be disabled by the storyboard.
/// </summary>
[Test]
public void StoryboardBackgroundVisibilityTest()
{
performFullSetup();
createFakeStoryboard();
AddStep("Storyboard Enabled", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
waitForDim();
AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible());
AddStep("Storyboard Disabled", () =>
{
player.ReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false;
});
waitForDim();
AddAssert("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && player.IsStoryboardInvisible());
}
/// <summary>
/// When exiting player, the screen that it suspends/exits to needs to have a fully visible (Alpha == 1) background.
/// </summary>
[Test]
public void StoryboardTransitionTest()
{
performFullSetup();
createFakeStoryboard();
AddStep("Exit to song select", () => player.Exit());
waitForDim();
AddAssert("Background is visible", () => songSelect.IsBackgroundVisible());
}
/// <summary>
/// Check if the <see cref="UserDimContainer"/> is properly accepting user dim changes at all.
/// </summary>
[Test]
public void DisableUserDimTest()
{
performFullSetup();
waitForDim();
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
AddStep("EnableUserDim disabled", () => songSelect.DimEnabled.Value = false);
waitForDim();
AddAssert("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
AddStep("EnableUserDim enabled", () => songSelect.DimEnabled.Value = true);
waitForDim();
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
}
/// <summary>
/// Check if the fade container retains dim when pausing
/// </summary>
[Test]
public void PauseTest()
{
performFullSetup(true);
AddStep("Pause", () => player.CurrentPausableGameplayContainer.Pause());
waitForDim();
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
AddStep("Unpause", () => player.CurrentPausableGameplayContainer.Resume());
waitForDim();
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
}
/// <summary>
/// Check if the fade container removes user dim when suspending <see cref="Player"/> for <see cref="SoloResults"/>
/// </summary>
[Test]
public void TransitionTest()
{
performFullSetup();
AddStep("Transition to Results", () => player.Push(new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } })));
waitForDim();
AddAssert("Screen is undimmed and is original background", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent());
}
/// <summary>
/// Check if background gets undimmed when leaving <see cref="Player"/> for <see cref="PlaySongSelect"/>
/// </summary>
[Test]
public void TransitionOutTest()
{
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
waitForDim();
AddAssert("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
}
private void waitForDim() => AddWaitStep(5, "Wait for dim");
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
{
player.StoryboardEnabled.Value = false;
player.ReplacesBackground.Value = false;
player.CurrentStoryboardContainer.Add(new SpriteText
{
Size = new Vector2(250, 50),
Alpha = 1,
Colour = Color4.Tomato,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "THIS IS A STORYBOARD",
});
});
private void performFullSetup(bool allowPause = false)
{
setupUserSettings();
AddStep("Start player loader", () =>
{
songSelect.Push(playerLoader = new DimAccessiblePlayerLoader(player = new DimAccessiblePlayer
{
AllowPause = allowPause,
Ready = true,
}));
});
AddUntilStep(() => playerLoader.IsLoaded, "Wait for Player Loader to load");
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
AddUntilStep(() => player.IsLoaded, "Wait for player to load");
}
private void setupUserSettings()
{
AddUntilStep(() => songSelect.Carousel.SelectedBeatmap != null, "Song select has selection");
AddStep("Set default user settings", () =>
{
Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { new OsuModNoFail() });
songSelect.DimLevel.Value = 0.7f;
songSelect.BlurLevel.Value = 0.0f;
});
}
private class DummySongSelect : PlaySongSelect
{
protected override BackgroundScreen CreateBackground()
{
FadeAccessibleBackground background = new FadeAccessibleBackground(Beatmap.Value);
DimEnabled.BindTo(background.EnableUserDim);
return background;
}
public readonly Bindable<bool> DimEnabled = new Bindable<bool>();
public readonly Bindable<double> DimLevel = new Bindable<double>();
public readonly Bindable<double> BlurLevel = new Bindable<double>();
public new BeatmapCarousel Carousel => base.Carousel;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.DimLevel, DimLevel);
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
}
public bool IsBackgroundDimmed() => ((FadeAccessibleBackground)Background).CurrentColour == OsuColour.Gray(1 - (float)DimLevel.Value);
public bool IsBackgroundUnblurred() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(0);
public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White;
public bool IsBackgroundInvisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 0;
public bool IsBackgroundVisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 1;
/// <summary>
/// Make sure every time a screen gets pushed, the background doesn't get replaced
/// </summary>
/// <returns>Whether or not the original background (The one created in DummySongSelect) is still the current background</returns>
public bool IsBackgroundCurrent() => ((FadeAccessibleBackground)Background).IsCurrentScreen();
}
private class FadeAccessibleResults : SoloResults
{
public FadeAccessibleResults(ScoreInfo score)
: base(score)
{
}
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
}
private class DimAccessiblePlayer : Player
{
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
protected override UserDimContainer CreateStoryboardContainer()
{
return new TestUserDimContainer(true)
{
RelativeSizeAxes = Axes.Both,
Alpha = 1,
EnableUserDim = { Value = true }
};
}
public PausableGameplayContainer CurrentPausableGameplayContainer => PausableGameplayContainer;
public UserDimContainer CurrentStoryboardContainer => StoryboardContainer;
// Whether or not the player should be allowed to load.
public bool Ready;
public Bindable<bool> StoryboardEnabled;
public readonly Bindable<bool> ReplacesBackground = new Bindable<bool>();
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
public bool IsStoryboardVisible() => ((TestUserDimContainer)CurrentStoryboardContainer).CurrentAlpha == 1;
public bool IsStoryboardInvisible() => ((TestUserDimContainer)CurrentStoryboardContainer).CurrentAlpha <= 1;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
while (!Ready)
Thread.Sleep(1);
StoryboardEnabled = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
ReplacesBackground.BindTo(Background.StoryboardReplacesBackground);
RulesetContainer.IsPaused.BindTo(IsPaused);
}
}
private class ScreenStackCacheContainer : Container
{
[Cached]
private BackgroundScreenStack backgroundScreenStack;
public readonly ScreenStack ScreenStack;
public ScreenStackCacheContainer()
{
Add(backgroundScreenStack = new BackgroundScreenStack { RelativeSizeAxes = Axes.Both });
Add(ScreenStack = new ScreenStack { RelativeSizeAxes = Axes.Both });
}
}
private class DimAccessiblePlayerLoader : PlayerLoader
{
public VisualSettings VisualSettingsPos => VisualSettings;
public BackgroundScreen ScreenPos => Background;
public DimAccessiblePlayerLoader(Player player)
: base(() => player)
{
}
public void TriggerOnHover() => OnHover(new HoverEvent(new InputState()));
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
}
private class FadeAccessibleBackground : BackgroundScreenBeatmap
{
protected override UserDimContainer CreateFadeContainer() => fadeContainer = new TestUserDimContainer { RelativeSizeAxes = Axes.Both };
public Color4 CurrentColour => fadeContainer.CurrentColour;
public float CurrentAlpha => fadeContainer.CurrentAlpha;
public Vector2 CurrentBlur => Background.BlurSigma;
private TestUserDimContainer fadeContainer;
public FadeAccessibleBackground(WorkingBeatmap beatmap)
: base(beatmap)
{
}
}
private class TestUserDimContainer : UserDimContainer
{
public Color4 CurrentColour => DimContainer.Colour;
public float CurrentAlpha => DimContainer.Alpha;
public TestUserDimContainer(bool isStoryboard = false)
: base(isStoryboard)
{
}
}
}
}

View File

@ -23,9 +23,6 @@ namespace osu.Game.Tests.Visual
[System.ComponentModel.Description("in BeatmapOverlay")] [System.ComponentModel.Description("in BeatmapOverlay")]
public class TestCaseBeatmapScoresContainer : OsuTestCase public class TestCaseBeatmapScoresContainer : OsuTestCase
{ {
private readonly IEnumerable<APIScoreInfo> scores;
private readonly IEnumerable<APIScoreInfo> anotherScores;
private readonly APIScoreInfo topScoreInfo;
private readonly Box background; private readonly Box background;
public TestCaseBeatmapScoresContainer() public TestCaseBeatmapScoresContainer()
@ -47,15 +44,7 @@ namespace osu.Game.Tests.Visual
} }
}; };
AddStep("scores pack 1", () => scoresContainer.Scores = scores); IEnumerable<APIScoreInfo> scores = new[]
AddStep("scores pack 2", () => scoresContainer.Scores = anotherScores);
AddStep("only top score", () => scoresContainer.Scores = new[] { topScoreInfo });
AddStep("remove scores", () => scoresContainer.Scores = null);
AddStep("resize to big", () => container.ResizeWidthTo(1, 300));
AddStep("resize to normal", () => container.ResizeWidthTo(0.8f, 300));
AddStep("online scores", () => scoresContainer.Beatmap = new BeatmapInfo { OnlineBeatmapID = 75, Ruleset = new OsuRuleset().RulesetInfo });
scores = new[]
{ {
new APIScoreInfo new APIScoreInfo
{ {
@ -168,7 +157,7 @@ namespace osu.Game.Tests.Visual
s.Statistics.Add(HitResult.Meh, RNG.Next(2000)); s.Statistics.Add(HitResult.Meh, RNG.Next(2000));
} }
anotherScores = new[] IEnumerable<APIScoreInfo> anotherScores = new[]
{ {
new APIScoreInfo new APIScoreInfo
{ {
@ -280,7 +269,7 @@ namespace osu.Game.Tests.Visual
s.Statistics.Add(HitResult.Meh, RNG.Next(2000)); s.Statistics.Add(HitResult.Meh, RNG.Next(2000));
} }
topScoreInfo = new APIScoreInfo var topScoreInfo = new APIScoreInfo
{ {
User = new User User = new User
{ {
@ -305,6 +294,14 @@ namespace osu.Game.Tests.Visual
topScoreInfo.Statistics.Add(HitResult.Great, RNG.Next(2000)); topScoreInfo.Statistics.Add(HitResult.Great, RNG.Next(2000));
topScoreInfo.Statistics.Add(HitResult.Good, RNG.Next(2000)); topScoreInfo.Statistics.Add(HitResult.Good, RNG.Next(2000));
topScoreInfo.Statistics.Add(HitResult.Meh, RNG.Next(2000)); topScoreInfo.Statistics.Add(HitResult.Meh, RNG.Next(2000));
AddStep("scores pack 1", () => scoresContainer.Scores = scores);
AddStep("scores pack 2", () => scoresContainer.Scores = anotherScores);
AddStep("only top score", () => scoresContainer.Scores = new[] { topScoreInfo });
AddStep("remove scores", () => scoresContainer.Scores = null);
AddStep("resize to big", () => container.ResizeWidthTo(1, 300));
AddStep("resize to normal", () => container.ResizeWidthTo(0.8f, 300));
AddStep("online scores", () => scoresContainer.Beatmap = new BeatmapInfo { OnlineBeatmapID = 75, Ruleset = new OsuRuleset().RulesetInfo });
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual
{
public class TestCaseDirectPanel : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(DirectGridPanel),
typeof(DirectListPanel),
typeof(IconPill)
};
[BackgroundDependencyLoader]
private void load()
{
var beatmap = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true;
beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true;
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(20),
Spacing = new Vector2(0, 20),
Children = new Drawable[]
{
new DirectGridPanel(beatmap.BeatmapSetInfo),
new DirectListPanel(beatmap.BeatmapSetInfo)
}
};
}
}
}

View File

@ -17,15 +17,15 @@ namespace osu.Game.Tests.Visual
[Description("player pause/fail screens")] [Description("player pause/fail screens")]
public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseContainer) }; public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(FailOverlay), typeof(PausableGameplayContainer) };
private FailOverlay failOverlay; private FailOverlay failOverlay;
private PauseContainer.PauseOverlay pauseOverlay; private PausableGameplayContainer.PauseOverlay pauseOverlay;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Add(pauseOverlay = new PauseContainer.PauseOverlay Add(pauseOverlay = new PausableGameplayContainer.PauseOverlay
{ {
OnResume = () => Logger.Log(@"Resume"), OnResume = () => Logger.Log(@"Resume"),
OnRetry = () => Logger.Log(@"Retry"), OnRetry = () => Logger.Log(@"Retry"),

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
@ -13,15 +14,22 @@ namespace osu.Game.Tests.Visual
public class TestCasePlayerLoader : ManualInputManagerTestCase public class TestCasePlayerLoader : ManualInputManagerTestCase
{ {
private PlayerLoader loader; private PlayerLoader loader;
private ScreenStack stack; private readonly ScreenStack stack;
[Cached]
private BackgroundScreenStack backgroundStack;
public TestCasePlayerLoader()
{
InputManager.Add(backgroundStack = new BackgroundScreenStack { RelativeSizeAxes = Axes.Both });
InputManager.Add(stack = new ScreenStack { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGameBase game) private void load(OsuGameBase game)
{ {
Beatmap.Value = new DummyWorkingBeatmap(game); Beatmap.Value = new DummyWorkingBeatmap(game);
InputManager.Add(stack = new ScreenStack { RelativeSizeAxes = Axes.Both });
AddStep("load dummy beatmap", () => stack.Push(loader = new PlayerLoader(() => new Player AddStep("load dummy beatmap", () => stack.Push(loader = new PlayerLoader(() => new Player
{ {
AllowPause = false, AllowPause = false,

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -19,14 +20,20 @@ namespace osu.Game.Tests.Visual
private readonly StopwatchClock clock; private readonly StopwatchClock clock;
[Cached]
private readonly GameplayClock gameplayClock;
private readonly FramedClock framedClock;
public TestCaseSongProgress() public TestCaseSongProgress()
{ {
clock = new StopwatchClock(true); clock = new StopwatchClock(true);
gameplayClock = new GameplayClock(framedClock = new FramedClock(clock));
Add(progress = new SongProgress Add(progress = new SongProgress
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AudioClock = new StopwatchClock(true),
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
}); });
@ -68,8 +75,13 @@ namespace osu.Game.Tests.Visual
progress.Objects = objects; progress.Objects = objects;
graph.Objects = objects; graph.Objects = objects;
progress.AudioClock = clock; progress.RequestSeek = pos => clock.Seek(pos);
progress.OnSeek = pos => clock.Seek(pos); }
protected override void Update()
{
base.Update();
framedClock.ProcessFrame();
} }
private class TestSongProgressGraph : SongProgressGraph private class TestSongProgressGraph : SongProgressGraph

View File

@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -50,11 +49,6 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public event Action<DownloadBeatmapSetRequest> BeatmapDownloadFailed; public event Action<DownloadBeatmapSetRequest> BeatmapDownloadFailed;
/// <summary>
/// Fired when a beatmap load is requested (into the interactive game UI).
/// </summary>
public Action<BeatmapSetInfo> PresentBeatmap;
/// <summary> /// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available. /// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary> /// </summary>
@ -151,8 +145,7 @@ namespace osu.Game.Beatmaps
var downloadNotification = new DownloadNotification var downloadNotification = new DownloadNotification
{ {
CompletionText = $"Imported {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}!", Text = $"Downloading {beatmapSetInfo}",
Text = $"Downloading {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}",
}; };
var request = new DownloadBeatmapSetRequest(beatmapSetInfo, noVideo); var request = new DownloadBeatmapSetRequest(beatmapSetInfo, noVideo);
@ -165,20 +158,10 @@ namespace osu.Game.Beatmaps
request.Success += filename => request.Success += filename =>
{ {
downloadNotification.Text = $"Importing {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}";
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
// This gets scheduled back to the update thread, but we want the import to run in the background. // This gets scheduled back to the update thread, but we want the import to run in the background.
var importedBeatmap = Import(filename); Import(downloadNotification, filename);
downloadNotification.CompletionClickAction = () =>
{
PresentCompletedImport(importedBeatmap.Yield());
return true;
};
downloadNotification.State = ProgressNotificationState.Completed;
currentDownloads.Remove(request); currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
}; };
@ -221,12 +204,6 @@ namespace osu.Game.Beatmaps
return true; return true;
} }
protected override void PresentCompletedImport(IEnumerable<BeatmapSetInfo> imported)
{
base.PresentCompletedImport(imported);
PresentBeatmap?.Invoke(imported.LastOrDefault());
}
/// <summary> /// <summary>
/// Get an existing download request if it exists. /// Get an existing download request if it exists.
/// </summary> /// </summary>

View File

@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing
/// </summary> /// </summary>
public class BeatmapStore : MutableDatabaseBackedStore<BeatmapSetInfo> public class BeatmapStore : MutableDatabaseBackedStoreWithFileIncludes<BeatmapSetInfo, BeatmapSetFileInfo>
{ {
public event Action<BeatmapInfo> BeatmapHidden; public event Action<BeatmapInfo> BeatmapHidden;
public event Action<BeatmapInfo> BeatmapRestored; public event Action<BeatmapInfo> BeatmapRestored;
@ -64,18 +64,17 @@ namespace osu.Game.Beatmaps
protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query) => protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query) =>
base.AddIncludesForDeletion(query) base.AddIncludesForDeletion(query)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Metadata) .Include(s => s.Metadata)
.Include(s => s.Beatmaps).ThenInclude(b => b.Scores); .Include(s => s.Beatmaps).ThenInclude(b => b.Scores)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata);
protected override IQueryable<BeatmapSetInfo> AddIncludesForConsumption(IQueryable<BeatmapSetInfo> query) => protected override IQueryable<BeatmapSetInfo> AddIncludesForConsumption(IQueryable<BeatmapSetInfo> query) =>
base.AddIncludesForConsumption(query) base.AddIncludesForConsumption(query)
.Include(s => s.Metadata) .Include(s => s.Metadata)
.Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata);
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context) protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context)
{ {

View File

@ -48,11 +48,11 @@ namespace osu.Game.Beatmaps.Drawables
var rating = beatmap.StarDifficulty; var rating = beatmap.StarDifficulty;
if (rating < 1.5) return DifficultyRating.Easy; if (rating < 2.0) return DifficultyRating.Easy;
if (rating < 2.25) return DifficultyRating.Normal; if (rating < 2.7) return DifficultyRating.Normal;
if (rating < 3.75) return DifficultyRating.Hard; if (rating < 4.0) return DifficultyRating.Hard;
if (rating < 5.25) return DifficultyRating.Insane; if (rating < 5.3) return DifficultyRating.Insane;
if (rating < 6.75) return DifficultyRating.Expert; if (rating < 6.5) return DifficultyRating.Expert;
return DifficultyRating.ExpertPlus; return DifficultyRating.ExpertPlus;
} }

View File

@ -18,9 +18,12 @@ namespace osu.Game.Beatmaps.Drawables
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
public UpdateableBeatmapBackgroundSprite() private readonly BeatmapSetCoverType beatmapSetCoverType;
public UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
{ {
Beatmap.BindValueChanged(b => Model = b.NewValue); Beatmap.BindValueChanged(b => Model = b.NewValue);
this.beatmapSetCoverType = beatmapSetCoverType;
} }
protected override Drawable CreateDrawable(BeatmapInfo model) protected override Drawable CreateDrawable(BeatmapInfo model)
@ -31,10 +34,18 @@ namespace osu.Game.Beatmaps.Drawables
var localBeatmap = beatmaps.GetWorkingBeatmap(model); var localBeatmap = beatmaps.GetWorkingBeatmap(model);
if (localBeatmap.BeatmapInfo.ID == 0 && model?.BeatmapSet?.OnlineInfo != null) if (model?.BeatmapSet?.OnlineInfo != null)
drawable = new BeatmapSetCover(model.BeatmapSet); drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
else else if (localBeatmap.BeatmapInfo.ID != 0)
{
// Fall back to local background if one exists
drawable = new BeatmapBackgroundSprite(localBeatmap); drawable = new BeatmapBackgroundSprite(localBeatmap);
}
else
{
// Use the default background if somehow an online set does not exist and we don't have a local copy.
drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
}
drawable.RelativeSizeAxes = Axes.Both; drawable.RelativeSizeAxes = Axes.Both;
drawable.Anchor = Anchor.Centre; drawable.Anchor = Anchor.Centre;

View File

@ -72,9 +72,6 @@ namespace osu.Game.Configuration
Set(OsuSetting.MenuParallax, true); Set(OsuSetting.MenuParallax, true);
Set(OsuSetting.SnakingInSliders, true);
Set(OsuSetting.SnakingOutSliders, true);
// Gameplay // Gameplay
Set(OsuSetting.DimLevel, 0.3, 0, 1, 0.01); Set(OsuSetting.DimLevel, 0.3, 0, 1, 0.01);
Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01); Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
@ -150,8 +147,6 @@ namespace osu.Game.Configuration
DisplayStarsMinimum, DisplayStarsMinimum,
DisplayStarsMaximum, DisplayStarsMaximum,
RandomSelectAlgorithm, RandomSelectAlgorithm,
SnakingInSliders,
SnakingOutSliders,
ShowFpsDisplay, ShowFpsDisplay,
ChatDisplayHeight, ChatDisplayHeight,
Version, Version,

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.IO.File; using osu.Framework.IO.File;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -32,7 +33,7 @@ namespace osu.Game.Database
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : INamedFileInfo, new() where TFileModel : INamedFileInfo, new()
{ {
public delegate void ItemAddedDelegate(TModel model, bool existing, bool silent); public delegate void ItemAddedDelegate(TModel model, bool existing);
/// <summary> /// <summary>
/// Set an endpoint for notifications to be posted to. /// Set an endpoint for notifications to be posted to.
@ -53,6 +54,8 @@ namespace osu.Game.Database
public virtual string[] HandledExtensions => new[] { ".zip" }; public virtual string[] HandledExtensions => new[] { ".zip" };
public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
protected readonly FileStore Files; protected readonly FileStore Files;
protected readonly IDatabaseContextFactory ContextFactory; protected readonly IDatabaseContextFactory ContextFactory;
@ -105,12 +108,12 @@ namespace osu.Game.Database
a.Invoke(); a.Invoke();
} }
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore<TModel> modelStore, IIpcHost importHost = null) protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
{ {
ContextFactory = contextFactory; ContextFactory = contextFactory;
ModelStore = modelStore; ModelStore = modelStore;
ModelStore.ItemAdded += (item, silent) => handleEvent(() => ItemAdded?.Invoke(item, false, silent)); ModelStore.ItemAdded += item => handleEvent(() => ItemAdded?.Invoke(item, false));
ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s)); ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s));
Files = new FileStore(contextFactory, storage); Files = new FileStore(contextFactory, storage);
@ -128,14 +131,18 @@ namespace osu.Game.Database
/// <param name="paths">One or more archive locations on disk.</param> /// <param name="paths">One or more archive locations on disk.</param>
public void Import(params string[] paths) public void Import(params string[] paths)
{ {
var notification = new ProgressNotification var notification = new ProgressNotification { State = ProgressNotificationState.Active };
{
Text = "Import is initialising...",
Progress = 0,
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
Import(notification, paths);
}
protected void Import(ProgressNotification notification, params string[] paths)
{
notification.Progress = 0;
notification.Text = "Import is initialising...";
var term = $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
List<TModel> imported = new List<TModel>(); List<TModel> imported = new List<TModel>();
@ -148,7 +155,18 @@ namespace osu.Game.Database
try try
{ {
notification.Text = $"Importing ({++current} of {paths.Length})\n{Path.GetFileName(path)}"; var text = "Importing ";
if (path.Length > 1)
text += $"{++current} of {paths.Length} {term}s..";
else
text += $"{term}..";
// only show the filename if it isn't a temporary one (as those look ugly).
if (!path.Contains(Path.GetTempPath()))
text += $"\n{Path.GetFileName(path)}";
notification.Text = text;
imported.Add(Import(path)); imported.Add(Import(path));
@ -168,13 +186,20 @@ namespace osu.Game.Database
} }
else else
{ {
notification.CompletionText = $"Imported {current} {typeof(TModel).Name.Replace("Info", "").ToLower()}s!"; notification.CompletionText = imported.Count == 1
notification.CompletionClickAction += () => ? $"Imported {imported.First()}!"
: $"Imported {current} {term}s!";
if (imported.Count > 0 && PresentImport != null)
{ {
if (imported.Count > 0) notification.CompletionText += " Click to view.";
PresentCompletedImport(imported); notification.CompletionClickAction = () =>
return true; {
}; PresentImport?.Invoke(imported);
return true;
};
}
notification.State = ProgressNotificationState.Completed; notification.State = ProgressNotificationState.Completed;
} }
} }
@ -207,9 +232,10 @@ namespace osu.Game.Database
return import; return import;
} }
protected virtual void PresentCompletedImport(IEnumerable<TModel> imported) /// <summary>
{ /// Fired when the user requests to view the resulting import.
} /// </summary>
public Action<IEnumerable<TModel>> PresentImport;
/// <summary> /// <summary>
/// Import an item from an <see cref="ArchiveReader"/>. /// Import an item from an <see cref="ArchiveReader"/>.
@ -225,7 +251,7 @@ namespace osu.Game.Database
model.Hash = computeHash(archive); model.Hash = computeHash(archive);
return Import(model, false, archive); return Import(model, archive);
} }
catch (Exception e) catch (Exception e)
{ {
@ -259,9 +285,8 @@ namespace osu.Game.Database
/// Import an item from a <see cref="TModel"/>. /// Import an item from a <see cref="TModel"/>.
/// </summary> /// </summary>
/// <param name="item">The model to be imported.</param> /// <param name="item">The model to be imported.</param>
/// <param name="silent">Whether the user should be notified fo the import.</param>
/// <param name="archive">An optional archive to use for model population.</param> /// <param name="archive">An optional archive to use for model population.</param>
public TModel Import(TModel item, bool silent = false, ArchiveReader archive = null) public TModel Import(TModel item, ArchiveReader archive = null)
{ {
delayEvents(); delayEvents();
@ -281,7 +306,7 @@ namespace osu.Game.Database
{ {
Undelete(existing); Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database); Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
handleEvent(() => ItemAdded?.Invoke(existing, true, silent)); handleEvent(() => ItemAdded?.Invoke(existing, true));
return existing; return existing;
} }
@ -291,7 +316,7 @@ namespace osu.Game.Database
Populate(item, archive); Populate(item, archive);
// import to store // import to store
ModelStore.Add(item, silent); ModelStore.Add(item);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -16,9 +16,7 @@ namespace osu.Game.Database
public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore
where T : class, IHasPrimaryKey, ISoftDelete where T : class, IHasPrimaryKey, ISoftDelete
{ {
public delegate void ItemAddedDelegate(T model, bool silent); public event Action<T> ItemAdded;
public event ItemAddedDelegate ItemAdded;
public event Action<T> ItemRemoved; public event Action<T> ItemRemoved;
protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
@ -35,8 +33,7 @@ namespace osu.Game.Database
/// Add a <see cref="T"/> to the database. /// Add a <see cref="T"/> to the database.
/// </summary> /// </summary>
/// <param name="item">The item to add.</param> /// <param name="item">The item to add.</param>
/// <param name="silent">Whether the user should be notified of the addition.</param> public void Add(T item)
public void Add(T item, bool silent)
{ {
using (var usage = ContextFactory.GetForWrite()) using (var usage = ContextFactory.GetForWrite())
{ {
@ -44,7 +41,7 @@ namespace osu.Game.Database
context.Attach(item); context.Attach(item);
} }
ItemAdded?.Invoke(item, silent); ItemAdded?.Invoke(item);
} }
/// <summary> /// <summary>
@ -57,7 +54,7 @@ namespace osu.Game.Database
usage.Context.Update(item); usage.Context.Update(item);
ItemRemoved?.Invoke(item); ItemRemoved?.Invoke(item);
ItemAdded?.Invoke(item, true); ItemAdded?.Invoke(item);
} }
/// <summary> /// <summary>
@ -94,7 +91,7 @@ namespace osu.Game.Database
item.DeletePending = false; item.DeletePending = false;
} }
ItemAdded?.Invoke(item, true); ItemAdded?.Invoke(item);
return true; return true;
} }

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Platform;
namespace osu.Game.Database
{
public abstract class MutableDatabaseBackedStoreWithFileIncludes<T, U> : MutableDatabaseBackedStore<T>
where T : class, IHasPrimaryKey, ISoftDelete, IHasFiles<U>
where U : INamedFileInfo
{
protected MutableDatabaseBackedStoreWithFileIncludes(IDatabaseContextFactory contextFactory, Storage storage = null)
: base(contextFactory, storage)
{
}
protected override IQueryable<T> AddIncludesForConsumption(IQueryable<T> query) =>
base.AddIncludesForConsumption(query)
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
protected override IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) =>
base.AddIncludesForDeletion(query)
.Include(s => s.Files); // don't include FileInfo. these are handled by the FileStore itself.
}
}

View File

@ -24,7 +24,17 @@ namespace osu.Game.Graphics.Containers
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
private PreviewTrackManager previewTrackManager; /// <summary>
/// Temporary to allow for overlays in the main screen content to not dim theirselves.
/// Should be eventually replaced by dimming which is aware of the target dim container (traverse parent for certain interface type?).
/// </summary>
protected virtual bool DimMainContent => true;
[Resolved(CanBeNull = true)]
private OsuGame osuGame { get; set; }
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
protected readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); protected readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
@ -36,10 +46,8 @@ namespace osu.Game.Graphics.Containers
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame, AudioManager audio, PreviewTrackManager previewTrackManager) private void load(AudioManager audio)
{ {
this.previewTrackManager = previewTrackManager;
if (osuGame != null) if (osuGame != null)
OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); OverlayActivationMode.BindTo(osuGame.OverlayActivationMode);
@ -93,6 +101,7 @@ namespace osu.Game.Graphics.Containers
if (OverlayActivationMode.Value != OverlayActivation.Disabled) if (OverlayActivationMode.Value != OverlayActivation.Disabled)
{ {
if (PlaySamplesOnStateChange) samplePopIn?.Play(); if (PlaySamplesOnStateChange) samplePopIn?.Play();
if (BlockScreenWideMouse && DimMainContent) osuGame?.AddBlockingOverlay(this);
} }
else else
State = Visibility.Hidden; State = Visibility.Hidden;
@ -100,6 +109,7 @@ namespace osu.Game.Graphics.Containers
break; break;
case Visibility.Hidden: case Visibility.Hidden:
if (PlaySamplesOnStateChange) samplePopOut?.Play(); if (PlaySamplesOnStateChange) samplePopOut?.Play();
if (BlockScreenWideMouse) osuGame?.RemoveBlockingOverlay(this);
break; break;
} }
} }
@ -109,5 +119,11 @@ namespace osu.Game.Graphics.Containers
base.PopOut(); base.PopOut();
previewTrackManager.StopAnyPlaying(this); previewTrackManager.StopAnyPlaying(this);
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
osuGame?.RemoveBlockingOverlay(this);
}
} }
} }

View File

@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osuTK.Graphics;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// A container that applies user-configured dim levels to its contents.
/// This container specifies behavior that applies to both Storyboards and Backgrounds.
/// </summary>
public class UserDimContainer : Container
{
private const float background_fade_duration = 800;
private Bindable<double> dimLevel { get; set; }
private Bindable<bool> showStoryboard { get; set; }
/// <summary>
/// Whether or not user-configured dim levels should be applied to the container.
/// </summary>
public readonly Bindable<bool> EnableUserDim = new Bindable<bool>();
/// <summary>
/// Whether or not the storyboard loaded should completely hide the background behind it.
/// </summary>
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
protected Container DimContainer { get; }
protected override Container<Drawable> Content => DimContainer;
private readonly bool isStoryboard;
/// <summary>
/// Creates a new <see cref="UserDimContainer"/>.
/// </summary>
/// <param name="isStoryboard"> Whether or not this instance of UserDimContainer contains a storyboard.
/// <remarks>
/// While both backgrounds and storyboards allow user dim levels to be applied, storyboards can be toggled via <see cref="showStoryboard"/>
/// and can cause backgrounds to become hidden via <see cref="StoryboardReplacesBackground"/>.
/// </remarks>
/// </param>
public UserDimContainer(bool isStoryboard = false)
{
this.isStoryboard = isStoryboard;
AddInternal(DimContainer = new Container { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
dimLevel = config.GetBindable<double>(OsuSetting.DimLevel);
showStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
EnableUserDim.ValueChanged += _ => updateBackgroundDim();
dimLevel.ValueChanged += _ => updateBackgroundDim();
showStoryboard.ValueChanged += _ => updateBackgroundDim();
StoryboardReplacesBackground.ValueChanged += _ => updateBackgroundDim();
}
protected override void LoadComplete()
{
base.LoadComplete();
updateBackgroundDim();
}
private void updateBackgroundDim()
{
if (isStoryboard)
{
DimContainer.FadeTo(!showStoryboard.Value || dimLevel.Value == 1 ? 0 : 1, background_fade_duration, Easing.OutQuint);
}
else
{
// The background needs to be hidden in the case of it being replaced by the storyboard
DimContainer.FadeTo(showStoryboard.Value && StoryboardReplacesBackground.Value ? 0 : 1, background_fade_duration, Easing.OutQuint);
}
DimContainer.FadeColour(EnableUserDim.Value ? OsuColour.Gray(1 - (float)dimLevel.Value) : Color4.White, background_fade_duration, Easing.OutQuint);
}
}
}

View File

@ -59,7 +59,6 @@ namespace osu.Game.Graphics.UserInterface
public abstract void Increment(T amount); public abstract void Increment(T amount);
public float TextSize public float TextSize
{ {
get => DisplayedCountSpriteText.Font.Size; get => DisplayedCountSpriteText.Font.Size;

View File

@ -0,0 +1,487 @@
// <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("20190225062029_AddUserIDColumn")]
partial class AddUserIDColumn
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028");
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<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<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<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<int>("IntKey")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
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<int>("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.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,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class AddUserIDColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "UserID",
table: "ScoreInfo",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "UserID",
table: "ScoreInfo");
}
}
}

View File

@ -338,6 +338,9 @@ namespace osu.Game.Migrations
b.Property<long>("TotalScore"); b.Property<long>("TotalScore");
b.Property<long?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString") b.Property<string>("UserString")
.HasColumnName("User"); .HasColumnName("User");

View File

@ -36,11 +36,12 @@ namespace osu.Game.Online.API.Requests
[Description("Ranked & Approved")] [Description("Ranked & Approved")]
RankedApproved = 0, RankedApproved = 0,
Approved = 1, Qualified = 3,
Loved = 8, Loved = 8,
Favourites = 2, Favourites = 2,
Qualified = 3,
Pending = 4, [Description("Pending & WIP")]
PendingWIP = 4,
Graveyard = 5, Graveyard = 5,
[Description("My Maps")] [Description("My Maps")]

View File

@ -213,12 +213,6 @@ namespace osu.Game.Online.Leaderboards
pendingUpdateScores?.Cancel(); pendingUpdateScores?.Cancel();
pendingUpdateScores = Schedule(() => pendingUpdateScores = Schedule(() =>
{ {
if (api?.IsLoggedIn != true)
{
PlaceholderState = PlaceholderState.NotLoggedIn;
return;
}
PlaceholderState = PlaceholderState.Retrieving; PlaceholderState = PlaceholderState.Retrieving;
loading.Show(); loading.Show();
@ -231,6 +225,12 @@ 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)
@ -243,6 +243,11 @@ namespace osu.Game.Online.Leaderboards
}); });
} }
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected abstract APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback); protected abstract APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback);
private Placeholder currentPlaceholder; private Placeholder currentPlaceholder;

View File

@ -124,6 +124,7 @@ namespace osu.Game.Online.Multiplayer
if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id)
Host.Value = other.Host.Value; Host.Value = other.Host.Value;
ChannelId.Value = other.ChannelId.Value;
Status.Value = other.Status.Value; Status.Value = other.Status.Value;
Availability.Value = other.Availability.Value; Availability.Value = other.Availability.Value;
Type.Value = other.Type.Value; Type.Value = other.Type.Value;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -109,6 +110,8 @@ namespace osu.Game
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>(); private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
private readonly List<OverlayContainer> visibleBlockingOverlays = new List<OverlayContainer>();
// todo: move this to SongSelect once Screen has the ability to unsuspend. // todo: move this to SongSelect once Screen has the ability to unsuspend.
[Cached] [Cached]
[Cached(Type = typeof(IBindable<IEnumerable<Mod>>))] [Cached(Type = typeof(IBindable<IEnumerable<Mod>>))]
@ -127,6 +130,22 @@ namespace osu.Game
public void ToggleDirect() => direct.ToggleVisibility(); public void ToggleDirect() => direct.ToggleVisibility();
private void updateBlockingOverlayFade() =>
screenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint);
public void AddBlockingOverlay(OverlayContainer overlay)
{
if (!visibleBlockingOverlays.Contains(overlay))
visibleBlockingOverlays.Add(overlay);
updateBlockingOverlayFade();
}
public void RemoveBlockingOverlay(OverlayContainer overlay)
{
visibleBlockingOverlays.Remove(overlay);
updateBlockingOverlayFade();
}
/// <summary> /// <summary>
/// Close all game-wide overlays. /// Close all game-wide overlays.
/// </summary> /// </summary>
@ -148,8 +167,6 @@ namespace osu.Game
{ {
this.frameworkConfig = frameworkConfig; this.frameworkConfig = frameworkConfig;
ScoreManager.ItemAdded += (score, _, silent) => Schedule(() => LoadScore(score, silent));
if (!Host.IsPrimaryInstance) if (!Host.IsPrimaryInstance)
{ {
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
@ -198,63 +215,12 @@ namespace osu.Game
externalLinkOpener.OpenUrlExternally(url); externalLinkOpener.OpenUrlExternally(url);
} }
private ScheduledDelegate scoreLoad;
/// <summary> /// <summary>
/// Show a beatmap set as an overlay. /// Show a beatmap set as an overlay.
/// </summary> /// </summary>
/// <param name="setId">The set to display.</param> /// <param name="setId">The set to display.</param>
public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId); public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId);
/// <summary>
/// Present a beatmap at song select.
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
public void PresentBeatmap(BeatmapSetInfo beatmap)
{
if (menuScreen == null)
{
Schedule(() => PresentBeatmap(beatmap));
return;
}
CloseAllOverlays(false);
void setBeatmap()
{
if (Beatmap.Disabled)
{
Schedule(setBeatmap);
return;
}
var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) : BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash);
if (databasedSet != null)
{
// Use first beatmap available for current ruleset, else switch ruleset.
var first = databasedSet.Beatmaps.Find(b => b.Ruleset == ruleset.Value) ?? databasedSet.Beatmaps.First();
ruleset.Value = first.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first);
}
}
switch (screenStack.CurrentScreen)
{
case SongSelect _:
break;
default:
// navigate to song select if we are not already there.
menuScreen.MakeCurrent();
menuScreen.LoadToSolo();
break;
}
setBeatmap();
}
/// <summary> /// <summary>
/// Show a user's profile as an overlay. /// Show a user's profile as an overlay.
/// </summary> /// </summary>
@ -267,19 +233,44 @@ namespace osu.Game
/// <param name="beatmapId">The beatmap to show.</param> /// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId); public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
protected void LoadScore(ScoreInfo score, bool silent) /// <summary>
/// Present a beatmap at song select immediately.
/// The user should have already requested this interactively.
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
public void PresentBeatmap(BeatmapSetInfo beatmap)
{ {
if (silent) var databasedSet = beatmap.OnlineBeatmapSetID != null
return; ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID)
: BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash);
scoreLoad?.Cancel(); if (databasedSet == null)
if (menuScreen == null)
{ {
scoreLoad = Schedule(() => LoadScore(score, false)); Logger.Log("The requested beatmap could not be loaded.", LoggingTarget.Information);
return; return;
} }
performFromMainMenu(() =>
{
// we might already be at song select, so a check is required before performing the load to solo.
if (menuScreen.IsCurrentScreen())
menuScreen.LoadToSolo();
// Use first beatmap available for current ruleset, else switch ruleset.
var first = databasedSet.Beatmaps.Find(b => b.Ruleset == ruleset.Value) ?? databasedSet.Beatmaps.First();
ruleset.Value = first.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first);
}, $"load {beatmap}", bypassScreenAllowChecks: true, targetScreen: typeof(PlaySongSelect));
}
/// <summary>
/// Present a score's replay immediately.
/// The user should have already requested this interactively.
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
public void PresentScore(ScoreInfo score)
{
var databasedScore = ScoreManager.GetScore(score); var databasedScore = ScoreManager.GetScore(score);
var databasedScoreInfo = databasedScore.ScoreInfo; var databasedScoreInfo = databasedScore.ScoreInfo;
if (databasedScore.Replay == null) if (databasedScore.Replay == null)
@ -295,14 +286,40 @@ namespace osu.Game
return; return;
} }
if ((screenStack.CurrentScreen as IOsuScreen)?.AllowExternalScreenChange == false) performFromMainMenu(() =>
{
ruleset.Value = databasedScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
Beatmap.Value.Mods.Value = databasedScoreInfo.Mods;
menuScreen.Push(new PlayerLoader(() => new ReplayPlayer(databasedScore)));
}, $"watch {databasedScoreInfo}", bypassScreenAllowChecks: true);
}
private ScheduledDelegate performFromMainMenuTask;
/// <summary>
/// Perform an action only after returning to the main menu.
/// Eagerly tries to exit the current screen until it succeeds.
/// </summary>
/// <param name="action">The action to perform once we are in the correct state.</param>
/// <param name="taskName">The task name to display in a notification (if we can't immediately reach the main menu state).</param>
/// <param name="targetScreen">An optional target screen type. If this screen is already current we can immediately perform the action without returning to the menu.</param>
/// <param name="bypassScreenAllowChecks">Whether checking <see cref="IOsuScreen.AllowExternalScreenChange"/> should be bypassed.</param>
private void performFromMainMenu(Action action, string taskName, Type targetScreen = null, bool bypassScreenAllowChecks = false)
{
performFromMainMenuTask?.Cancel();
// if the current screen does not allow screen changing, give the user an option to try again later.
if (!bypassScreenAllowChecks && (screenStack.CurrentScreen as IOsuScreen)?.AllowExternalScreenChange == false)
{ {
notifications.Post(new SimpleNotification notifications.Post(new SimpleNotification
{ {
Text = $"Click here to watch {databasedScoreInfo.User.Username} on {databasedScoreInfo.Beatmap}", Text = $"Click here to {taskName}",
Activated = () => Activated = () =>
{ {
loadScore(); performFromMainMenu(action, taskName, targetScreen, true);
return true; return true;
} }
}); });
@ -310,24 +327,26 @@ namespace osu.Game
return; return;
} }
loadScore(); CloseAllOverlays(false);
void loadScore() // we may already be at the target screen type.
if (targetScreen != null && screenStack.CurrentScreen?.GetType() == targetScreen)
{ {
if (!menuScreen.IsCurrentScreen() || Beatmap.Disabled) action();
{ return;
menuScreen.MakeCurrent();
this.Delay(500).Schedule(loadScore, out scoreLoad);
return;
}
ruleset.Value = databasedScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
Beatmap.Value.Mods.Value = databasedScoreInfo.Mods;
menuScreen.Push(new PlayerLoader(() => new ReplayPlayer(databasedScore)));
} }
// all conditions have been met to continue with the action.
if (menuScreen?.IsCurrentScreen() == true && !Beatmap.Disabled)
{
action();
return;
}
// menuScreen may not be initialised yet (null check required).
menuScreen?.MakeCurrent();
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@ -351,8 +370,10 @@ namespace osu.Game
BeatmapManager.PostNotification = n => notifications?.Post(n); BeatmapManager.PostNotification = n => notifications?.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall; BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
BeatmapManager.PresentBeatmap = PresentBeatmap; ScoreManager.PostNotification = n => notifications?.Post(n);
ScoreManager.PresentImport = items => PresentScore(items.First());
Container logoContainer; Container logoContainer;
@ -597,20 +618,10 @@ namespace osu.Game
} }
private Task asyncLoadStream; private Task asyncLoadStream;
private int visibleOverlayCount;
private void loadComponentSingleFile<T>(T d, Action<T> add) private void loadComponentSingleFile<T>(T d, Action<T> add)
where T : Drawable where T : Drawable
{ {
if (d is FocusedOverlayContainer focused)
{
focused.StateChanged += s =>
{
visibleOverlayCount += s == Visibility.Visible ? 1 : -1;
screenContainer.FadeColour(visibleOverlayCount > 0 ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint);
};
}
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
// with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile, // with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile,
// we could avoid the need for scheduling altogether. // we could avoid the need for scheduling altogether.
@ -628,10 +639,24 @@ namespace osu.Game
{ {
Logger.Log($"Loading {d}...", level: LogLevel.Debug); Logger.Log($"Loading {d}...", level: LogLevel.Debug);
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
Task task = null;
var del = new ScheduledDelegate(() => task = LoadComponentAsync(d, add));
Scheduler.Add(del);
// The delegate won't complete if OsuGame has been disposed in the meantime
while (!IsDisposed && !del.Completed)
await Task.Delay(10);
// Either we're disposed or the load process has started successfully
if (IsDisposed) if (IsDisposed)
return; return;
await LoadComponentAsync(d, add); Debug.Assert(task != null);
await task;
Logger.Log($"Loaded {d}!", level: LogLevel.Debug); Logger.Log($"Loaded {d}!", level: LogLevel.Debug);
} }
catch (OperationCanceledException) catch (OperationCanceledException)

View File

@ -75,9 +75,9 @@ namespace osu.Game
private Bindable<bool> fpsDisplayVisible; private Bindable<bool> fpsDisplayVisible;
protected AssemblyName AssemblyName => Assembly.GetEntryAssembly()?.GetName() ?? new AssemblyName { Version = new Version() }; public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
public bool IsDeployedBuild => AssemblyName.Version.Major > 0; public bool IsDeployedBuild => AssemblyVersion.Major > 0;
public string Version public string Version
{ {
@ -86,8 +86,8 @@ namespace osu.Game
if (!IsDeployedBuild) if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebug ? @"debug" : @"release"); return @"local " + (DebugUtils.IsDebug ? @"debug" : @"release");
var assembly = AssemblyName; var version = AssemblyVersion;
return $@"{assembly.Version.Major}.{assembly.Version.Minor}.{assembly.Version.Build}"; return $@"{version.Major}.{version.Minor}.{version.Build}";
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -21,30 +22,10 @@ namespace osu.Game.Overlays.BeatmapSet
private const float metadata_width = 225; private const float metadata_width = 225;
private const float spacing = 20; private const float spacing = 20;
private readonly MetadataSection source, tags;
private readonly Box successRateBackground; private readonly Box successRateBackground;
private readonly SuccessRate successRate; private readonly SuccessRate successRate;
private BeatmapSetInfo beatmapSet; public readonly Bindable<BeatmapSetInfo> BeatmapSet = new Bindable<BeatmapSetInfo>();
public BeatmapSetInfo BeatmapSet
{
get => beatmapSet;
set
{
if (value == beatmapSet) return;
beatmapSet = value;
updateDisplay();
}
}
private void updateDisplay()
{
source.Text = BeatmapSet?.Metadata.Source ?? string.Empty;
tags.Text = BeatmapSet?.Metadata.Tags ?? string.Empty;
}
public BeatmapInfo Beatmap public BeatmapInfo Beatmap
{ {
@ -54,6 +35,7 @@ namespace osu.Game.Overlays.BeatmapSet
public Info() public Info()
{ {
MetadataSection source, tags;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 220; Height = 220;
Masking = true; Masking = true;
@ -131,14 +113,18 @@ namespace osu.Game.Overlays.BeatmapSet
}, },
}, },
}; };
BeatmapSet.ValueChanged += b =>
{
source.Text = b.NewValue?.Metadata.Source ?? string.Empty;
tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty;
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
successRateBackground.Colour = colours.GrayE; successRateBackground.Colour = colours.GrayE;
updateDisplay();
} }
private class MetadataSection : FillFlowContainer private class MetadataSection : FillFlowContainer

View File

@ -3,6 +3,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -29,32 +30,20 @@ namespace osu.Game.Overlays
public const float RIGHT_WIDTH = 275; public const float RIGHT_WIDTH = 275;
private readonly Header header; private readonly Header header;
private readonly Info info;
private APIAccess api; private APIAccess api;
private RulesetStore rulesets; private RulesetStore rulesets;
private readonly ScrollContainer scroll; private readonly ScrollContainer scroll;
private BeatmapSetInfo beatmapSet; private readonly Bindable<BeatmapSetInfo> beatmapSet = new Bindable<BeatmapSetInfo>();
public BeatmapSetInfo BeatmapSet
{
get => beatmapSet;
set
{
if (value == beatmapSet)
return;
header.BeatmapSet.Value = info.BeatmapSet = beatmapSet = value;
}
}
// receive input outside our bounds so we can trigger a close event on ourselves. // receive input outside our bounds so we can trigger a close event on ourselves.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public BeatmapSetOverlay() public BeatmapSetOverlay()
{ {
Info info;
ScoresContainer scores; ScoresContainer scores;
Waves.FirstWaveColour = OsuColour.Gray(0.4f); Waves.FirstWaveColour = OsuColour.Gray(0.4f);
Waves.SecondWaveColour = OsuColour.Gray(0.3f); Waves.SecondWaveColour = OsuColour.Gray(0.3f);
@ -101,6 +90,9 @@ namespace osu.Game.Overlays
}, },
}; };
header.BeatmapSet.BindTo(beatmapSet);
info.BeatmapSet.BindTo(beatmapSet);
header.Picker.Beatmap.ValueChanged += b => header.Picker.Beatmap.ValueChanged += b =>
{ {
info.Beatmap = b.NewValue; info.Beatmap = b.NewValue;
@ -124,7 +116,7 @@ namespace osu.Game.Overlays
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut(); base.PopOut();
FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out).OnComplete(_ => BeatmapSet = null); FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out).OnComplete(_ => beatmapSet.Value = null);
} }
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
@ -135,11 +127,11 @@ namespace osu.Game.Overlays
public void FetchAndShowBeatmap(int beatmapId) public void FetchAndShowBeatmap(int beatmapId)
{ {
BeatmapSet = null; beatmapSet.Value = null;
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
req.Success += res => req.Success += res =>
{ {
BeatmapSet = res.ToBeatmapSet(rulesets); beatmapSet.Value = res.ToBeatmapSet(rulesets);
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);
@ -148,16 +140,16 @@ namespace osu.Game.Overlays
public void FetchAndShowBeatmapSet(int beatmapSetId) public void FetchAndShowBeatmapSet(int beatmapSetId)
{ {
BeatmapSet = null; beatmapSet.Value = null;
var req = new GetBeatmapSetRequest(beatmapSetId); var req = new GetBeatmapSetRequest(beatmapSetId);
req.Success += res => BeatmapSet = res.ToBeatmapSet(rulesets); req.Success += res => beatmapSet.Value = res.ToBeatmapSet(rulesets);
api.Queue(req); api.Queue(req);
Show(); Show();
} }
public void ShowBeatmapSet(BeatmapSetInfo set) public void ShowBeatmapSet(BeatmapSetInfo set)
{ {
BeatmapSet = set; beatmapSet.Value = set;
Show(); Show();
scroll.ScrollTo(0); scroll.ScrollTo(0);
} }

View File

@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Chat
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.Bold, fixedWidth: true) Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true)
}, },
new MessageSender(message.Sender) new MessageSender(message.Sender)
{ {

View File

@ -13,6 +13,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
namespace osu.Game.Overlays.Direct namespace osu.Game.Overlays.Direct
{ {
@ -23,6 +24,7 @@ namespace osu.Game.Overlays.Direct
private const float vertical_padding = 5; private const float vertical_padding = 5;
private const float height = 70; private const float height = 70;
private FillFlowContainer statusContainer;
private PlayButton playButton; private PlayButton playButton;
private Box progressBar; private Box progressBar;
@ -108,10 +110,24 @@ namespace osu.Game.Overlays.Direct
}, },
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.Both,
Height = 20, Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, Children = new Drawable[]
Children = GetDifficultyIcons(), {
statusContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Vertical = vertical_padding, Horizontal = 5 },
Spacing = new Vector2(5),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.X,
Height = 20,
Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding },
Children = GetDifficultyIcons(),
},
},
}, },
}, },
}, },
@ -194,6 +210,23 @@ namespace osu.Game.Overlays.Direct
Colour = colours.Yellow, Colour = colours.Yellow,
}, },
}); });
if (SetInfo.OnlineInfo?.HasVideo ?? false)
{
statusContainer.Add(new IconPill(FontAwesome.fa_film) { IconSize = new Vector2(20) });
}
if (SetInfo.OnlineInfo?.HasStoryboard ?? false)
{
statusContainer.Add(new IconPill(FontAwesome.fa_image) { IconSize = new Vector2(20) });
}
statusContainer.Add(new BeatmapSetOnlineStatusPill
{
TextSize = 12,
TextPadding = new MarginPadding { Horizontal = 10, Vertical = 4 },
Status = SetInfo.OnlineInfo?.Status ?? BeatmapSetOnlineStatus.None,
});
} }
} }
} }

View File

@ -118,7 +118,7 @@ namespace osu.Game.Overlays.Direct
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
private void setAdded(BeatmapSetInfo s, bool existing, bool silent) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable); private void setAdded(BeatmapSetInfo s, bool existing) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable);
private void setRemoved(BeatmapSetInfo s) => setDownloadStateFromManager(s, DownloadState.NotDownloaded); private void setRemoved(BeatmapSetInfo s) => setDownloadStateFromManager(s, DownloadState.NotDownloaded);

View File

@ -12,6 +12,14 @@ namespace osu.Game.Overlays.Direct
{ {
public class IconPill : CircularContainer public class IconPill : CircularContainer
{ {
public Vector2 IconSize
{
get => iconContainer.Size;
set => iconContainer.Size = value;
}
private readonly Container iconContainer;
public IconPill(FontAwesome icon) public IconPill(FontAwesome icon)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -25,16 +33,16 @@ namespace osu.Game.Overlays.Direct
Colour = Color4.Black, Colour = Color4.Black,
Alpha = 0.5f, Alpha = 0.5f,
}, },
new Container iconContainer = new Container
{ {
AutoSizeAxes = Axes.Both, Size = new Vector2(22),
Margin = new MarginPadding(5), Padding = new MarginPadding(5),
Child = new SpriteIcon Child = new SpriteIcon
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Icon = icon, Icon = icon,
Size = new Vector2(12),
}, },
}, },
}; };

View File

@ -38,6 +38,8 @@ namespace osu.Game.Overlays.Mods
protected override bool BlockNonPositionalInput => false; protected override bool BlockNonPositionalInput => false;
protected override bool DimMainContent => false;
protected readonly FillFlowContainer<ModSection> ModSectionsContainer; protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
protected readonly Bindable<IEnumerable<Mod>> SelectedMods = new Bindable<IEnumerable<Mod>>(new Mod[] { }); protected readonly Bindable<IEnumerable<Mod>> SelectedMods = new Bindable<IEnumerable<Mod>>(new Mod[] { });

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, IBindable<WorkingBeatmap> beatmap) private void load(BeatmapManager beatmaps, IBindable<WorkingBeatmap> beatmap)
{ {
beatmaps.GetAllUsableBeatmapSets().ForEach(b => addBeatmapSet(b, false, false)); beatmaps.GetAllUsableBeatmapSets().ForEach(b => addBeatmapSet(b, false));
beatmaps.ItemAdded += addBeatmapSet; beatmaps.ItemAdded += addBeatmapSet;
beatmaps.ItemRemoved += removeBeatmapSet; beatmaps.ItemRemoved += removeBeatmapSet;
@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Music
beatmapBacking.ValueChanged += _ => updateSelectedSet(); beatmapBacking.ValueChanged += _ => updateSelectedSet();
} }
private void addBeatmapSet(BeatmapSetInfo obj, bool existing, bool silent) => Schedule(() => private void addBeatmapSet(BeatmapSetInfo obj, bool existing) => Schedule(() =>
{ {
if (existing) if (existing)
return; return;

View File

@ -212,7 +212,7 @@ namespace osu.Game.Overlays
beatmapSets.Insert(index, beatmapSetInfo); beatmapSets.Insert(index, beatmapSetInfo);
} }
private void handleBeatmapAdded(BeatmapSetInfo obj, bool existing, bool silent) private void handleBeatmapAdded(BeatmapSetInfo obj, bool existing)
{ {
if (existing) if (existing)
return; return;

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -15,19 +15,20 @@ namespace osu.Game.Overlays.Settings.Sections.General
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(Storage storage, OsuConfigManager config) private void load(Storage storage, OsuConfigManager config)
{ {
Children = new Drawable[] Add(new SettingsEnumDropdown<ReleaseStream>
{ {
new SettingsEnumDropdown<ReleaseStream> LabelText = "Release stream",
{ Bindable = config.GetBindable<ReleaseStream>(OsuSetting.ReleaseStream),
LabelText = "Release stream", });
Bindable = config.GetBindable<ReleaseStream>(OsuSetting.ReleaseStream),
}, if (RuntimeInfo.IsDesktop)
new SettingsButton {
Add(new SettingsButton
{ {
Text = "Open osu! folder", Text = "Open osu! folder",
Action = storage.OpenInNativeExplorer, Action = storage.OpenInNativeExplorer,
} });
}; }
} }
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
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.Platform;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -26,10 +27,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private Bindable<ScalingMode> scalingMode; private Bindable<ScalingMode> scalingMode;
private Bindable<Size> sizeFullscreen; private Bindable<Size> sizeFullscreen;
private readonly BindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private OsuGameBase game; private OsuGameBase game;
private SettingsDropdown<Size> resolutionDropdown; private SettingsDropdown<Size> resolutionDropdown;
private SettingsEnumDropdown<WindowMode> windowModeDropdown; private SettingsDropdown<WindowMode> windowModeDropdown;
private Bindable<float> scalingPositionX; private Bindable<float> scalingPositionX;
private Bindable<float> scalingPositionY; private Bindable<float> scalingPositionY;
@ -39,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private const int transition_duration = 400; private const int transition_duration = 400;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, OsuGameBase game) private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, OsuGameBase game, GameHost host)
{ {
this.game = game; this.game = game;
@ -50,14 +52,18 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX); scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY); scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
if (host.Window != null)
windowModes.BindTo(host.Window.SupportedWindowModes);
Container resolutionSettingsContainer; Container resolutionSettingsContainer;
Children = new Drawable[] Children = new Drawable[]
{ {
windowModeDropdown = new SettingsEnumDropdown<WindowMode> windowModeDropdown = new SettingsDropdown<WindowMode>
{ {
LabelText = "Screen mode", LabelText = "Screen mode",
Bindable = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode), Bindable = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
ItemSource = windowModes,
}, },
resolutionSettingsContainer = new Container resolutionSettingsContainer = new Container
{ {
@ -150,6 +156,19 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything);
}, true); }, true);
windowModes.ItemsAdded += _ => windowModesChanged();
windowModes.ItemsRemoved += _ => windowModesChanged();
windowModesChanged();
}
private void windowModesChanged()
{
if (windowModes.Count > 1)
windowModeDropdown.Show();
else
windowModeDropdown.Hide();
} }
/// <summary> /// <summary>

View File

@ -25,9 +25,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay) private void load(BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay)
{ {
Children = new Drawable[] if (beatmaps.SupportsImportFromStable)
{ {
importBeatmapsButton = new SettingsButton Add(importBeatmapsButton = new SettingsButton
{ {
Text = "Import beatmaps from stable", Text = "Import beatmaps from stable",
Action = () => Action = () =>
@ -35,20 +35,25 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
importBeatmapsButton.Enabled.Value = false; importBeatmapsButton.Enabled.Value = false;
beatmaps.ImportFromStableAsync().ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true)); beatmaps.ImportFromStableAsync().ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true));
} }
}, });
deleteBeatmapsButton = new DangerousSettingsButton }
Add(deleteBeatmapsButton = new DangerousSettingsButton
{
Text = "Delete ALL beatmaps",
Action = () =>
{ {
Text = "Delete ALL beatmaps", dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
Action = () =>
{ {
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => deleteBeatmapsButton.Enabled.Value = false;
{ Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true));
deleteBeatmapsButton.Enabled.Value = false; }));
Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true)); }
})); });
}
}, if (skins.SupportsImportFromStable)
importSkinsButton = new SettingsButton {
Add(importSkinsButton = new SettingsButton
{ {
Text = "Import skins from stable", Text = "Import skins from stable",
Action = () => Action = () =>
@ -56,7 +61,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
importSkinsButton.Enabled.Value = false; importSkinsButton.Enabled.Value = false;
skins.ImportFromStableAsync().ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true)); skins.ImportFromStableAsync().ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true));
} }
}, });
}
AddRange(new Drawable[]
{
deleteSkinsButton = new DangerousSettingsButton deleteSkinsButton = new DangerousSettingsButton
{ {
Text = "Delete ALL skins", Text = "Delete ALL skins",
@ -91,7 +100,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
} }
}, },
}; });
} }
} }
} }

View File

@ -82,7 +82,7 @@ namespace osu.Game.Overlays.Settings.Sections
private void itemRemoved(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray()); private void itemRemoved(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray());
private void itemAdded(SkinInfo s, bool existing, bool silent) private void itemAdded(SkinInfo s, bool existing)
{ {
if (existing) if (existing)
return; return;

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -26,11 +27,25 @@ namespace osu.Game.Overlays.Settings
} }
} }
private IBindableList<T> itemSource;
public IBindableList<T> ItemSource
{
get => itemSource;
set
{
itemSource = value;
if (Control != null)
Control.ItemSource = value;
}
}
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
protected sealed override Drawable CreateControl() => CreateDropdown(); protected sealed override Drawable CreateControl() => CreateDropdown();
protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl { Items = Items }; protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl { Items = Items, ItemSource = ItemSource };
protected class DropdownControl : OsuDropdown<T> protected class DropdownControl : OsuDropdown<T>
{ {

View File

@ -139,6 +139,7 @@ namespace osu.Game.Overlays.Toolbar
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
HoverBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint); HoverBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint);
tooltipContainer.FadeOut(100);
return base.OnClick(e); return base.OnClick(e);
} }

View File

@ -301,7 +301,16 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
// Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
if (!string.IsNullOrEmpty(bankInfo.Filename)) if (!string.IsNullOrEmpty(bankInfo.Filename))
return new List<SampleInfo> { new FileSampleInfo { Filename = bankInfo.Filename } }; {
return new List<SampleInfo>
{
new FileSampleInfo
{
Filename = bankInfo.Filename,
Volume = bankInfo.Volume
}
};
}
var soundTypes = new List<SampleInfo> var soundTypes = new List<SampleInfo>
{ {

View File

@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.UI
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{ {
InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique); InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique);
gameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
} }
#region Action mapping (for replays) #region Action mapping (for replays)
@ -86,22 +87,28 @@ namespace osu.Game.Rulesets.UI
#region Clock control #region Clock control
private ManualClock clock; private readonly ManualClock manualClock;
private IFrameBasedClock parentClock;
private readonly FramedClock framedClock;
[Cached]
private GameplayClock gameplayClock;
private IFrameBasedClock parentGameplayClock;
[BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, GameplayClock clock)
{
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
if (clock != null)
parentGameplayClock = clock;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
setClock();
//our clock will now be our parent's clock, but we want to replace this to allow manual control.
parentClock = Clock;
ProcessCustomClock = false;
Clock = new FramedClock(clock = new ManualClock
{
CurrentTime = parentClock.CurrentTime,
Rate = parentClock.Rate,
});
} }
/// <summary> /// <summary>
@ -147,25 +154,28 @@ namespace osu.Game.Rulesets.UI
private void updateClock() private void updateClock()
{ {
if (parentClock == null) return; if (parentGameplayClock == null)
setClock(); // LoadComplete may not be run yet, but we still want the clock.
clock.Rate = parentClock.Rate; validState = true;
clock.IsRunning = parentClock.IsRunning;
var newProposedTime = parentClock.CurrentTime; manualClock.Rate = parentGameplayClock.Rate;
manualClock.IsRunning = parentGameplayClock.IsRunning;
var newProposedTime = parentGameplayClock.CurrentTime;
try try
{ {
if (Math.Abs(clock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
{ {
newProposedTime = clock.Rate > 0 newProposedTime = manualClock.Rate > 0
? Math.Min(newProposedTime, clock.CurrentTime + sixty_frame_time) ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
: Math.Max(newProposedTime, clock.CurrentTime - sixty_frame_time); : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
} }
if (!isAttached) if (!isAttached)
{ {
clock.CurrentTime = newProposedTime; manualClock.CurrentTime = newProposedTime;
} }
else else
{ {
@ -177,35 +187,39 @@ namespace osu.Game.Rulesets.UI
validState = false; validState = false;
requireMoreUpdateLoops = true; requireMoreUpdateLoops = true;
clock.CurrentTime = newProposedTime; manualClock.CurrentTime = newProposedTime;
return; return;
} }
clock.CurrentTime = newTime.Value; manualClock.CurrentTime = newTime.Value;
} }
requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime; requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime;
} }
finally finally
{ {
// The manual clock time has changed in the above code. The framed clock now needs to be updated // The manual clock time has changed in the above code. The framed clock now needs to be updated
// to ensure that the its time is valid for our children before input is processed // to ensure that the its time is valid for our children before input is processed
Clock.ProcessFrame(); framedClock.ProcessFrame();
} }
} }
private void setClock()
{
// in case a parent gameplay clock isn't available, just use the parent clock.
if (parentGameplayClock == null)
parentGameplayClock = Clock;
Clock = gameplayClock;
ProcessCustomClock = false;
}
#endregion #endregion
#region Setting application (disables etc.) #region Setting application (disables etc.)
private Bindable<bool> mouseDisabled; private Bindable<bool> mouseDisabled;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
}
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
switch (e) switch (e)

View File

@ -109,7 +109,27 @@ namespace osu.Game.Scoring
public string UserString public string UserString
{ {
get => User?.Username; get => User?.Username;
set => User = new User { Username = value }; set
{
if (User == null)
User = new User();
User.Username = value;
}
}
[JsonIgnore]
[Column("UserID")]
public long? UserID
{
get => User?.Id ?? 1;
set
{
if (User == null)
User = new User();
User.Id = value ?? 1;
}
} }
[JsonIgnore] [JsonIgnore]
@ -158,5 +178,7 @@ namespace osu.Game.Scoring
{ {
public string Acronym { get; set; } public string Acronym { get; set; }
} }
public override string ToString() => $"{User} playing {Beatmap}";
} }
} }

View File

@ -8,7 +8,7 @@ using osu.Game.Database;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
public class ScoreStore : MutableDatabaseBackedStore<ScoreInfo> public class ScoreStore : MutableDatabaseBackedStoreWithFileIncludes<ScoreInfo, ScoreFileInfo>
{ {
public ScoreStore(IDatabaseContextFactory factory, Storage storage) public ScoreStore(IDatabaseContextFactory factory, Storage storage)
: base(factory, storage) : base(factory, storage)
@ -17,7 +17,6 @@ namespace osu.Game.Scoring
protected override IQueryable<ScoreInfo> AddIncludesForConsumption(IQueryable<ScoreInfo> query) protected override IQueryable<ScoreInfo> AddIncludesForConsumption(IQueryable<ScoreInfo> query)
=> base.AddIncludesForConsumption(query) => base.AddIncludesForConsumption(query)
.Include(s => s.Files).ThenInclude(f => f.FileInfo)
.Include(s => s.Beatmap) .Include(s => s.Beatmap)
.Include(s => s.Ruleset); .Include(s => s.Ruleset);
} }

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Backgrounds namespace osu.Game.Screens.Backgrounds
{ {
@ -13,7 +15,18 @@ namespace osu.Game.Screens.Backgrounds
{ {
private WorkingBeatmap beatmap; private WorkingBeatmap beatmap;
public WorkingBeatmap Beatmap /// <summary>
/// Whether or not user dim settings should be applied to this Background.
/// </summary>
public readonly Bindable<bool> EnableUserDim = new Bindable<bool>();
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
private readonly UserDimContainer fadeContainer;
protected virtual UserDimContainer CreateFadeContainer() => new UserDimContainer { RelativeSizeAxes = Axes.Both };
public virtual WorkingBeatmap Beatmap
{ {
get => beatmap; get => beatmap;
set set
@ -37,8 +50,9 @@ namespace osu.Game.Screens.Backgrounds
} }
b.Depth = newDepth; b.Depth = newDepth;
AddInternal(Background = b); fadeContainer.Add(Background = b);
Background.BlurSigma = BlurTarget; Background.BlurSigma = BlurTarget;
StoryboardReplacesBackground.BindTo(fadeContainer.StoryboardReplacesBackground);
})); }));
}); });
} }
@ -47,6 +61,8 @@ namespace osu.Game.Screens.Backgrounds
public BackgroundScreenBeatmap(WorkingBeatmap beatmap = null) public BackgroundScreenBeatmap(WorkingBeatmap beatmap = null)
{ {
Beatmap = beatmap; Beatmap = beatmap;
InternalChild = fadeContainer = CreateFadeContainer();
fadeContainer.EnableUserDim.BindTo(EnableUserDim);
} }
public override bool Equals(BackgroundScreen other) public override bool Equals(BackgroundScreen other)
@ -57,7 +73,7 @@ namespace osu.Game.Screens.Backgrounds
return base.Equals(other) && beatmap == otherBeatmapBackground.Beatmap; return base.Equals(other) && beatmap == otherBeatmapBackground.Beatmap;
} }
private class BeatmapBackground : Background protected class BeatmapBackground : Background
{ {
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;

View File

@ -15,6 +15,9 @@ namespace osu.Game.Screens
protected Vector2 BlurTarget; protected Vector2 BlurTarget;
public TransformSequence<Background> BlurTo(Vector2 sigma, double duration, Easing easing = Easing.None) public TransformSequence<Background> BlurTo(Vector2 sigma, double duration, Easing easing = Easing.None)
=> Background?.BlurTo(BlurTarget = sigma, duration, easing); {
BlurTarget = sigma;
return Background?.BlurTo(BlurTarget, duration, easing);
}
} }
} }

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 0.5f, Height = 0.5f,
Icon = FontAwesome.fa_search_plus, Icon = FontAwesome.fa_search_plus,
Action = () => timeline.Zoom++ Action = () => changeZoom(1)
}, },
new TimelineButton new TimelineButton
{ {
@ -100,7 +100,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 0.5f, Height = 0.5f,
Icon = FontAwesome.fa_search_minus, Icon = FontAwesome.fa_search_minus,
Action = () => timeline.Zoom-- Action = () => changeZoom(-1)
}, },
} }
} }
@ -124,5 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
timeline.WaveformVisible.BindTo(waveformCheckbox.Current); timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
} }
private void changeZoom(float change) => timeline.Zoom += change;
} }
} }

View File

@ -22,6 +22,8 @@ using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.Design;
using osuTK.Input; using osuTK.Input;
using System.Collections.Generic;
using osu.Framework;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
@ -67,6 +69,15 @@ namespace osu.Game.Screens.Edit
SummaryTimeline timeline; SummaryTimeline timeline;
PlaybackControl playback; PlaybackControl playback;
var fileMenuItems = new List<MenuItem>();
if (RuntimeInfo.IsDesktop)
{
fileMenuItems.Add(new EditorMenuItem("Export", MenuItemType.Standard, exportBeatmap));
fileMenuItems.Add(new EditorMenuItemSpacer());
}
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
InternalChildren = new[] InternalChildren = new[]
{ {
new Container new Container
@ -94,12 +105,7 @@ namespace osu.Game.Screens.Edit
{ {
new MenuItem("File") new MenuItem("File")
{ {
Items = new[] Items = fileMenuItems
{
new EditorMenuItem("Export", MenuItemType.Standard, exportBeatmap),
new EditorMenuItemSpacer(),
new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)
}
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens
/// <summary> /// <summary>
/// Whether a top-level component should be allowed to exit the current screen to, for example, /// Whether a top-level component should be allowed to exit the current screen to, for example,
/// complete an import. /// complete an import. Note that this can be overridden by a user if they specifically request.
/// </summary> /// </summary>
bool AllowExternalScreenChange { get; } bool AllowExternalScreenChange { get; }

View File

@ -5,14 +5,17 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Users;
namespace osu.Game.Screens.Menu namespace osu.Game.Screens.Menu
{ {
@ -33,13 +36,15 @@ namespace osu.Game.Screens.Menu
private const float icon_y = -85; private const float icon_y = -85;
private readonly Bindable<User> currentUser = new Bindable<User>();
public Disclaimer() public Disclaimer()
{ {
ValidForResume = false; ValidForResume = false;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, APIAccess api)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -70,7 +75,7 @@ namespace osu.Game.Screens.Menu
textFlow.AddParagraph("Things may not work as expected", t => t.Font = t.Font.With(size: 20)); textFlow.AddParagraph("Things may not work as expected", t => t.Font = t.Font.With(size: 20));
textFlow.NewParagraph(); textFlow.NewParagraph();
Action<SpriteText> format = t => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold); Action<SpriteText> format = t => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold);
textFlow.AddParagraph("Detailed bug reports are welcomed via github issues.", format); textFlow.AddParagraph("Detailed bug reports are welcomed via github issues.", format);
textFlow.NewParagraph(); textFlow.NewParagraph();
@ -96,6 +101,13 @@ namespace osu.Game.Screens.Menu
}).First()); }).First());
iconColour = colours.Yellow; iconColour = colours.Yellow;
currentUser.BindTo(api.LocalUser);
currentUser.BindValueChanged(e =>
{
if (e.NewValue.IsSupporter)
supporterDrawables.ForEach(d => d.FadeOut(500, Easing.OutQuint).Expire());
}, true);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -9,6 +9,13 @@ namespace osu.Game.Screens.Multi.Components
{ {
public class MultiplayerBackgroundSprite : MultiplayerComposite public class MultiplayerBackgroundSprite : MultiplayerComposite
{ {
private readonly BeatmapSetCoverType beatmapSetCoverType;
public MultiplayerBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
{
this.beatmapSetCoverType = beatmapSetCoverType;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -19,6 +26,6 @@ namespace osu.Game.Screens.Multi.Components
CurrentItem.BindValueChanged(item => sprite.Beatmap.Value = item.NewValue?.Beatmap, true); CurrentItem.BindValueChanged(item => sprite.Beatmap.Value = item.NewValue?.Beatmap, true);
} }
protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }; protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(beatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
} }
} }

View File

@ -11,6 +11,7 @@ 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.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -137,7 +138,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
Width = cover_width, Width = cover_width,
Masking = true, Masking = true,
Margin = new MarginPadding { Left = side_strip_width }, Margin = new MarginPadding { Left = side_strip_width },
Child = new MultiplayerBackgroundSprite { RelativeSizeAxes = Axes.Both } Child = new MultiplayerBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both }
}, },
new Container new Container
{ {

View File

@ -28,13 +28,15 @@ namespace osu.Game.Screens.Multi.Match.Components
{ {
base.LoadComplete(); base.LoadComplete();
roomId.BindValueChanged(_ => updateChannel(), true); channelId.BindValueChanged(_ => updateChannel(), true);
} }
private void updateChannel() private void updateChannel()
{ {
if (roomId.Value != null) if (roomId.Value == null || channelId.Value == 0)
Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#mp_{roomId.Value}" }); return;
Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" });
} }
} }
} }

View File

@ -54,7 +54,7 @@ namespace osu.Game.Screens.Multi.Match.Components
hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID) != null; hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID) != null;
} }
private void beatmapAdded(BeatmapSetInfo model, bool existing, bool silent) private void beatmapAdded(BeatmapSetInfo model, bool existing)
{ {
if (Beatmap.Value == null) if (Beatmap.Value == null)
return; return;

View File

@ -202,7 +202,7 @@ namespace osu.Game.Screens.Multi.Match
/// <summary> /// <summary>
/// Handle the case where a beatmap is imported (and can be used by this match). /// Handle the case where a beatmap is imported (and can be used by this match).
/// </summary> /// </summary>
private void beatmapAdded(BeatmapSetInfo model, bool existing, bool silent) => Schedule(() => private void beatmapAdded(BeatmapSetInfo model, bool existing) => Schedule(() =>
{ {
if (Beatmap.Value != beatmapManager.DefaultBeatmap) if (Beatmap.Value != beatmapManager.DefaultBeatmap)
return; return;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
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;
@ -40,13 +41,7 @@ namespace osu.Game.Screens.Play
private readonly BreakInfo info; private readonly BreakInfo info;
private readonly BreakArrows breakArrows; private readonly BreakArrows breakArrows;
public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null)
: this(letterboxing)
{
bindProcessor(scoreProcessor);
}
public BreakOverlay(bool letterboxing)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Child = fadeContainer = new Container Child = fadeContainer = new Container
@ -98,6 +93,14 @@ namespace osu.Game.Screens.Play
} }
} }
}; };
if (scoreProcessor != null) bindProcessor(scoreProcessor);
}
[BackgroundDependencyLoader(true)]
private void load(GameplayClock clock)
{
if (clock != null) Clock = clock;
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Timing;
namespace osu.Game.Screens.Play
{
/// <summary>
/// A clock which is used for gameplay elements that need to follow audio time 1:1.
/// Exposed via DI by <see cref="PausableGameplayContainer"/>.
/// <remarks>
/// The main purpose of this clock is to stop components using it from accidentally processing the main
/// <see cref="IFrameBasedClock"/>, as this should only be done once to ensure accuracy.
/// </remarks>
/// </summary>
public class GameplayClock : IFrameBasedClock
{
private readonly IFrameBasedClock underlyingClock;
public GameplayClock(IFrameBasedClock underlyingClock)
{
this.underlyingClock = underlyingClock;
}
public double CurrentTime => underlyingClock.CurrentTime;
public double Rate => underlyingClock.Rate;
public bool IsRunning => underlyingClock.IsRunning;
public void ProcessFrame()
{
// we do not want to process the underlying clock.
// this is handled by PauseContainer.
}
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
public double FramesPerSecond => underlyingClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play
private static bool hasShownNotificationOnce; private static bool hasShownNotificationOnce;
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock) public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
KeyCounter = CreateKeyCounter(adjustableClock as IFrameBasedClock), KeyCounter = CreateKeyCounter(),
HoldToQuit = CreateHoldForMenuButton(), HoldToQuit = CreateHoldForMenuButton(),
} }
} }
@ -91,9 +91,8 @@ namespace osu.Game.Screens.Play
BindRulesetContainer(rulesetContainer); BindRulesetContainer(rulesetContainer);
Progress.Objects = rulesetContainer.Objects; Progress.Objects = rulesetContainer.Objects;
Progress.AudioClock = offsetClock;
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
Progress.OnSeek = pos => adjustableClock.Seek(pos); Progress.RequestSeek = pos => adjustableClock.Seek(pos);
ModDisplay.Current.BindTo(working.Mods); ModDisplay.Current.BindTo(working.Mods);
@ -202,13 +201,12 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20 } Margin = new MarginPadding { Top = 20 }
}; };
protected virtual KeyCounterCollection CreateKeyCounter(IFrameBasedClock offsetClock) => new KeyCounterCollection protected virtual KeyCounterCollection CreateKeyCounter() => new KeyCounterCollection
{ {
FadeTime = 50, FadeTime = 50,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding(10), Margin = new MarginPadding(10),
AudioClock = offsetClock
}; };
protected virtual SongProgress CreateProgress() => new SongProgress protected virtual SongProgress CreateProgress() => new SongProgress

View File

@ -71,9 +71,12 @@ namespace osu.Game.Screens.Play
Name = name; Name = name;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(TextureStore textures) private void load(TextureStore textures, GameplayClock clock)
{ {
if (clock != null)
Clock = clock;
Children = new Drawable[] Children = new Drawable[]
{ {
buttonSprite = new Sprite buttonSprite = new Sprite

View File

@ -8,7 +8,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -37,9 +36,6 @@ namespace osu.Game.Screens.Play
key.FadeTime = FadeTime; key.FadeTime = FadeTime;
key.KeyDownTextColor = KeyDownTextColor; key.KeyDownTextColor = KeyDownTextColor;
key.KeyUpTextColor = KeyUpTextColor; key.KeyUpTextColor = KeyUpTextColor;
// Use the same clock object as SongProgress for saving KeyCounter state
if (AudioClock != null)
key.Clock = AudioClock;
} }
public void ResetCount() public void ResetCount()
@ -125,8 +121,6 @@ namespace osu.Game.Screens.Play
public override bool HandleNonPositionalInput => receptor == null; public override bool HandleNonPositionalInput => receptor == null;
public override bool HandlePositionalInput => receptor == null; public override bool HandlePositionalInput => receptor == null;
public IFrameBasedClock AudioClock { get; set; }
private Receptor receptor; private Receptor receptor;
public Receptor GetReceptor() public Receptor GetReceptor()

View File

@ -14,10 +14,11 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
/// <summary> /// <summary>
/// A container which handles pausing children, displaying a pause overlay with choices etc. /// A container which handles pausing children, displaying a pause overlay with choices and processing the clock.
/// Exposes a <see cref="GameplayClock"/> to children via DI.
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/> /// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
/// </summary> /// </summary>
public class PauseContainer : Container public class PausableGameplayContainer : Container
{ {
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool();
@ -43,24 +44,32 @@ namespace osu.Game.Screens.Play
public Action OnRetry; public Action OnRetry;
public Action OnQuit; public Action OnQuit;
private readonly FramedClock framedClock; private readonly FramedClock offsetClock;
private readonly DecoupleableInterpolatingFramedClock decoupledClock; private readonly DecoupleableInterpolatingFramedClock adjustableClock;
/// <summary> /// <summary>
/// Creates a new <see cref="PauseContainer"/>. /// The final clock which is exposed to underlying components.
/// </summary> /// </summary>
/// <param name="framedClock">The gameplay clock. This is the clock that will process frames.</param> [Cached]
/// <param name="decoupledClock">The seekable clock. This is the clock that will be paused and resumed.</param> private readonly GameplayClock gameplayClock;
public PauseContainer(FramedClock framedClock, DecoupleableInterpolatingFramedClock decoupledClock)
/// <summary>
/// Creates a new <see cref="PausableGameplayContainer"/>.
/// </summary>
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param>
/// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param>
public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock)
{ {
this.framedClock = framedClock; this.offsetClock = offsetClock;
this.decoupledClock = decoupledClock; this.adjustableClock = adjustableClock;
gameplayClock = new GameplayClock(offsetClock);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AddInternal(content = new Container AddInternal(content = new Container
{ {
Clock = this.framedClock, Clock = this.offsetClock,
ProcessCustomClock = false, ProcessCustomClock = false,
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}); });
@ -84,7 +93,7 @@ namespace osu.Game.Screens.Play
if (IsPaused.Value) return; if (IsPaused.Value) return;
// stop the seekable clock (stops the audio eventually) // stop the seekable clock (stops the audio eventually)
decoupledClock.Stop(); adjustableClock.Stop();
IsPaused.Value = true; IsPaused.Value = true;
pauseOverlay.Show(); pauseOverlay.Show();
@ -102,8 +111,8 @@ namespace osu.Game.Screens.Play
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
// This accounts for the audio clock source potentially taking time to enter a completely stopped state // This accounts for the audio clock source potentially taking time to enter a completely stopped state
decoupledClock.Seek(decoupledClock.CurrentTime); adjustableClock.Seek(adjustableClock.CurrentTime);
decoupledClock.Start(); adjustableClock.Start();
pauseOverlay.Hide(); pauseOverlay.Hide();
} }
@ -123,7 +132,7 @@ namespace osu.Game.Screens.Play
Pause(); Pause();
if (!IsPaused.Value) if (!IsPaused.Value)
framedClock.ProcessFrame(); offsetClock.ProcessFrame();
base.Update(); base.Update();
} }

View File

@ -19,7 +19,6 @@ using osu.Framework.Threading;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -32,7 +31,6 @@ using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -57,6 +55,8 @@ namespace osu.Game.Screens.Play
private Bindable<bool> mouseWheelDisabled; private Bindable<bool> mouseWheelDisabled;
private Bindable<double> userAudioOffset; private Bindable<double> userAudioOffset;
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
public int RestartCount; public int RestartCount;
public CursorContainer Cursor => RulesetContainer.Cursor; public CursorContainer Cursor => RulesetContainer.Cursor;
@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
private PauseContainer pauseContainer; protected PausableGameplayContainer PausableGameplayContainer { get; private set; }
private RulesetInfo ruleset; private RulesetInfo ruleset;
@ -80,14 +80,21 @@ namespace osu.Game.Screens.Play
private SampleChannel sampleRestart; private SampleChannel sampleRestart;
protected ScoreProcessor ScoreProcessor; protected ScoreProcessor ScoreProcessor { get; private set; }
protected RulesetContainer RulesetContainer; protected RulesetContainer RulesetContainer { get; private set; }
protected HUDOverlay HUDOverlay; protected HUDOverlay HUDOverlay { get; private set; }
private FailOverlay failOverlay; private FailOverlay failOverlay;
private DrawableStoryboard storyboard; private DrawableStoryboard storyboard;
private Container storyboardContainer; protected UserDimContainer StoryboardContainer { get; private set; }
protected virtual UserDimContainer CreateStoryboardContainer() => new UserDimContainer(true)
{
RelativeSizeAxes = Axes.Both,
Alpha = 1,
EnableUserDim = { Value = true }
};
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true; public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
@ -168,19 +175,15 @@ namespace osu.Game.Screens.Play
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
pauseContainer = new PauseContainer(offsetClock, adjustableClock) PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock)
{ {
Retries = RestartCount, Retries = RestartCount,
OnRetry = Restart, OnRetry = restart,
OnQuit = performUserRequestedExit, OnQuit = performUserRequestedExit,
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value, CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
Children = new[] Children = new Container[]
{ {
storyboardContainer = new Container StoryboardContainer = CreateStoryboardContainer(),
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
new ScalingContainer(ScalingMode.Gameplay) new ScalingContainer(ScalingMode.Gameplay)
{ {
Child = new LocalSkinOverrideContainer(working.Skin) Child = new LocalSkinOverrideContainer(working.Skin)
@ -193,32 +196,26 @@ namespace osu.Game.Screens.Play
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
ProcessCustomClock = false,
Breaks = beatmap.Breaks Breaks = beatmap.Breaks
}, },
new ScalingContainer(ScalingMode.Gameplay) new ScalingContainer(ScalingMode.Gameplay)
{ {
Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(), Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
}, },
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock)
{ {
Clock = Clock, // hud overlay doesn't want to use the audio clock directly
ProcessCustomClock = false,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
}, },
new SkipOverlay(RulesetContainer.GameplayStartTime) new SkipOverlay(RulesetContainer.GameplayStartTime)
{ {
Clock = Clock, // skip button doesn't want to use the audio clock directly RequestSeek = time => adjustableClock.Seek(time)
ProcessCustomClock = false,
AdjustableClock = adjustableClock,
FramedClock = offsetClock,
}, },
} }
}, },
failOverlay = new FailOverlay failOverlay = new FailOverlay
{ {
OnRetry = Restart, OnRetry = restart,
OnQuit = performUserRequestedExit, OnQuit = performUserRequestedExit,
}, },
new HotkeyRetryOverlay new HotkeyRetryOverlay
@ -228,7 +225,7 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
fadeOut(true); fadeOut(true);
Restart(); restart();
}, },
} }
}; };
@ -236,7 +233,7 @@ namespace osu.Game.Screens.Play
HUDOverlay.HoldToQuit.Action = performUserRequestedExit; HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded); HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
RulesetContainer.IsPaused.BindTo(pauseContainer.IsPaused); RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused);
if (ShowStoryboard.Value) if (ShowStoryboard.Value)
initializeStoryboard(false); initializeStoryboard(false);
@ -265,7 +262,7 @@ namespace osu.Game.Screens.Play
this.Exit(); this.Exit();
} }
public void Restart() private void restart()
{ {
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
@ -295,7 +292,7 @@ namespace osu.Game.Screens.Play
var score = CreateScore(); var score = CreateScore();
if (RulesetContainer.ReplayScore == null) if (RulesetContainer.ReplayScore == null)
scoreManager.Import(score, true); scoreManager.Import(score);
this.Push(CreateResults(score)); this.Push(CreateResults(score));
@ -346,6 +343,18 @@ namespace osu.Game.Screens.Play
.Delay(250) .Delay(250)
.FadeIn(250); .FadeIn(250);
ShowStoryboard.ValueChanged += enabled =>
{
if (enabled.NewValue) initializeStoryboard(true);
};
Background.EnableUserDim.Value = true;
Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
StoryboardContainer.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
Task.Run(() => Task.Run(() =>
{ {
sourceClock.Reset(); sourceClock.Reset();
@ -357,7 +366,7 @@ namespace osu.Game.Screens.Play
this.Delay(750).Schedule(() => this.Delay(750).Schedule(() =>
{ {
if (!pauseContainer.IsPaused.Value) if (!PausableGameplayContainer.IsPaused.Value)
{ {
adjustableClock.Start(); adjustableClock.Start();
} }
@ -365,8 +374,8 @@ namespace osu.Game.Screens.Play
}); });
}); });
pauseContainer.Alpha = 0; PausableGameplayContainer.Alpha = 0;
pauseContainer.FadeIn(750, Easing.OutQuint); PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)
@ -384,17 +393,16 @@ namespace osu.Game.Screens.Play
return true; return true;
} }
if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!pauseContainer?.IsResuming ?? true)) if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true))
{ {
// In the case of replays, we may have changed the playback rate. // In the case of replays, we may have changed the playback rate.
applyRateFromMods(); applyRateFromMods();
fadeOut(); fadeOut();
return base.OnExiting(next); return base.OnExiting(next);
} }
if (LoadedBeatmapSuccessfully) if (LoadedBeatmapSuccessfully)
pauseContainer?.Pause(); PausableGameplayContainer?.Pause();
return true; return true;
} }
@ -403,14 +411,16 @@ namespace osu.Game.Screens.Play
{ {
float fadeOutDuration = instant ? 0 : 250; float fadeOutDuration = instant ? 0 : 250;
this.FadeOut(fadeOutDuration); this.FadeOut(fadeOutDuration);
Background?.FadeColour(Color4.White, fadeOutDuration, Easing.OutQuint);
Background.EnableUserDim.Value = false;
storyboardReplacesBackground.Value = false;
} }
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !pauseContainer.IsPaused.Value; protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !PausableGameplayContainer.IsPaused.Value;
private void initializeStoryboard(bool asyncLoad) private void initializeStoryboard(bool asyncLoad)
{ {
if (storyboardContainer == null) if (StoryboardContainer == null || storyboard != null)
return; return;
var beatmap = Beatmap.Value; var beatmap = Beatmap.Value;
@ -419,29 +429,9 @@ namespace osu.Game.Screens.Play
storyboard.Masking = true; storyboard.Masking = true;
if (asyncLoad) if (asyncLoad)
LoadComponentAsync(storyboard, storyboardContainer.Add); LoadComponentAsync(storyboard, StoryboardContainer.Add);
else else
storyboardContainer.Add(storyboard); StoryboardContainer.Add(storyboard);
}
protected override void UpdateBackgroundElements()
{
if (!this.IsCurrentScreen()) return;
base.UpdateBackgroundElements();
if (ShowStoryboard.Value && storyboard == null)
initializeStoryboard(true);
var beatmap = Beatmap.Value;
var storyboardVisible = ShowStoryboard.Value && beatmap.Storyboard.HasDrawable;
storyboardContainer?
.FadeColour(OsuColour.Gray(BackgroundOpacity), BACKGROUND_FADE_DURATION, Easing.OutQuint)
.FadeTo(storyboardVisible && BackgroundOpacity > 0 ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint);
if (storyboardVisible && beatmap.Storyboard.ReplacesBackground)
Background?.FadeColour(Color4.Black, BACKGROUND_FADE_DURATION, Easing.OutQuint);
} }
protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score);

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -78,7 +79,7 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding(25), Margin = new MarginPadding(25),
Children = new PlayerSettingsGroup[] Children = new PlayerSettingsGroup[]
{ {
visualSettings = new VisualSettings(), VisualSettings = new VisualSettings(),
new InputSettings() new InputSettings()
} }
} }
@ -151,23 +152,31 @@ namespace osu.Game.Screens.Play
} }
private ScheduledDelegate pushDebounce; private ScheduledDelegate pushDebounce;
private VisualSettings visualSettings; protected VisualSettings VisualSettings;
private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && GetContainingInputManager()?.DraggedDrawable == null; private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && GetContainingInputManager()?.DraggedDrawable == null;
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
// restore our screen defaults // restore our screen defaults
InitializeBackgroundElements(); if (this.IsCurrentScreen())
{
InitializeBackgroundElements();
Background.EnableUserDim.Value = false;
}
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
if (GetContainingInputManager()?.HoveredDrawables.Contains(visualSettings) == true) if (GetContainingInputManager()?.HoveredDrawables.Contains(VisualSettings) == true)
{ {
// show user setting preview // Update background elements is only being called here because blur logic still exists in Player.
// Will need to be removed when resolving https://github.com/ppy/osu/issues/4322
UpdateBackgroundElements(); UpdateBackgroundElements();
if (this.IsCurrentScreen())
Background.EnableUserDim.Value = true;
} }
base.OnHoverLost(e); base.OnHoverLost(e);
@ -241,6 +250,8 @@ namespace osu.Game.Screens.Play
this.FadeOut(150); this.FadeOut(150);
cancelLoad(); cancelLoad();
Background.EnableUserDim.Value = false;
return base.OnExiting(next); return base.OnExiting(next);
} }
@ -286,6 +297,7 @@ namespace osu.Game.Screens.Play
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;
private LoadingAnimation loading; private LoadingAnimation loading;
private Sprite backgroundSprite; private Sprite backgroundSprite;
private ModDisplay modDisplay;
public bool Loading public bool Loading
{ {
@ -312,7 +324,7 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var metadata = beatmap?.BeatmapInfo?.Metadata ?? new BeatmapMetadata(); var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Children = new Drawable[] Children = new Drawable[]
@ -381,6 +393,14 @@ namespace osu.Game.Screens.Play
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
}, },
new ModDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 20 },
Current = beatmap.Mods
}
}, },
} }
}; };

View File

@ -6,7 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osuTK; using osuTK;
@ -16,15 +15,12 @@ namespace osu.Game.Screens.Play
{ {
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value);
protected new BackgroundScreenBeatmap Background => base.Background as BackgroundScreenBeatmap; protected new BackgroundScreenBeatmap Background => (BackgroundScreenBeatmap)base.Background;
protected const float BACKGROUND_FADE_DURATION = 800; protected const float BACKGROUND_FADE_DURATION = 800;
protected float BackgroundOpacity => 1 - (float)DimLevel.Value;
#region User Settings #region User Settings
protected Bindable<double> DimLevel;
protected Bindable<double> BlurLevel; protected Bindable<double> BlurLevel;
protected Bindable<bool> ShowStoryboard; protected Bindable<bool> ShowStoryboard;
@ -33,7 +29,6 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
DimLevel = config.GetBindable<double>(OsuSetting.DimLevel);
BlurLevel = config.GetBindable<double>(OsuSetting.BlurLevel); BlurLevel = config.GetBindable<double>(OsuSetting.BlurLevel);
ShowStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard); ShowStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
} }
@ -41,9 +36,7 @@ namespace osu.Game.Screens.Play
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
DimLevel.ValueChanged += _ => UpdateBackgroundElements();
BlurLevel.ValueChanged += _ => UpdateBackgroundElements(); BlurLevel.ValueChanged += _ => UpdateBackgroundElements();
ShowStoryboard.ValueChanged += _ => UpdateBackgroundElements();
InitializeBackgroundElements(); InitializeBackgroundElements();
} }
@ -66,7 +59,6 @@ namespace osu.Game.Screens.Play
{ {
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
Background?.FadeColour(OsuColour.Gray(BackgroundOpacity), BACKGROUND_FADE_DURATION, Easing.OutQuint);
Background?.BlurTo(new Vector2((float)BlurLevel.Value * 25), BACKGROUND_FADE_DURATION, Easing.OutQuint); Background?.BlurTo(new Vector2((float)BlurLevel.Value * 25), BACKGROUND_FADE_DURATION, Easing.OutQuint);
} }
} }

View File

@ -9,7 +9,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -27,8 +26,7 @@ namespace osu.Game.Screens.Play
{ {
private readonly double startTime; private readonly double startTime;
public IAdjustableClock AdjustableClock; public Action<double> RequestSeek;
public IFrameBasedClock FramedClock;
private Button button; private Button button;
private Box remainingTimeBox; private Box remainingTimeBox;
@ -54,16 +52,13 @@ namespace osu.Game.Screens.Play
Origin = Anchor.Centre; Origin = Anchor.Centre;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours) private void load(OsuColour colours, GameplayClock clock)
{ {
var baseClock = Clock; var baseClock = Clock;
if (FramedClock != null) if (clock != null)
{ Clock = clock;
Clock = FramedClock;
ProcessCustomClock = false;
}
Children = new Drawable[] Children = new Drawable[]
{ {
@ -111,7 +106,7 @@ namespace osu.Game.Screens.Play
using (BeginAbsoluteSequence(beginFadeTime)) using (BeginAbsoluteSequence(beginFadeTime))
this.FadeOut(fade_time); this.FadeOut(fade_time);
button.Action = () => AdjustableClock?.Seek(startTime - skip_required_cutoff - fade_time); button.Action = () => RequestSeek?.Invoke(startTime - skip_required_cutoff - fade_time);
displayTime = Time.Current; displayTime = Time.Current;

View File

@ -10,7 +10,6 @@ using osu.Game.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -29,18 +28,11 @@ namespace osu.Game.Screens.Play
private readonly SongProgressGraph graph; private readonly SongProgressGraph graph;
private readonly SongProgressInfo info; private readonly SongProgressInfo info;
public Action<double> OnSeek; public Action<double> RequestSeek;
public override bool HandleNonPositionalInput => AllowSeeking; public override bool HandleNonPositionalInput => AllowSeeking;
public override bool HandlePositionalInput => AllowSeeking; public override bool HandlePositionalInput => AllowSeeking;
private IClock audioClock;
public IClock AudioClock
{
set => audioClock = info.AudioClock = value;
}
private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1; private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1;
private double firstHitTime => objects.First().StartTime; private double firstHitTime => objects.First().StartTime;
@ -63,9 +55,14 @@ namespace osu.Game.Screens.Play
private readonly BindableBool replayLoaded = new BindableBool(); private readonly BindableBool replayLoaded = new BindableBool();
[BackgroundDependencyLoader] private GameplayClock gameplayClock;
private void load(OsuColour colours)
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, GameplayClock clock)
{ {
if (clock != null)
gameplayClock = clock;
graph.FillColour = bar.FillColour = colours.BlueLighter; graph.FillColour = bar.FillColour = colours.BlueLighter;
} }
@ -99,7 +96,7 @@ namespace osu.Game.Screens.Play
Alpha = 0, Alpha = 0,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
OnSeek = position => OnSeek?.Invoke(position), OnSeek = time => RequestSeek?.Invoke(time),
}, },
}; };
} }
@ -157,14 +154,11 @@ namespace osu.Game.Screens.Play
if (objects == null) if (objects == null)
return; return;
double position = audioClock?.CurrentTime ?? Time.Current; double position = gameplayClock?.CurrentTime ?? Time.Current;
double progress = (position - firstHitTime) / (lastHitTime - firstHitTime); double progress = Math.Min(1, (position - firstHitTime) / (lastHitTime - firstHitTime));
if (progress < 1) bar.CurrentTime = position;
{ graph.Progress = (int)(graph.ColumnCount * progress);
bar.CurrentTime = position;
graph.Progress = (int)(graph.ColumnCount * progress);
}
} }
} }
} }

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using System; using System;
@ -27,8 +26,6 @@ namespace osu.Game.Screens.Play
private const int margin = 10; private const int margin = 10;
public IClock AudioClock;
public double StartTime public double StartTime
{ {
set => startTime = value; set => startTime = value;
@ -39,9 +36,14 @@ namespace osu.Game.Screens.Play
set => endTime = value; set => endTime = value;
} }
[BackgroundDependencyLoader] private GameplayClock gameplayClock;
private void load(OsuColour colours)
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, GameplayClock clock)
{ {
if (clock != null)
gameplayClock = clock;
Children = new Drawable[] Children = new Drawable[]
{ {
timeCurrent = new OsuSpriteText timeCurrent = new OsuSpriteText
@ -80,7 +82,9 @@ namespace osu.Game.Screens.Play
{ {
base.Update(); base.Update();
double songCurrentTime = AudioClock.CurrentTime - startTime; var time = gameplayClock?.CurrentTime ?? Time.Current;
double songCurrentTime = time - startTime;
int currentPercent = Math.Max(0, Math.Min(100, (int)(songCurrentTime / songLength * 100))); int currentPercent = Math.Max(0, Math.Min(100, (int)(songCurrentTime / songLength * 100)));
int currentSecond = (int)Math.Floor(songCurrentTime / 1000.0); int currentSecond = (int)Math.Floor(songCurrentTime / 1000.0);
@ -93,7 +97,7 @@ namespace osu.Game.Screens.Play
if (currentSecond != previousSecond && songCurrentTime < songLength) if (currentSecond != previousSecond && songCurrentTime < songLength)
{ {
timeCurrent.Text = formatTime(TimeSpan.FromSeconds(currentSecond)); timeCurrent.Text = formatTime(TimeSpan.FromSeconds(currentSecond));
timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - AudioClock.CurrentTime)); timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - time));
previousSecond = currentSecond; previousSecond = currentSecond;
} }

View File

@ -242,7 +242,11 @@ namespace osu.Game.Screens.Play
// Reverse drawableRows so when iterating through them they start at the bottom // Reverse drawableRows so when iterating through them they start at the bottom
drawableRows.Reverse(); drawableRows.Reverse();
}
protected override void LoadComplete()
{
base.LoadComplete();
fillActive(); fillActive();
} }

View File

@ -0,0 +1,48 @@
// 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.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Overlays.Dialog;
using osu.Game.Scoring;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace osu.Game.Screens.Select
{
public class BeatmapClearScoresDialog : PopupDialog
{
private ScoreManager scoreManager;
public BeatmapClearScoresDialog(BeatmapInfo beatmap, Action onCompletion)
{
BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}";
Icon = FontAwesome.fa_eraser;
HeaderText = @"Clearing all local scores. Are you sure?";
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Yes. Please.",
Action = () =>
{
Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmap.ID).ToList()))
.ContinueWith(_ => onCompletion);
}
},
new PopupDialogCancelButton
{
Text = @"No, I'm still attached.",
},
};
}
[BackgroundDependencyLoader]
private void load(ScoreManager scoreManager)
{
this.scoreManager = scoreManager;
}
}
}

View File

@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select.Leaderboards
{ {
if (Scope == BeatmapLeaderboardScope.Local) if (Scope == BeatmapLeaderboardScope.Local)
{ {
Scores = scoreManager.QueryScores(s => s.Beatmap.ID == Beatmap.ID).ToArray(); Scores = scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == Beatmap.ID).ToArray();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
return null; return null;
} }

View File

@ -60,9 +60,12 @@ namespace osu.Game.Screens.Select
if (base.OnExiting(next)) if (base.OnExiting(next))
return true; return true;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value?.Beatmap); if (CurrentItem.Value != null)
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value?.RequiredMods; {
Ruleset.Value = CurrentItem.Value?.Ruleset; Ruleset.Value = CurrentItem.Value.Ruleset;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap);
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods;
}
Beatmap.Disabled = true; Beatmap.Disabled = true;
Ruleset.Disabled = true; Ruleset.Disabled = true;

View File

@ -1,11 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osuTK;
using osuTK.Input;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -13,8 +8,8 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -31,7 +26,12 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -226,7 +226,7 @@ namespace osu.Game.Screens.Select
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.fa_trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.fa_trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue);
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.fa_times_circle_o, colours.Purple, null, Key.Number1); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.fa_times_circle_o, colours.Purple, null, Key.Number1);
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.fa_eraser, colours.Purple, null, Key.Number2); BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.fa_eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo), Key.Number2);
} }
if (this.beatmaps == null) if (this.beatmaps == null)
@ -582,7 +582,7 @@ namespace osu.Game.Screens.Select
} }
} }
private void onBeatmapSetAdded(BeatmapSetInfo s, bool existing, bool silent) => Carousel.UpdateBeatmapSet(s); private void onBeatmapSetAdded(BeatmapSetInfo s, bool existing) => Carousel.UpdateBeatmapSet(s);
private void onBeatmapSetRemoved(BeatmapSetInfo s) => Carousel.RemoveBeatmapSet(s); private void onBeatmapSetRemoved(BeatmapSetInfo s) => Carousel.RemoveBeatmapSet(s);
private void onBeatmapRestored(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); private void onBeatmapRestored(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));
private void onBeatmapHidden(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); private void onBeatmapHidden(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));
@ -621,6 +621,15 @@ namespace osu.Game.Screens.Select
dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap));
} }
private void clearScores(BeatmapInfo beatmap)
{
if (beatmap == null || beatmap.ID <= 0) return;
dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap, () =>
// schedule done here rather than inside the dialog as the dialog may fade out and never callback.
Schedule(() => BeatmapDetails.Leaderboard.RefreshScores())));
}
public override bool OnPressed(GlobalAction action) public override bool OnPressed(GlobalAction action)
{ {
if (!this.IsCurrentScreen()) return false; if (!this.IsCurrentScreen()) return false;

View File

@ -1,22 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class SkinStore : MutableDatabaseBackedStore<SkinInfo> public class SkinStore : MutableDatabaseBackedStoreWithFileIncludes<SkinInfo, SkinFileInfo>
{ {
public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null) public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null)
: base(contextFactory, storage) : base(contextFactory, storage)
{ {
} }
protected override IQueryable<SkinInfo> AddIncludesForConsumption(IQueryable<SkinInfo> query) =>
base.AddIncludesForConsumption(query)
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -21,6 +22,18 @@ namespace osu.Game.Tests.Beatmaps
HitObjects = baseBeatmap.HitObjects; HitObjects = baseBeatmap.HitObjects;
BeatmapInfo.Ruleset = ruleset; BeatmapInfo.Ruleset = ruleset;
BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo
{
Status = BeatmapSetOnlineStatus.Ranked,
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/163112/covers/cover.jpg",
Card = "https://assets.ppy.sh/beatmaps/163112/covers/card.jpg",
List = "https://assets.ppy.sh/beatmaps/163112/covers/list.jpg"
}
};
} }
private static Beatmap createTestBeatmap() private static Beatmap createTestBeatmap()

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