1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-27 14:12:56 +08:00

Merge branch 'master' into hide-resume-overlay

This commit is contained in:
Dean Herbert 2023-02-16 15:42:14 +09:00
commit 7afdcb9383
312 changed files with 6063 additions and 2309 deletions

View File

@ -121,21 +121,12 @@ jobs:
build-only-ios: build-only-ios:
name: Build only (iOS) name: Build only (iOS)
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12. runs-on: macos-latest
runs-on: macos-12
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
# see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
# remove once all workflow VMs use Xcode 14.1
- name: Set Xcode Version
shell: bash
run: |
sudo xcode-select -s "/Applications/Xcode_14.1.app"
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
- name: Install .NET 6.0.x - name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:

View File

@ -17,7 +17,7 @@
<EmbeddedResource Include="Resources\**\*.*" /> <EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Code Analysis"> <PropertyGroup Label="Code Analysis">

View File

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

View File

@ -1,234 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.653.0)
aws-sdk-core (3.166.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.59.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.117.1)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.93.1)
faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.210.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.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.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-clean_testflight_testers (0.3.0)
fastlane-plugin-souyuz (0.11.1)
souyuz (= 0.11.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.29.0)
google-apis-core (>= 0.9.0, < 2.a)
google-apis-core (0.9.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.15.0)
google-apis-core (>= 0.9.0, < 2.a)
google-apis-playcustomapp_v1 (0.12.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0)
google-cloud-storage (1.43.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.3.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.5.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
nokogiri (1.13.9)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (5.0.0)
racc (1.6.0)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
souyuz (0.11.1)
fastlane (>= 2.182.0)
highline (~> 2.0)
nokogiri (~> 1.7)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
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

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

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

View File

@ -1,147 +0,0 @@
update_fastlane
platform :android do
desc 'Deploy to play store'
lane :beta do |options|
update_version(
version: options[:version],
build: options[:build],
)
build(options)
supply(
apk: './osu.Android/bin/Release/sh.ppy.osulazer-Signed.apk',
package_name: 'sh.ppy.osulazer',
track: 'alpha', # upload to alpha, we can promote it later
json_key: options[:json_key],
)
end
desc 'Deploy to github release'
lane :build_github do |options|
update_version(
version: options[:version],
build: options[:build],
)
build(options)
client = HTTPClient.new
changelog = client.get_content 'https://gist.githubusercontent.com/peppy/aaa2ec1a323554b619671cac6dbbb776/raw'
changelog.gsub!('$BUILD_ID', options[:build])
set_github_release(
repository_name: "ppy/osu",
api_token: ENV["GITHUB_TOKEN"],
name: options[:build],
tag_name: options[:build],
is_draft: true,
description: changelog,
commitish: "master",
upload_assets: ["osu.Android/bin/Release/sh.ppy.osulazer.apk"]
)
end
desc 'Compile the project'
lane :build do |options|
nuget_restore(project_path: 'osu.Android/osu.Android.csproj')
nuget_restore(project_path: 'osu.Game/osu.Game.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj')
souyuz(
build_configuration: 'Release',
solution_path: 'osu.sln',
platform: "android",
output_path: "osu.Android/bin/Release/",
keystore_path: options[:keystore_path],
keystore_alias: options[:keystore_alias],
keystore_password: ENV["KEYSTORE_PASSWORD"]
)
end
lane :update_version do |options|
split = options[:build].split('.')
split[1] = split[1].to_s.rjust(4, '0')
android_build = split.join('')
app_version(
solution_path: 'osu.sln',
version: options[:version],
build: android_build,
)
end
end
platform :ios do
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,
groups: ['osu! supporters', 'public'],
distribute_external: true,
ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa'
)
end
desc 'Compile the project'
lane :build do
nuget_restore(project_path: 'osu.iOS/osu.iOS.csproj')
nuget_restore(project_path: 'osu.Game/osu.Game.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj')
nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj')
souyuz(
platform: "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
lane :update_version do |options|
options[:plist_path] = 'osu.iOS/Info.plist'
app_version(options)
end
lane :testflight_prune_dry do
clean_testflight_testers(days_of_inactivity:30, dry_run: true)
end
lane :testflight_prune do
clean_testflight_testers(days_of_inactivity: 30)
end
end

View File

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

View File

@ -1,7 +0,0 @@
# 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'

View File

@ -1,109 +0,0 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android beta
```sh
[bundle exec] fastlane android beta
```
Deploy to play store
### android build_github
```sh
[bundle exec] fastlane android build_github
```
Deploy to github release
### android build
```sh
[bundle exec] fastlane android build
```
Compile the project
### android update_version
```sh
[bundle exec] fastlane android update_version
```
----
## iOS
### ios beta
```sh
[bundle exec] fastlane ios beta
```
Deploy to testflight
### ios build
```sh
[bundle exec] fastlane ios build
```
Compile the project
### ios provision
```sh
[bundle exec] fastlane ios provision
```
Install provisioning profiles using match
### ios update_version
```sh
[bundle exec] fastlane ios update_version
```
### ios testflight_prune_dry
```sh
[bundle exec] fastlane ios testflight_prune_dry
```
### ios testflight_prune
```sh
[bundle exec] fastlane ios testflight_prune
```
----
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

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.120.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.131.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -0,0 +1,63 @@
// 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.IO;
using System.Threading.Tasks;
using Android.Content;
using Android.Net;
using Android.Provider;
using osu.Game.Database;
namespace osu.Android
{
public class AndroidImportTask : ImportTask
{
private readonly ContentResolver contentResolver;
private readonly Uri uri;
private AndroidImportTask(Stream stream, string filename, ContentResolver contentResolver, Uri uri)
: base(stream, filename)
{
this.contentResolver = contentResolver;
this.uri = uri;
}
public override void DeleteFile()
{
contentResolver.Delete(uri, null, null);
}
public static async Task<AndroidImportTask?> Create(ContentResolver contentResolver, Uri uri)
{
// there are more performant overloads of this method, but this one is the most backwards-compatible
// (dates back to API 1).
var cursor = contentResolver.Query(uri, null, null, null, null);
if (cursor == null)
return null;
if (!cursor.MoveToFirst())
return null;
int filenameColumn = cursor.GetColumnIndex(IOpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn) ?? uri.Path ?? string.Empty;
// SharpCompress requires archive streams to be seekable, which the stream opened by
// OpenInputStream() seems to not necessarily be.
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = contentResolver.OpenInputStream(uri))
{
if (stream == null)
return null;
await stream.CopyToAsync(copy).ConfigureAwait(false);
}
return new AndroidImportTask(copy, filename, contentResolver, uri);
}
}
}

View File

@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -14,7 +13,6 @@ using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Graphics; using Android.Graphics;
using Android.OS; using Android.OS;
using Android.Provider;
using Android.Views; using Android.Views;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Database; using osu.Game.Database;
@ -131,28 +129,14 @@ namespace osu.Android
await Task.WhenAll(uris.Select(async uri => await Task.WhenAll(uris.Select(async uri =>
{ {
// there are more performant overloads of this method, but this one is the most backwards-compatible var task = await AndroidImportTask.Create(ContentResolver!, uri).ConfigureAwait(false);
// (dates back to API 1).
var cursor = ContentResolver?.Query(uri, null, null, null, null);
if (cursor == null) if (task != null)
return;
cursor.MoveToFirst();
int filenameColumn = cursor.GetColumnIndex(IOpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by
// OpenInputStream() seems to not necessarily be.
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = ContentResolver.OpenInputStream(uri))
await stream.CopyToAsync(copy).ConfigureAwait(false);
lock (tasks)
{ {
tasks.Add(new ImportTask(copy, filename)); lock (tasks)
{
tasks.Add(task);
}
} }
})).ConfigureAwait(false); })).ConfigureAwait(false);

View File

@ -98,7 +98,7 @@ namespace osu.Desktop
if (status.Value is UserStatusOnline && activity.Value != null) if (status.Value is UserStatusOnline && activity.Value != null)
{ {
presence.State = truncate(activity.Value.Status); presence.State = truncate(activity.Value.GetStatus(privacyMode.Value == DiscordRichPresenceMode.Limited));
presence.Details = truncate(getDetails(activity.Value)); presence.Details = truncate(getDetails(activity.Value));
if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0) if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0)
@ -169,7 +169,7 @@ namespace osu.Desktop
case UserActivity.InGame game: case UserActivity.InGame game:
return game.BeatmapInfo; return game.BeatmapInfo;
case UserActivity.Editing edit: case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo; return edit.BeatmapInfo;
} }
@ -183,9 +183,12 @@ namespace osu.Desktop
case UserActivity.InGame game: case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty; return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit: case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty; return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.WatchingReplay watching:
return watching.BeatmapInfo.ToString();
case UserActivity.InLobby lobby: case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
} }

View File

@ -29,6 +29,7 @@ namespace osu.Desktop
internal partial class OsuGameDesktop : OsuGame internal partial class OsuGameDesktop : OsuGame
{ {
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel; private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
public OsuGameDesktop(string[]? args = null) public OsuGameDesktop(string[]? args = null)
: base(args) : base(args)
@ -123,6 +124,7 @@ namespace osu.Desktop
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add); LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this); osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
} }
public override void SetHost(GameHost host) public override void SetHost(GameHost host)
@ -181,6 +183,7 @@ namespace osu.Desktop
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
osuSchemeLinkIPCChannel?.Dispose(); osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
} }
private class SDL2BatteryInfo : BatteryInfo private class SDL2BatteryInfo : BatteryInfo

View File

@ -26,8 +26,8 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" /> <PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.1.14" /> <PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -7,9 +7,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
<PackageReference Include="nunit" Version="3.13.3" /> <PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()), new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
new CatchModHidden(), new CatchModHidden(),
new CatchModFlashlight(), new CatchModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (targetComponent.Lookup) switch (targetComponent.Lookup)
{ {
case GlobalSkinComponentLookup.LookupType.MainHUDComponents: case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as SkinnableTargetComponentsContainer; var components = base.GetDrawableComponent(lookup) as Container;
if (providesComboCounter && components != null) if (providesComboCounter && components != null)
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -14,4 +14,6 @@ Hit200: mania/hit200@2x
Hit300: mania/hit300@2x Hit300: mania/hit300@2x
Hit300g: mania/hit300g@2x Hit300g: mania/hit300g@2x
StageLeft: mania/stage-left StageLeft: mania/stage-left
StageRight: mania/stage-right StageRight: mania/stage-right
NoteImage0L: LongNoteTailWang
NoteImage1L: LongNoteTailWang

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()), new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
new ManiaModFlashlight(), new ManiaModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
}; };
} }
private partial class ManiaScrollSlider : OsuSliderBar<double> private partial class ManiaScrollSlider : RoundedSliderBar<double>
{ {
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value)); public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value));
} }

View File

@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
private double? releaseTime; private double? releaseTime;
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
public DrawableHoldNote() public DrawableHoldNote()
: this(null) : this(null)
{ {
@ -376,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void OnFree() protected override void OnFree()
{ {
slidingSample.Samples = null; slidingSample.ClearSamples();
base.OnFree(); base.OnFree();
} }
} }

View File

@ -15,13 +15,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public partial class DrawableHoldNoteTail : DrawableNote public partial class DrawableHoldNoteTail : DrawableNote
{ {
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
private const double release_window_lenience = 1.5;
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail; protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
@ -40,14 +33,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true); public void UpdateResult() => base.UpdateResult(true);
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
Debug.Assert(HitObject.HitWindows != null); Debug.Assert(HitObject.HitWindows != null);
// Factor in the release lenience // Factor in the release lenience
timeOffset /= release_window_lenience; timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
if (!userTriggered) if (!userTriggered)
{ {

View File

@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary> /// </summary>
public TailNote Tail { get; private set; } public TailNote Tail { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
/// <summary> /// <summary>
/// The time between ticks of this hold. /// The time between ticks of this hold.
/// </summary> /// </summary>

View File

@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{ {
public class TailNote : Note public class TailNote : Note
{ {
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
public const double RELEASE_WINDOW_LENIENCE = 1.5;
public override Judgement CreateJudgement() => new ManiaJudgement(); public override Judgement CreateJudgement() => new ManiaJudgement();
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * RELEASE_WINDOW_LENIENCE;
} }
} }

View File

@ -5,11 +5,11 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; 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.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon namespace osu.Game.Rulesets.Mania.Skinning.Argon
@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly Box colouredBox; private readonly Box shadeBackground;
private readonly Box shadow; private readonly Box shadeForeground;
public ArgonHoldNoteTailPiece() public ArgonHoldNoteTailPiece()
{ {
@ -32,32 +32,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
shadow = new Box shadeBackground = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
new Container new Container
{ {
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 0.82f, Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
Masking = true, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
CornerRadius = ArgonNotePiece.CORNER_RADIUS, CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
colouredBox = new Box shadeForeground = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
} },
} },
},
new Circle
{
RelativeSizeAxes = Axes.X,
Height = ArgonNotePiece.CORNER_RADIUS * 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}, },
}; };
} }
@ -77,19 +70,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction) private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{ {
colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1);
? Anchor.TopCentre
: Anchor.BottomCentre;
} }
private void onAccentChanged(ValueChangedEvent<Color4> accent) private void onAccentChanged(ValueChangedEvent<Color4> accent)
{ {
colouredBox.Colour = ColourInfo.GradientVertical( shadeBackground.Colour = accent.NewValue.Darken(1.7f);
accent.NewValue, shadeForeground.Colour = accent.NewValue.Darken(1.1f);
accent.NewValue.Darken(0.1f)
);
shadow.Colour = accent.NewValue.Darken(0.5f);
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
internal partial class ArgonNotePiece : CompositeDrawable internal partial class ArgonNotePiece : CompositeDrawable
{ {
public const float NOTE_HEIGHT = 42; public const float NOTE_HEIGHT = 42;
public const float NOTE_ACCENT_RATIO = 0.82f;
public const float CORNER_RADIUS = 3.4f; public const float CORNER_RADIUS = 3.4f;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 0.82f, Height = NOTE_ACCENT_RATIO,
Masking = true, Masking = true,
CornerRadius = CORNER_RADIUS, CornerRadius = CORNER_RADIUS,
Children = new Drawable[] Children = new Drawable[]
@ -95,6 +95,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
? Anchor.TopCentre ? Anchor.TopCentre
: Anchor.BottomCentre; : Anchor.BottomCentre;
Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1);
} }
private void onAccentChanged(ValueChangedEvent<Color4> accent) private void onAccentChanged(ValueChangedEvent<Color4> accent)

View File

@ -2,12 +2,15 @@
// 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;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -34,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable? lightContainer; private Drawable? lightContainer;
private Drawable? light; private Drawable? light;
private LegacyNoteBodyStyle? bodyStyle;
public LegacyBodyPiece() public LegacyBodyPiece()
{ {
@ -80,7 +84,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}; };
} }
bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => bodyStyle = skin.GetConfig<ManiaSkinConfigurationLookup, LegacyNoteBodyStyle>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.NoteBodyStyle))?.Value;
var wrapMode = bodyStyle == LegacyNoteBodyStyle.Stretch ? WrapMode.ClampToEdge : WrapMode.Repeat;
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d =>
{ {
if (d == null) if (d == null)
return; return;
@ -91,15 +102,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
d.Anchor = Anchor.TopCentre; d.Anchor = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both; d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One; d.Size = Vector2.One;
d.FillMode = FillMode.Stretch; // Todo: Wrap?
// Todo: Wrap
}); });
if (bodySprite != null) if (bodySprite != null)
InternalChild = bodySprite; InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -161,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null) if (bodySprite != null)
{ {
bodySprite.Origin = Anchor.BottomCentre; bodySprite.Origin = Anchor.BottomCentre;
bodySprite.Scale = new Vector2(1, -1); bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y) * -1);
} }
if (light != null) if (light != null)
@ -172,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null) if (bodySprite != null)
{ {
bodySprite.Origin = Anchor.TopCentre; bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = Vector2.One; bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y));
} }
if (light != null) if (light != null)
@ -203,6 +210,29 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
base.Update(); base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime; missFadeTime.Value ??= holdNote.HoldBrokenTime;
// here we go...
switch (bodyStyle)
{
case LegacyNoteBodyStyle.Stretch:
// this is how lazer works by default. nothing required.
break;
default:
// this is where things get fucked up.
// honestly there's three modes to handle here but they seem really pointless?
// let's wait to see if anyone actually uses them in skins.
if (bodySprite != null)
{
var sprite = bodySprite as Sprite ?? bodySprite.ChildrenOfType<Sprite>().Single();
bodySprite.FillMode = FillMode.Stretch;
// i dunno this looks about right??
bodySprite.Scale = new Vector2(1, 32800 / sprite.DrawHeight);
}
break;
}
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public partial class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene public partial class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
{ {
private Slider slider; private Slider slider;
private PathControlPointVisualiser visualiser; private PathControlPointVisualiser<Slider> visualiser;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPathType(3, null); assertControlPointPathType(3, null);
} }
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre

View File

@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
} }
private void assertSelectionCount(int count) => private void assertSelectionCount(int count) =>
AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == count); AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == count);
private void assertSelected(int index) => private void assertSelected(int index) =>
AddAssert($"{(index + 1).ToOrdinalWords()} control point piece selected", AddAssert($"{(index + 1).ToOrdinalWords()} control point piece selected",
() => this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value); () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value);
private void moveMouseToRelativePosition(Vector2 relativePosition) => private void moveMouseToRelativePosition(Vector2 relativePosition) =>
AddStep($"move mouse to {relativePosition}", () => AddStep($"move mouse to {relativePosition}", () =>
@ -202,12 +202,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
moveMouseToControlPoint(2); moveMouseToControlPoint(2);
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
addMovementStep(new Vector2(450, 50)); addMovementStep(new Vector2(450, 50));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
assertControlPointPosition(2, new Vector2(450, 50)); assertControlPointPosition(2, new Vector2(450, 50));
assertControlPointType(2, PathType.PerfectCurve); assertControlPointType(2, PathType.PerfectCurve);
@ -236,12 +236,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
moveMouseToControlPoint(3); moveMouseToControlPoint(3);
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
addMovementStep(new Vector2(550, 50)); addMovementStep(new Vector2(550, 50));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
// note: if the head is part of the selection being moved, the entire slider is moved. // note: if the head is part of the selection being moved, the entire slider is moved.
// the unselected nodes will therefore change position relative to the slider head. // the unselected nodes will therefore change position relative to the slider head.
@ -354,7 +354,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public new SliderBodyPiece BodyPiece => base.BodyPiece; public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(Slider slider) public TestSliderBlueprint(Slider slider)
: base(slider) : base(slider)

View File

@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public new SliderBodyPiece BodyPiece => base.BodyPiece; public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(Slider slider) public TestSliderBlueprint(Slider slider)
: base(slider) : base(slider)

View File

@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test] [Test]
public void TestMovingUnsnappedSliderNodesSnaps() public void TestMovingUnsnappedSliderNodesSnaps()
{ {
PathControlPointPiece sliderEnd = null; PathControlPointPiece<Slider> sliderEnd = null;
assertSliderSnapped(false); assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select slider end", () => AddStep("select slider end", () =>
{ {
sliderEnd = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last()); sliderEnd = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last());
InputManager.MoveMouseTo(sliderEnd.ScreenSpaceDrawQuad.Centre); InputManager.MoveMouseTo(sliderEnd.ScreenSpaceDrawQuad.Centre);
}); });
AddStep("move slider end", () => AddStep("move slider end", () =>
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to new point location", () => AddStep("move mouse to new point location", () =>
{ {
var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]); var firstPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
var pos = slider.Path.PositionAt(0.25d) + slider.Position; var pos = slider.Path.PositionAt(0.25d) + slider.Position;
InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos)); InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
}); });
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to second control point", () => AddStep("move mouse to second control point", () =>
{ {
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]); var secondPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
InputManager.MoveMouseTo(secondPiece); InputManager.MoveMouseTo(secondPiece);
}); });
AddStep("quick delete", () => AddStep("quick delete", () =>

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First(); => Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
private Slider? slider; private Slider? slider;
private PathControlPointVisualiser? visualiser; private PathControlPointVisualiser<Slider>? visualiser;
private const double split_gap = 100; private const double split_gap = 100;
@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select added slider", () => AddStep("select added slider", () =>
{ {
EditorBeatmap.SelectedHitObjects.Add(slider); EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First(); visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First();
}); });
moveMouseToControlPoint(2); moveMouseToControlPoint(2);
@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select added slider", () => AddStep("select added slider", () =>
{ {
EditorBeatmap.SelectedHitObjects.Add(slider); EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First(); visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First();
}); });
moveMouseToControlPoint(2); moveMouseToControlPoint(2);
@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select added slider", () => AddStep("select added slider", () =>
{ {
EditorBeatmap.SelectedHitObjects.Add(slider); EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First(); visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First();
}); });
moveMouseToControlPoint(2); moveMouseToControlPoint(2);

View File

@ -5,9 +5,11 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
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;
@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
private int depthIndex; private int depthIndex;
[Resolved]
private OsuConfigManager config { get; set; }
[Test] [Test]
public void TestHits() public void TestHits()
{ {
@ -56,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150))); AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150)));
} }
[Test]
public void TestHitLighting()
{
AddToggleStep("toggle hit lighting", v => config.SetValue(OsuSetting.HitLighting, v));
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{ {
var playfield = new TestOsuPlayfield(); var playfield = new TestOsuPlayfield();

View File

@ -4,29 +4,38 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
[TestFixture] [TestFixture]
public partial class TestSceneHitCircleKiai : TestSceneHitCircle public partial class TestSceneHitCircleKiai : TestSceneHitCircle, IBeatSyncProvider
{ {
private ControlPointInfo controlPoints { get; set; }
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
var controlPointInfo = new ControlPointInfo(); controlPoints = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); controlPoints.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{ {
ControlPointInfo = controlPointInfo ControlPointInfo = controlPoints
}); });
// track needs to be playing for BeatSyncedContainer to work. // track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start(); Beatmap.Value.Track.Start();
}); });
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => new ChannelAmplitudes();
ControlPointInfo IBeatSyncProvider.ControlPoints => controlPoints;
IClock IBeatSyncProvider.Clock => Clock;
} }
} }

View File

@ -13,7 +13,14 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -22,7 +29,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
[TestFixture] [TestFixture]
public partial class TestSceneTouchInput : OsuManualInputManagerTestScene public partial class TestSceneOsuTouchInput : OsuManualInputManagerTestScene
{ {
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -33,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private OsuInputManager osuInputManager = null!; private OsuInputManager osuInputManager = null!;
private Container mainContent = null!;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
@ -44,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo) osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
{ {
Child = new Container Child = mainContent = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -54,13 +63,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Depth = float.MinValue,
X = -100, X = -100,
}, },
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Depth = float.MinValue,
X = 100, X = 100,
},
new OsuCursorContainer
{
Depth = float.MinValue,
} }
}, },
} }
@ -70,6 +85,40 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
} }
[Test]
public void TestStreamInputVisual()
{
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
beginTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
endTouch(TouchSource.Touch1);
int i = 0;
AddRepeatStep("Alternate", () =>
{
TouchSource down = i % 2 == 0 ? TouchSource.Touch3 : TouchSource.Touch4;
TouchSource up = i % 2 == 0 ? TouchSource.Touch4 : TouchSource.Touch3;
// sometimes the user will end the previous touch before touching again, sometimes not.
if (RNG.NextBool())
{
InputManager.BeginTouch(new Touch(down, getSanePositionForSource(down)));
InputManager.EndTouch(new Touch(up, getSanePositionForSource(up)));
}
else
{
InputManager.EndTouch(new Touch(up, getSanePositionForSource(up)));
InputManager.BeginTouch(new Touch(down, getSanePositionForSource(down)));
}
i++;
}, 100);
}
[Test] [Test]
public void TestSimpleInput() public void TestSimpleInput()
{ {
@ -116,9 +165,224 @@ namespace osu.Game.Rulesets.Osu.Tests
endTouch(TouchSource.Touch2); endTouch(TouchSource.Touch2);
checkPosition(TouchSource.Touch2); checkPosition(TouchSource.Touch2);
// note that touch1 was never ended, but becomes active for tracking again. // note that touch1 was never ended, but is no longer valid for touch input due to touch 2 occurring.
beginTouch(TouchSource.Touch1); beginTouch(TouchSource.Touch1);
checkPosition(TouchSource.Touch2);
}
[Test]
public void TestStreamInput()
{
// In this scenario, the user is tapping on the first object in a stream,
// then using one or two fingers in empty space to continue the stream.
addHitCircleAt(TouchSource.Touch1);
beginTouch(TouchSource.Touch1);
// The first touch is handled as normal.
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1); checkPosition(TouchSource.Touch1);
// The second touch should release the first, and also act as a right button.
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
// Importantly, this is different from the simple case because an object was interacted with in the first touch, but not the second touch.
// left button is automatically released.
checkNotPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Also importantly, the positional part of the second touch is ignored.
checkPosition(TouchSource.Touch1);
// In this scenario, a third touch should be allowed, and handled similarly to the second.
beginTouch(TouchSource.Touch3);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Position is still ignored.
checkPosition(TouchSource.Touch1);
endTouch(TouchSource.Touch2);
checkPressed(OsuAction.LeftButton);
checkNotPressed(OsuAction.RightButton);
// Position is still ignored.
checkPosition(TouchSource.Touch1);
// User continues streaming
beginTouch(TouchSource.Touch2);
assertKeyCounter(2, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Position is still ignored.
checkPosition(TouchSource.Touch1);
// In this mode a maximum of three touches should be supported.
// A fourth touch should result in no changes anywhere.
beginTouch(TouchSource.Touch4);
assertKeyCounter(2, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch1);
endTouch(TouchSource.Touch4);
}
[Test]
public void TestStreamInputWithInitialTouchDownLeft()
{
// In this scenario, the user is wanting to use stream input but we start with one finger still on the screen.
// That finger is mapped to a left action.
addHitCircleAt(TouchSource.Touch2);
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
// hits circle as right action
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
endTouch(TouchSource.Touch1);
checkNotPressed(OsuAction.LeftButton);
// stream using other two fingers while touch2 tracks
beginTouch(TouchSource.Touch1);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
// right button is automatically released
checkNotPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
beginTouch(TouchSource.Touch3);
assertKeyCounter(2, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
endTouch(TouchSource.Touch1);
checkNotPressed(OsuAction.LeftButton);
beginTouch(TouchSource.Touch1);
assertKeyCounter(3, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
}
[Test]
public void TestStreamInputWithInitialTouchDownRight()
{
// In this scenario, the user is wanting to use stream input but we start with one finger still on the screen.
// That finger is mapped to a right action.
beginTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
endTouch(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch1);
// hits circle as left action
beginTouch(TouchSource.Touch1);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch1);
endTouch(TouchSource.Touch2);
// stream using other two fingers while touch1 tracks
beginTouch(TouchSource.Touch2);
assertKeyCounter(2, 2);
checkPressed(OsuAction.RightButton);
// left button is automatically released
checkNotPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch3);
assertKeyCounter(3, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch1);
endTouch(TouchSource.Touch2);
checkNotPressed(OsuAction.RightButton);
beginTouch(TouchSource.Touch2);
assertKeyCounter(3, 3);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch1);
}
[Test]
public void TestNonStreamOverlappingDirectTouchesWithRelease()
{
// In this scenario, the user is tapping on three circles directly while correctly releasing the first touch.
// All three should be recognised.
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
addHitCircleAt(TouchSource.Touch3);
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
endTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch3);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch3);
}
[Test]
public void TestNonStreamOverlappingDirectTouchesWithoutRelease()
{
// In this scenario, the user is tapping on three circles directly without releasing any touches.
// The first two should be recognised, but a third should not (as the user already has two fingers down).
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
addHitCircleAt(TouchSource.Touch3);
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
beginTouch(TouchSource.Touch3);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch3);
} }
[Test] [Test]
@ -263,6 +527,22 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1); assertKeyCounter(1, 1);
} }
private void addHitCircleAt(TouchSource source)
{
AddStep($"Add circle at {source}", () =>
{
var hitCircle = new HitCircle();
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
mainContent.Add(new DrawableHitCircle(hitCircle)
{
Clock = new FramedClock(new ManualClock()),
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
});
});
}
private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) => private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) =>
AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source)))); AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source))));

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.2" /> <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -8,34 +8,36 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
/// <summary> /// <summary>
/// A visualisation of the line between two <see cref="PathControlPointPiece"/>s. /// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s.
/// </summary> /// </summary>
public partial class PathControlPointConnectionPiece : CompositeDrawable /// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam>
public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath
{ {
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
private readonly Path path; private readonly Path path;
private readonly Slider slider; private readonly T hitObject;
public int ControlPointIndex { get; set; } public int ControlPointIndex { get; set; }
private IBindable<Vector2> sliderPosition; private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion; private IBindable<int> pathVersion;
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
{ {
this.slider = slider; this.hitObject = hitObject;
ControlPointIndex = controlPointIndex; ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
ControlPoint = slider.Path.ControlPoints[controlPointIndex]; ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
InternalChild = path = new SmoothPath InternalChild = path = new SmoothPath
{ {
@ -48,10 +50,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
base.LoadComplete(); base.LoadComplete();
sliderPosition = slider.PositionBindable.GetBoundCopy(); hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateConnectingPath()); hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
pathVersion = slider.Path.Version.GetBoundCopy(); pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updateConnectingPath()); pathVersion.BindValueChanged(_ => updateConnectingPath());
updateConnectingPath(); updateConnectingPath();
@ -62,16 +64,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
private void updateConnectingPath() private void updateConnectingPath()
{ {
Position = slider.StackedPosition + ControlPoint.Position; Position = hitObject.StackedPosition + ControlPoint.Position;
path.ClearVertices(); path.ClearVertices();
int nextIndex = ControlPointIndex + 1; int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count)
return; return;
path.AddVertex(Vector2.Zero); path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position - ControlPoint.Position); path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
} }

View File

@ -29,11 +29,13 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
/// <summary> /// <summary>
/// A visualisation of a single <see cref="PathControlPoint"/> in a <see cref="Slider"/>. /// A visualisation of a single <see cref="PathControlPoint"/> in an osu hit object with a path.
/// </summary> /// </summary>
public partial class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip /// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointPiece{T}"/> visualises.</typeparam>
public partial class PathControlPointPiece<T> : BlueprintPiece<T>, IHasTooltip
where T : OsuHitObject, IHasPath
{ {
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; public Action<PathControlPointPiece<T>, MouseButtonEvent> RequestSelection;
public Action<PathControlPoint> DragStarted; public Action<PathControlPoint> DragStarted;
public Action<DragEvent> DragInProgress; public Action<DragEvent> DragInProgress;
@ -44,34 +46,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly BindableBool IsSelected = new BindableBool(); public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
private readonly Slider slider; private readonly T hitObject;
private readonly Container marker; private readonly Container marker;
private readonly Drawable markerRing; private readonly Drawable markerRing;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private IBindable<Vector2> sliderPosition; private IBindable<Vector2> hitObjectPosition;
private IBindable<float> sliderScale; private IBindable<float> hitObjectScale;
[UsedImplicitly] [UsedImplicitly]
private readonly IBindable<int> sliderVersion; private readonly IBindable<int> hitObjectVersion;
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
{ {
this.slider = slider; this.hitObject = hitObject;
ControlPoint = controlPoint; ControlPoint = controlPoint;
// we don't want to run the path type update on construction as it may inadvertently change the slider. // we don't want to run the path type update on construction as it may inadvertently change the hit object.
cachePoints(slider); cachePoints(hitObject);
sliderVersion = slider.Path.Version.GetBoundCopy(); hitObjectVersion = hitObject.Path.Version.GetBoundCopy();
// schedule ensure that updates are only applied after all operations from a single frame are applied. // schedule ensure that updates are only applied after all operations from a single frame are applied.
// this avoids inadvertently changing the slider path type for batch operations. // this avoids inadvertently changing the hit object path type for batch operations.
sliderVersion.BindValueChanged(_ => Scheduler.AddOnce(() => hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
{ {
cachePoints(slider); cachePoints(hitObject);
updatePathType(); updatePathType();
})); }));
@ -120,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
base.LoadComplete(); base.LoadComplete();
sliderPosition = slider.PositionBindable.GetBoundCopy(); hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateMarkerDisplay()); hitObjectPosition.BindValueChanged(_ => updateMarkerDisplay());
sliderScale = slider.ScaleBindable.GetBoundCopy(); hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
sliderScale.BindValueChanged(_ => updateMarkerDisplay()); hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
IsSelected.BindValueChanged(_ => updateMarkerDisplay()); IsSelected.BindValueChanged(_ => updateMarkerDisplay());
@ -212,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke(); protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint); private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
/// <summary> /// <summary>
/// Handles correction of invalid path types. /// Handles correction of invalid path types.
@ -239,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
private void updateMarkerDisplay() private void updateMarkerDisplay()
{ {
Position = slider.StackedPosition + ControlPoint.Position; Position = hitObject.StackedPosition + ControlPoint.Position;
markerRing.Alpha = IsSelected.Value ? 1 : 0; markerRing.Alpha = IsSelected.Value ? 1 : 0;
@ -249,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
colour = colour.Lighten(1); colour = colour.Lighten(1);
marker.Colour = colour; marker.Colour = colour;
marker.Scale = new Vector2(slider.Scale); marker.Scale = new Vector2(hitObject.Scale);
} }
private Color4 getColourFromNodeType() private Color4 getColourFromNodeType()

View File

@ -29,15 +29,16 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath
{ {
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
internal readonly Container<PathControlPointPiece> Pieces; internal readonly Container<PathControlPointPiece<T>> Pieces;
internal readonly Container<PathControlPointConnectionPiece> Connections; internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>(); private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly Slider slider; private readonly T hitObject;
private readonly bool allowSelection; private readonly bool allowSelection;
private InputManager inputManager; private InputManager inputManager;
@ -48,17 +49,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
public PathControlPointVisualiser(Slider slider, bool allowSelection) public PathControlPointVisualiser(T hitObject, bool allowSelection)
{ {
this.slider = slider; this.hitObject = hitObject;
this.allowSelection = allowSelection; this.allowSelection = allowSelection;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both }, Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both },
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both } Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
}; };
} }
@ -69,12 +70,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
controlPoints.CollectionChanged += onControlPointsChanged; controlPoints.CollectionChanged += onControlPointsChanged;
controlPoints.BindTo(slider.Path.ControlPoints); controlPoints.BindTo(hitObject.Path.ControlPoints);
} }
/// <summary> /// <summary>
/// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>, /// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece"/>s. /// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
/// </summary> /// </summary>
public void SetSelectionTo(PathControlPoint pathControlPoint) public void SetSelectionTo(PathControlPoint pathControlPoint)
{ {
@ -124,8 +125,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return true; return true;
} }
private bool isSplittable(PathControlPointPiece p) => private bool isSplittable(PathControlPointPiece<T> p) =>
// A slider can only be split on control points which connect two different slider segments. // A hit object can only be split on control points which connect two different path segments.
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault(); p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
@ -150,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
var point = (PathControlPoint)e.NewItems[i]; var point = (PathControlPoint)e.NewItems[i];
Pieces.Add(new PathControlPointPiece(slider, point).With(d => Pieces.Add(new PathControlPointPiece<T>(hitObject, point).With(d =>
{ {
if (allowSelection) if (allowSelection)
d.RequestSelection = selectionRequested; d.RequestSelection = selectionRequested;
@ -160,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
d.DragEnded = dragEnded; d.DragEnded = dragEnded;
})); }));
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
} }
break; break;
@ -219,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
} }
private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e) private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
{ {
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
piece.IsSelected.Toggle(); piece.IsSelected.Toggle();
@ -234,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
/// <param name="piece">The control point piece that we want to change the path type of.</param> /// <param name="piece">The control point piece that we want to change the path type of.</param>
/// <param name="type">The path type we want to assign to the given control point piece.</param> /// <param name="type">The path type we want to assign to the given control point piece.</param>
private void updatePathType(PathControlPointPiece piece, PathType? type) private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
{ {
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
@ -252,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
break; break;
} }
slider.Path.ExpectedDistance.Value = null; hitObject.Path.ExpectedDistance.Value = null;
piece.ControlPoint.Type = type; piece.ControlPoint.Type = type;
} }
@ -268,9 +269,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private void dragStarted(PathControlPoint controlPoint) private void dragStarted(PathControlPoint controlPoint)
{ {
dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray(); dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray(); dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray();
draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint); draggedControlPointIndex = hitObject.Path.ControlPoints.IndexOf(controlPoint);
selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint)); selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint));
Debug.Assert(draggedControlPointIndex >= 0); Debug.Assert(draggedControlPointIndex >= 0);
@ -280,25 +281,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private void dragInProgress(DragEvent e) private void dragInProgress(DragEvent e)
{ {
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray(); Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position; var oldPosition = hitObject.Position;
double oldStartTime = slider.StartTime; double oldStartTime = hitObject.StartTime;
if (selectedControlPoints.Contains(slider.Path.ControlPoints[0])) if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
{ {
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition); var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position; Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
slider.Position += movementDelta; hitObject.Position += movementDelta;
slider.StartTime = result?.Time ?? slider.StartTime; hitObject.StartTime = result?.Time ?? hitObject.StartTime;
for (int i = 1; i < slider.Path.ControlPoints.Count; i++) for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
{ {
var controlPoint = slider.Path.ControlPoints[i]; var controlPoint = hitObject.Path.ControlPoints[i];
// Since control points are relative to the position of the slider, all points that are _not_ selected // Since control points are relative to the position of the hit object, all points that are _not_ selected
// need to be offset _back_ by the delta corresponding to the movement of the head point. // need to be offset _back_ by the delta corresponding to the movement of the head point.
// All other selected control points (if any) will move together with the head point // All other selected control points (if any) will move together with the head point
// (and so they will not move at all, relative to each other). // (and so they will not move at all, relative to each other).
@ -310,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition)); var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - slider.Position; Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
for (int i = 0; i < controlPoints.Count; ++i) for (int i = 0; i < controlPoints.Count; ++i)
{ {
@ -321,23 +322,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
// Snap the path to the current beat divisor before checking length validity. // Snap the path to the current beat divisor before checking length validity.
slider.SnapTo(snapProvider); hitObject.SnapTo(snapProvider);
if (!slider.Path.HasValidLength) if (!hitObject.Path.HasValidLength)
{ {
for (int i = 0; i < slider.Path.ControlPoints.Count; i++) for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i]; hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition; hitObject.Position = oldPosition;
slider.StartTime = oldStartTime; hitObject.StartTime = oldStartTime;
// Snap the path length again to undo the invalid length. // Snap the path length again to undo the invalid length.
slider.SnapTo(snapProvider); hitObject.SnapTo(snapProvider);
return; return;
} }
// Maintain the path types in case they got defaulted to bezier at some point during the drag. // Maintain the path types in case they got defaulted to bezier at some point during the drag.
for (int i = 0; i < slider.Path.ControlPoints.Count; i++) for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Type = dragPathTypes[i]; hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
} }
private void dragEnded() => changeHandler?.EndChange(); private void dragEnded() => changeHandler?.EndChange();

View File

@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
public Vector2 PathStartLocation => body.PathOffset; public Vector2 PathStartLocation => body.PathOffset;
/// <summary>
/// Offset in absolute (local) coordinates from the end of the curve.
/// </summary>
public Vector2 PathEndLocation => body.PathEndOffset;
public SliderBodyPiece() public SliderBodyPiece()
{ {
InternalChild = body = new ManualSliderBody InternalChild = body = new ManualSliderBody

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private SliderBodyPiece bodyPiece; private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece; private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece; private HitCirclePiece tailCirclePiece;
private PathControlPointVisualiser controlPointVisualiser; private PathControlPointVisualiser<Slider> controlPointVisualiser;
private InputManager inputManager; private InputManager inputManager;
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(), bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(), headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(),
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false) controlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, false)
}; };
setState(SliderPlacementState.Initial); setState(SliderPlacementState.Initial);

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected SliderCircleOverlay TailOverlay { get; private set; } protected SliderCircleOverlay TailOverlay { get; private set; }
[CanBeNull] [CanBeNull]
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
if (ControlPointVisualiser == null) if (ControlPointVisualiser == null)
{ {
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true) AddInternal(ControlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, true)
{ {
RemoveControlPointsRequested = removeControlPoints, RemoveControlPointsRequested = removeControlPoints,
SplitControlPointsRequested = splitControlPoints SplitControlPointsRequested = splitControlPoints
@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset) public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
{
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
};
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;

View File

@ -187,28 +187,19 @@ namespace osu.Game.Rulesets.Osu.Edit
if (b.IsSelected) if (b.IsSelected)
continue; continue;
var hitObject = (OsuHitObject)b.Item; var snapPositions = b.ScreenSpaceSnapPoints;
Vector2? snap = checkSnap(hitObject.Position); if (!snapPositions.Any())
if (snap == null && hitObject.Position != hitObject.EndPosition) continue;
snap = checkSnap(hitObject.EndPosition);
if (snap != null) var closestSnapPosition = snapPositions.MinBy(p => Vector2.Distance(p, screenSpacePosition));
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{ {
// only return distance portion, since time is not really valid // only return distance portion, since time is not really valid
snapResult = new SnapResult(snap.Value, null, playfield); snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true; return true;
} }
Vector2? checkSnap(Vector2 checkPos)
{
Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos);
if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius)
return checkScreenPos;
return null;
}
} }
snapResult = null; snapResult = null;

View File

@ -0,0 +1,14 @@
// 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.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAccuracyChallenge : ModAccuracyChallenge
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PathVersion.UnbindFrom(HitObject.Path.Version); PathVersion.UnbindFrom(HitObject.Path.Version);
slidingSample.Samples = null; slidingSample?.ClearSamples();
} }
protected override void LoadSamples() protected override void LoadSamples()

View File

@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.OnFree(); base.OnFree();
spinningSample.Samples = null; spinningSample.ClearSamples();
} }
protected override void LoadSamples() protected override void LoadSamples()

View File

@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
} }
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
/// <summary> /// <summary>
/// Apply a judgement result. /// Apply a judgement result.
/// </summary> /// </summary>

View File

@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime } ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime }); : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
} }
} }

View File

@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SpinnerTick : OsuHitObject public class SpinnerTick : OsuHitObject
{ {
/// <summary>
/// Duration of the <see cref="Spinner"/> containing this spinner tick.
/// </summary>
public double SpinnerDuration { get; set; }
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override double MaximumJudgementOffset => SpinnerDuration;
public class OsuSpinnerTickJudgement : OsuJudgement public class OsuSpinnerTickJudgement : OsuJudgement
{ {
public override HitResult MaxResult => HitResult.SmallBonus; public override HitResult MaxResult => HitResult.SmallBonus;

View File

@ -9,8 +9,10 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
{ {
@ -40,6 +42,13 @@ namespace osu.Game.Rulesets.Osu
protected override KeyBindingContainer<OsuAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected override KeyBindingContainer<OsuAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new OsuKeyBindingContainer(ruleset, variant, unique); => new OsuKeyBindingContainer(ruleset, variant, unique);
public bool CheckScreenSpaceActionPressJudgeable(Vector2 screenSpacePosition) =>
// This is a very naive but simple approach.
//
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition));
public OsuInputManager(RulesetInfo ruleset) public OsuInputManager(RulesetInfo ruleset)
: base(ruleset, 0, SimultaneousBindingMode.Unique) : base(ruleset, 0, SimultaneousBindingMode.Unique)
{ {

View File

@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(), new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking() new OsuModStrictTracking(),
new OsuModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -43,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly FlashPiece flash; private readonly FlashPiece flash;
private readonly Container kiaiContainer;
private Bindable<bool> configHitLighting = null!;
[Resolved] [Resolved]
private DrawableHitObject drawableObject { get; set; } = null!; private DrawableHitObject drawableObject { get; set; } = null!;
@ -64,24 +68,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
outerGradient = new Circle // renders the outer bright gradient outerGradient = new Circle // renders the outer bright gradient
{ {
Size = new Vector2(OUTER_GRADIENT_SIZE), Size = new Vector2(OUTER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
innerGradient = new Circle // renders the inner bright gradient innerGradient = new Circle // renders the inner bright gradient
{ {
Size = new Vector2(INNER_GRADIENT_SIZE), Size = new Vector2(INNER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
innerFill = new Circle // renders the inner dark fill innerFill = new Circle // renders the inner dark fill
{ {
Size = new Vector2(INNER_FILL_SIZE), Size = new Vector2(INNER_FILL_SIZE),
Alpha = 1,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
kiaiContainer = new CircularContainer
{
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = Size,
Child = new KiaiFlash
{
RelativeSizeAxes = Axes.Both,
}
},
number = new OsuSpriteText number = new OsuSpriteText
{ {
Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold), Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
@ -96,12 +108,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
var drawableOsuObject = (DrawableOsuHitObject)drawableObject; var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
configHitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -117,20 +131,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
outerGradient.ClearTransforms(targetMember: nameof(Colour)); outerGradient.ClearTransforms(targetMember: nameof(Colour));
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f)); outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
kiaiContainer.Colour = colour.NewValue;
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4); outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f)); innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue; flash.Colour = colour.NewValue;
// Accent colour may be changed many times during a paused gameplay state. // Accent colour may be changed many times during a paused gameplay state.
// Schedule the change to avoid transforms piling up. // Schedule the change to avoid transforms piling up.
Scheduler.AddOnce(updateStateTransforms); Scheduler.AddOnce(() =>
{
ApplyTransformsAt(double.MinValue, true);
ClearTransformsAfter(double.MinValue, true);
updateStateTransforms(drawableObject, drawableObject.State.Value);
});
}, true); }, true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms; drawableObject.ApplyCustomUpdateState += updateStateTransforms;
} }
private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value);
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
@ -140,7 +159,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
case ArmedState.Hit: case ArmedState.Hit:
// Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec. // Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
const double fade_out_time = 800; const double fade_out_time = 800;
const double flash_in_duration = 150; const double flash_in_duration = 150;
const double resize_duration = 400; const double resize_duration = 400;
@ -171,20 +189,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
// gradient layers. // gradient layers.
border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf); border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf);
// Kiai flash should track the overall size but also be cleaned up quite fast, so we don't get additional
// flashes after the hit animation is already in a mostly-completed state.
kiaiContainer.ResizeTo(Size * shrink_size, resize_duration, Easing.OutElasticHalf);
kiaiContainer.FadeOut(flash_in_duration, Easing.OutQuint);
// The outer gradient is resize with a slight delay from the border. // The outer gradient is resize with a slight delay from the border.
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close. // This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
using (BeginDelayedSequence(flash_in_duration / 12)) using (BeginDelayedSequence(flash_in_duration / 12))
{ {
outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf); outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf);
outerGradient outerGradient
.FadeColour(Color4.White, 80) .FadeColour(Color4.White, 80)
.Then() .Then()
.FadeOut(flash_in_duration); .FadeOut(flash_in_duration);
} }
flash.FadeTo(1, flash_in_duration, Easing.OutQuint); if (configHitLighting.Value)
{
flash.HitLighting = true;
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
}
else
{
flash.HitLighting = false;
flash.FadeTo(1, flash_in_duration, Easing.OutQuint)
.Then()
.FadeOut(flash_in_duration, Easing.OutQuint);
this.FadeOut(fade_out_time * 0.8f, Easing.OutQuad);
}
this.FadeOut(fade_out_time, Easing.OutQuad);
break; break;
} }
} }
@ -215,6 +253,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Child.AlwaysPresent = true; Child.AlwaysPresent = true;
} }
public bool HitLighting { get; set; }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -223,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = Colour, Colour = Colour,
Radius = OsuHitObject.OBJECT_RADIUS * 1.2f, Radius = OsuHitObject.OBJECT_RADIUS * (HitLighting ? 1.2f : 0.6f),
}; };
} }
} }

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary> /// </summary>
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]); public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
/// <summary>
/// Offset in absolute coordinates from the end of the curve.
/// </summary>
public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
/// <summary> /// <summary>
/// Used to colour the path. /// Used to colour the path.
/// </summary> /// </summary>

View File

@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset; public override Vector2 PathOffset => snakedPathOffset;
public override Vector2 PathEndOffset => snakedPathEndOffset;
/// <summary> /// <summary>
/// The top-left position of the path when fully snaked. /// The top-left position of the path when fully snaked.
/// </summary> /// </summary>
@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary> /// </summary>
private Vector2 snakedPathOffset; private Vector2 snakedPathOffset;
/// <summary>
/// The offset of the end of path from <see cref="snakedPosition"/> when fully snaked.
/// </summary>
private Vector2 snakedPathEndOffset;
private DrawableSlider drawableSlider = null!; private DrawableSlider drawableSlider = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 0; double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0; double lastSnakedEnd = SnakedEnd ?? 0;

View File

@ -10,6 +10,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges;
using osu.Game.Configuration; using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary> /// </summary>
private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>(); private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>();
private TrackedTouch? positionTrackingTouch;
private readonly OsuInputManager osuInputManager; private readonly OsuInputManager osuInputManager;
private Bindable<bool> mouseDisabled = null!; private Bindable<bool> mouseDisabled = null!;
@ -38,6 +41,9 @@ namespace osu.Game.Rulesets.Osu.UI
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons); mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
} }
// Required to handle touches outside of the playfield when screen scaling is enabled.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override void OnTouchMove(TouchMoveEvent e) protected override void OnTouchMove(TouchMoveEvent e)
{ {
base.OnTouchMove(e); base.OnTouchMove(e);
@ -53,7 +59,15 @@ namespace osu.Game.Rulesets.Osu.UI
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future. // Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action); bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
trackedTouches.Add(new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null)); // If we can actually accept as an action, check whether this tap was on a circle's receptor.
// This case gets special handling to allow for empty-space stream tapping.
bool isDirectCircleTouch = osuInputManager.CheckScreenSpaceActionPressJudgeable(e.ScreenSpaceTouchDownPosition);
var newTouch = new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null, isDirectCircleTouch);
updatePositionTracking(newTouch);
trackedTouches.Add(newTouch);
// Important to update position before triggering the pressed action. // Important to update position before triggering the pressed action.
handleTouchMovement(e); handleTouchMovement(e);
@ -64,10 +78,47 @@ namespace osu.Game.Rulesets.Osu.UI
return true; return true;
} }
/// <summary>
/// Given a new touch, update the positional tracking state and any related operations.
/// </summary>
private void updatePositionTracking(TrackedTouch newTouch)
{
// If the new touch directly interacted with a circle's receptor, it always becomes the current touch for positional tracking.
if (newTouch.DirectTouch)
{
positionTrackingTouch = newTouch;
return;
}
// Otherwise, we only want to use the new touch for position tracking if no other touch is tracking position yet..
if (positionTrackingTouch == null)
{
positionTrackingTouch = newTouch;
return;
}
// ..or if the current position tracking touch was not a direct touch (this one is debatable and may be change in the future, but it's the simplest way to handle)
if (!positionTrackingTouch.DirectTouch)
{
positionTrackingTouch = newTouch;
return;
}
// In the case the new touch was not used for position tracking, we should also check the previous position tracking touch.
// If it was a direct touch and still has its action pressed, that action should be released.
//
// This is done to allow tracking with the initial touch while still having both Left/Right actions available for alternating with two more touches.
if (positionTrackingTouch.DirectTouch && positionTrackingTouch.Action is OsuAction directTouchAction)
{
osuInputManager.KeyBindingContainer.TriggerReleased(directTouchAction);
positionTrackingTouch.Action = null;
}
}
private void handleTouchMovement(TouchEvent touchEvent) private void handleTouchMovement(TouchEvent touchEvent)
{ {
// Movement should only be tracked for the most recent touch. // Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != trackedTouches.Last().Source) if (touchEvent.Touch.Source != positionTrackingTouch?.Source)
return; return;
if (!osuInputManager.AllowUserCursorMovement) if (!osuInputManager.AllowUserCursorMovement)
@ -83,6 +134,9 @@ namespace osu.Game.Rulesets.Osu.UI
if (tracked.Action is OsuAction action) if (tracked.Action is OsuAction action)
osuInputManager.KeyBindingContainer.TriggerReleased(action); osuInputManager.KeyBindingContainer.TriggerReleased(action);
if (positionTrackingTouch == tracked)
positionTrackingTouch = null;
trackedTouches.Remove(tracked); trackedTouches.Remove(tracked);
base.OnTouchUp(e); base.OnTouchUp(e);
@ -92,12 +146,15 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
public readonly TouchSource Source; public readonly TouchSource Source;
public readonly OsuAction? Action; public OsuAction? Action;
public TrackedTouch(TouchSource source, OsuAction? action) public readonly bool DirectTouch;
public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch)
{ {
Source = source; Source = source;
Action = action; Action = action;
DirectTouch = directTouch;
} }
} }
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("slider-conversion-v6")] [TestCase("slider-conversion-v6")]
[TestCase("slider-conversion-v14")] [TestCase("slider-conversion-v14")]
[TestCase("slider-generating-drumroll-2")] [TestCase("slider-generating-drumroll-2")]
[TestCase("file-hitsamples")]
public void Test(string name) => base.Test(name); public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -2,8 +2,11 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -14,36 +17,48 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
private DrumTouchInputArea drumTouchInputArea = null!; private DrumTouchInputArea drumTouchInputArea = null!;
[SetUpSteps] private readonly Bindable<TaikoTouchControlScheme> controlScheme = new Bindable<TaikoTouchControlScheme>();
public void SetUpSteps()
[BackgroundDependencyLoader]
private void load()
{ {
AddStep("create drum", () => var config = (TaikoRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(TaikoRulesetSetting.TouchControlScheme, controlScheme);
}
private void createDrum()
{
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{ {
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo) RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new InputDrum
Children = new Drawable[]
{ {
new InputDrum Anchor = Anchor.TopCentre,
{ Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre, Height = 0.2f,
Origin = Anchor.TopCentre,
Height = 0.2f,
},
drumTouchInputArea = new DrumTouchInputArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
}, },
}; drumTouchInputArea = new DrumTouchInputArea
}); {
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
}
};
} }
[Test] [Test]
public void TestDrum() public void TestDrum()
{ {
AddStep("create drum", createDrum);
AddStep("show drum", () => drumTouchInputArea.Show()); AddStep("show drum", () => drumTouchInputArea.Show());
AddStep("change scheme (kddk)", () => controlScheme.Value = TaikoTouchControlScheme.KDDK);
AddStep("change scheme (kkdd)", () => controlScheme.Value = TaikoTouchControlScheme.KKDD);
AddStep("change scheme (ddkk)", () => controlScheme.Value = TaikoTouchControlScheme.DDKK);
} }
protected override Ruleset CreateRuleset() => new TaikoRuleset();
} }
} }

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -0,0 +1,28 @@
// 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.Taiko.Configuration
{
public class TaikoRulesetConfigManager : RulesetConfigManager<TaikoRulesetSetting>
{
public TaikoRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{
}
protected override void InitialiseDefaults()
{
base.InitialiseDefaults();
SetDefault(TaikoRulesetSetting.TouchControlScheme, TaikoTouchControlScheme.KDDK);
}
}
public enum TaikoRulesetSetting
{
TouchControlScheme
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Taiko.Configuration
{
public enum TaikoTouchControlScheme
{
KDDK,
DDKK,
KKDD
}
}

View File

@ -1,38 +1,30 @@
// 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.Diagnostics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IApplicableToDrawableHitObject
{ {
private DrawableTaikoRuleset? drawableTaikoRuleset;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield; var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true; playfield.ClassicHitTargetPosition.Value = true;
} }
public void Update(Playfield playfield) public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{ {
Debug.Assert(drawableTaikoRuleset != null); if (drawable is DrawableTaikoHitObject hit)
hit.SnapJudgementLocation = true;
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = drawableTaikoRuleset.DrawHeight / 480;
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
} }
} }
} }

View File

@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), _ => new TickPiece()); protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), _ => new TickPiece());
public override double MaximumJudgementOffset => HitObject.HitWindow;
protected override void OnApply() protected override void OnApply()
{ {
base.OnApply(); base.OnApply();

View File

@ -207,6 +207,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
const float gravity_time = 300; const float gravity_time = 300;
const float gravity_travel_height = 200; const float gravity_travel_height = 200;
if (SnapJudgementLocation)
MainPiece.MoveToX(-X);
this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad); this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad);
this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out) this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out)

View File

@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly Container nonProxiedContent; private readonly Container nonProxiedContent;
/// <summary>
/// Whether the location of the hit should be snapped to the hit target before animating.
/// </summary>
/// <remarks>
/// This is how osu-stable worked, but notably is not how TnT works.
/// Not snapping results in less visual feedback on hit accuracy.
/// </remarks>
public bool SnapJudgementLocation { get; set; }
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject) protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override double MaximumJudgementOffset => HitWindow;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject

View File

@ -0,0 +1 @@
{"Mappings":[{"StartTime":500.0,"Objects":[{"StartTime":500.0,"EndTime":500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1000.0,"Objects":[{"StartTime":1000.0,"EndTime":1000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1500.0,"Objects":[{"StartTime":1500.0,"EndTime":1500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2000.0,"Objects":[{"StartTime":2000.0,"EndTime":2000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2500.0,"Objects":[{"StartTime":2500.0,"EndTime":2500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3000.0,"Objects":[{"StartTime":3000.0,"EndTime":3000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3500.0,"Objects":[{"StartTime":3500.0,"EndTime":3500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":4000.0,"Objects":[{"StartTime":4000.0,"EndTime":4000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]}]}

View File

@ -0,0 +1,22 @@
osu file format v14
[Difficulty]
HPDrainRate:5
CircleSize:7
OverallDifficulty:6.5
ApproachRate:10
SliderMultiplier:1.9
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
[HitObjects]
256,192,500,1,0,0:0:0:0:sample.ogg
256,192,1000,1,8,0:0:0:0:sample.ogg
256,192,1500,1,2,0:0:0:0:sample.ogg
256,192,2000,1,10,0:0:0:0:sample.ogg
256,192,2500,1,4,0:0:0:0:sample.ogg
256,192,3000,1,12,0:0:0:0:sample.ogg
256,192,3500,1,6,0:0:0:0:sample.ogg
256,192,4000,1,14,0:0:0:0:sample.ogg

View File

@ -28,9 +28,13 @@ using osu.Game.Rulesets.Taiko.Skinning.Argon;
using osu.Game.Rulesets.Taiko.Skinning.Legacy; using osu.Game.Rulesets.Taiko.Skinning.Legacy;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Overlays.Settings;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Rulesets.Configuration;
using osu.Game.Configuration;
using osu.Game.Rulesets.Taiko.Configuration;
namespace osu.Game.Rulesets.Taiko namespace osu.Game.Rulesets.Taiko
{ {
@ -144,6 +148,7 @@ namespace osu.Game.Rulesets.Taiko
new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()), new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
new TaikoModHidden(), new TaikoModHidden(),
new TaikoModFlashlight(), new TaikoModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:
@ -193,6 +198,10 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
public override RulesetSettingsSubsection CreateSettings() => new TaikoSettingsSubsection(this);
protected override IEnumerable<HitResult> GetValidHitResults() protected override IEnumerable<HitResult> GetValidHitResults()
{ {
return new[] return new[]
@ -200,9 +209,8 @@ namespace osu.Game.Rulesets.Taiko
HitResult.Great, HitResult.Great,
HitResult.Ok, HitResult.Ok,
HitResult.SmallTickHit,
HitResult.SmallBonus, HitResult.SmallBonus,
HitResult.LargeBonus,
}; };
} }
@ -211,6 +219,9 @@ namespace osu.Game.Rulesets.Taiko
switch (result) switch (result)
{ {
case HitResult.SmallBonus: case HitResult.SmallBonus:
return "drum tick";
case HitResult.LargeBonus:
return "bonus"; return "bonus";
} }

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Taiko.Configuration;
namespace osu.Game.Rulesets.Taiko
{
public partial class TaikoSettingsSubsection : RulesetSettingsSubsection
{
protected override LocalisableString Header => "osu!taiko";
public TaikoSettingsSubsection(TaikoRuleset ruleset)
: base(ruleset)
{
}
[BackgroundDependencyLoader]
private void load()
{
var config = (TaikoRulesetConfigManager)Config;
Children = new Drawable[]
{
new SettingsEnumDropdown<TaikoTouchControlScheme>
{
LabelText = "Touch control scheme",
Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme)
}
};
}
}
}

View File

@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
Direction.Value = ScrollingDirection.Left; Direction.Value = ScrollingDirection.Left;
TimeRange.Value = 7000;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
KeyBindingInputManager.Add(new DrumTouchInputArea()); KeyBindingInputManager.Add(new DrumTouchInputArea());
} }
protected override void Update()
{
base.Update();
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = DrawHeight / 480;
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();

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 System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
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;
@ -11,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Configuration;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -31,15 +34,18 @@ namespace osu.Game.Rulesets.Taiko.UI
private Container mainContent = null!; private Container mainContent = null!;
private QuarterCircle leftCentre = null!; private DrumSegment leftCentre = null!;
private QuarterCircle rightCentre = null!; private DrumSegment rightCentre = null!;
private QuarterCircle leftRim = null!; private DrumSegment leftRim = null!;
private QuarterCircle rightRim = null!; private DrumSegment rightRim = null!;
private readonly Bindable<TaikoTouchControlScheme> configTouchControlScheme = new Bindable<TaikoTouchControlScheme>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(TaikoInputManager taikoInputManager, OsuColour colours) private void load(TaikoInputManager taikoInputManager, TaikoRulesetConfigManager config)
{ {
Debug.Assert(taikoInputManager.KeyBindingContainer != null); Debug.Assert(taikoInputManager.KeyBindingContainer != null);
keyBindingContainer = taikoInputManager.KeyBindingContainer; keyBindingContainer = taikoInputManager.KeyBindingContainer;
// Container should handle input everywhere. // Container should handle input everywhere.
@ -65,27 +71,27 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
leftRim = new QuarterCircle(TaikoAction.LeftRim, colours.Blue) leftRim = new DrumSegment
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
X = -2, X = -2,
}, },
rightRim = new QuarterCircle(TaikoAction.RightRim, colours.Blue) rightRim = new DrumSegment
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
X = 2, X = 2,
Rotation = 90, Rotation = 90,
}, },
leftCentre = new QuarterCircle(TaikoAction.LeftCentre, colours.Pink) leftCentre = new DrumSegment
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
X = -2, X = -2,
Scale = new Vector2(centre_region), Scale = new Vector2(centre_region),
}, },
rightCentre = new QuarterCircle(TaikoAction.RightCentre, colours.Pink) rightCentre = new DrumSegment
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
@ -98,6 +104,17 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
}, },
}; };
config.BindWith(TaikoRulesetSetting.TouchControlScheme, configTouchControlScheme);
configTouchControlScheme.BindValueChanged(scheme =>
{
var actions = getOrderedActionsForScheme(scheme.NewValue);
leftRim.Action = actions[0];
leftCentre.Action = actions[1];
rightCentre.Action = actions[2];
rightRim.Action = actions[3];
}, true);
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
@ -119,11 +136,47 @@ namespace osu.Game.Rulesets.Taiko.UI
base.OnTouchUp(e); base.OnTouchUp(e);
} }
private static TaikoAction[] getOrderedActionsForScheme(TaikoTouchControlScheme scheme)
{
switch (scheme)
{
case TaikoTouchControlScheme.KDDK:
return new[]
{
TaikoAction.LeftRim,
TaikoAction.LeftCentre,
TaikoAction.RightCentre,
TaikoAction.RightRim
};
case TaikoTouchControlScheme.DDKK:
return new[]
{
TaikoAction.LeftCentre,
TaikoAction.RightCentre,
TaikoAction.LeftRim,
TaikoAction.RightRim
};
case TaikoTouchControlScheme.KKDD:
return new[]
{
TaikoAction.LeftRim,
TaikoAction.RightRim,
TaikoAction.LeftCentre,
TaikoAction.RightCentre
};
default:
throw new ArgumentOutOfRangeException(nameof(scheme), scheme, null);
}
}
private void handleDown(object source, Vector2 position) private void handleDown(object source, Vector2 position)
{ {
Show(); Show();
TaikoAction taikoAction = getTaikoActionFromInput(position); TaikoAction taikoAction = getTaikoActionFromPosition(position);
// Not too sure how this can happen, but let's avoid throwing. // Not too sure how this can happen, but let's avoid throwing.
if (trackedActions.ContainsKey(source)) if (trackedActions.ContainsKey(source))
@ -139,18 +192,15 @@ namespace osu.Game.Rulesets.Taiko.UI
trackedActions.Remove(source); trackedActions.Remove(source);
} }
private bool validMouse(MouseButtonEvent e) => private TaikoAction getTaikoActionFromPosition(Vector2 inputPosition)
leftRim.Contains(e.ScreenSpaceMouseDownPosition) || rightRim.Contains(e.ScreenSpaceMouseDownPosition);
private TaikoAction getTaikoActionFromInput(Vector2 inputPosition)
{ {
bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition); bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition);
bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2; bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2;
if (leftSide) if (leftSide)
return centreHit ? TaikoAction.LeftCentre : TaikoAction.LeftRim; return centreHit ? leftCentre.Action : leftRim.Action;
return centreHit ? TaikoAction.RightCentre : TaikoAction.RightRim; return centreHit ? rightCentre.Action : rightRim.Action;
} }
protected override void PopIn() protected override void PopIn()
@ -163,23 +213,42 @@ namespace osu.Game.Rulesets.Taiko.UI
mainContent.FadeOut(300); mainContent.FadeOut(300);
} }
private partial class QuarterCircle : CompositeDrawable, IKeyBindingHandler<TaikoAction> private partial class DrumSegment : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{ {
private readonly Circle overlay; private TaikoAction action;
private readonly TaikoAction handledAction; public TaikoAction Action
{
get => action;
set
{
if (action == value)
return;
private readonly Circle circle; action = value;
updateColoursFromAction();
}
}
private Circle overlay = null!;
private Circle circle = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos); public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos);
public QuarterCircle(TaikoAction handledAction, Color4 colour) public DrumSegment()
{ {
this.handledAction = handledAction;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit; FillMode = FillMode.Fit;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container new Container
@ -191,7 +260,6 @@ namespace osu.Game.Rulesets.Taiko.UI
circle = new Circle circle = new Circle
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colour.Multiply(1.4f).Darken(2.8f),
Alpha = 0.8f, Alpha = 0.8f,
Scale = new Vector2(2), Scale = new Vector2(2),
}, },
@ -200,7 +268,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Alpha = 0, Alpha = 0,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Colour = colour,
Scale = new Vector2(2), Scale = new Vector2(2),
} }
} }
@ -208,18 +275,52 @@ namespace osu.Game.Rulesets.Taiko.UI
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
updateColoursFromAction();
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e) public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{ {
if (e.Action == handledAction) if (e.Action == Action)
overlay.FadeTo(1f, 80, Easing.OutQuint); overlay.FadeTo(1f, 80, Easing.OutQuint);
return false; return false;
} }
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e) public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{ {
if (e.Action == handledAction) if (e.Action == Action)
overlay.FadeOut(1000, Easing.OutQuint); overlay.FadeOut(1000, Easing.OutQuint);
} }
private void updateColoursFromAction()
{
if (!IsLoaded)
return;
var colour = getColourFromTaikoAction(Action);
circle.Colour = colour.Multiply(1.4f).Darken(2.8f);
overlay.Colour = colour;
}
private Color4 getColourFromTaikoAction(TaikoAction handledAction)
{
switch (handledAction)
{
case TaikoAction.LeftRim:
case TaikoAction.RightRim:
return colours.Blue;
case TaikoAction.LeftCentre:
case TaikoAction.RightCentre:
return colours.Pink;
}
throw new ArgumentOutOfRangeException();
}
} }
} }
} }

View File

@ -214,7 +214,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration)); Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration));
StoryboardSprite manyTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "many-times.png"); StoryboardSprite manyTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "many-times.png");
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration)); // It is intentional that we don't consider the loop count (40) as part of the end time calculation to match stable's handling.
// If we were to include the loop count, storyboards which loop for stupid long loop counts would continue playing the outro forever.
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration));
} }
} }
} }

View File

@ -1,8 +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.
#nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
@ -12,7 +10,7 @@ using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing namespace osu.Game.Tests.Editing
{ {
[TestFixture] [TestFixture]
public class EditorChangeHandlerTest public class BeatmapEditorChangeHandlerTest
{ {
private int stateChangedFired; private int stateChangedFired;
@ -23,18 +21,23 @@ namespace osu.Game.Tests.Editing
} }
[Test] [Test]
public void TestSaveRestoreState() public void TestSaveRestoreStateUsingTransaction()
{ {
var (handler, beatmap) = createChangeHandler(); var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
addArbitraryChange(beatmap); handler.BeginChange();
handler.SaveState();
// Initial state will be saved on BeginChange
Assert.That(stateChangedFired, Is.EqualTo(1)); Assert.That(stateChangedFired, Is.EqualTo(1));
addArbitraryChange(beatmap);
handler.EndChange();
Assert.That(stateChangedFired, Is.EqualTo(2));
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
@ -43,7 +46,35 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(3));
}
[Test]
public void TestSaveRestoreState()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(2)); Assert.That(stateChangedFired, Is.EqualTo(2));
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
handler.RestoreState(-1);
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(3));
} }
[Test] [Test]
@ -54,6 +85,10 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
string originalHash = handler.CurrentStateHash; string originalHash = handler.CurrentStateHash;
addArbitraryChange(beatmap); addArbitraryChange(beatmap);
@ -61,7 +96,7 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1)); Assert.That(stateChangedFired, Is.EqualTo(2));
string hash = handler.CurrentStateHash; string hash = handler.CurrentStateHash;
@ -69,7 +104,7 @@ namespace osu.Game.Tests.Editing
handler.RestoreState(-1); handler.RestoreState(-1);
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash)); Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(2)); Assert.That(stateChangedFired, Is.EqualTo(3));
addArbitraryChange(beatmap); addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
@ -84,12 +119,16 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
addArbitraryChange(beatmap); addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1)); Assert.That(stateChangedFired, Is.EqualTo(2));
string hash = handler.CurrentStateHash; string hash = handler.CurrentStateHash;
@ -97,7 +136,7 @@ namespace osu.Game.Tests.Editing
handler.SaveState(); handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(1)); Assert.That(stateChangedFired, Is.EqualTo(2));
handler.RestoreState(-1); handler.RestoreState(-1);
@ -106,7 +145,7 @@ namespace osu.Game.Tests.Editing
// we should only be able to restore once even though we saved twice. // we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(2)); Assert.That(stateChangedFired, Is.EqualTo(3));
} }
[Test] [Test]
@ -114,11 +153,15 @@ namespace osu.Game.Tests.Editing
{ {
var (handler, beatmap) = createChangeHandler(); var (handler, beatmap) = createChangeHandler();
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{ {
Assert.That(stateChangedFired, Is.EqualTo(i)); Assert.That(stateChangedFired, Is.EqualTo(i + 1));
addArbitraryChange(beatmap); addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
@ -169,7 +212,7 @@ namespace osu.Game.Tests.Editing
}, },
}); });
var changeHandler = new EditorChangeHandler(beatmap); var changeHandler = new BeatmapEditorChangeHandler(beatmap);
changeHandler.OnStateChange += () => stateChangedFired++; changeHandler.OnStateChange += () => stateChangedFired++;
return (changeHandler, beatmap); return (changeHandler, beatmap);

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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
public class CheckPreviewTimeTest
{
private CheckPreviewTime check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckPreviewTime();
}
[Test]
public void TestPreviewTimeNotSet()
{
setNoPreviewTimeBeatmap();
var content = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(content).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckPreviewTime.IssueTemplateHasNoPreviewTime);
}
[Test]
public void TestPreviewTimeconflict()
{
setPreviewTimeConflictBeatmap();
var content = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(content).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckPreviewTime.IssueTemplatePreviewTimeConflict);
Assert.That(issues.Single().Arguments.FirstOrDefault()?.ToString() == "Test1");
}
private void setNoPreviewTimeBeatmap()
{
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { PreviewTime = -1 },
}
};
}
private void setPreviewTimeConflictBeatmap()
{
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { PreviewTime = 10 },
BeatmapSet = new BeatmapSetInfo(new List<BeatmapInfo>
{
new BeatmapInfo
{
DifficultyName = "Test1",
Metadata = new BeatmapMetadata { PreviewTime = 5 },
},
new BeatmapInfo
{
DifficultyName = "Test2",
Metadata = new BeatmapMetadata { PreviewTime = 10 },
},
})
}
};
}
}
}

View File

@ -150,6 +150,8 @@ namespace osu.Game.Tests.Rulesets
public IBindable<double> AggregateTempo => throw new NotImplementedException(); public IBindable<double> AggregateTempo => throw new NotImplementedException();
public int PlaybackConcurrency { get; set; } public int PlaybackConcurrency { get; set; }
public void AddExtension(string extension) => throw new NotImplementedException();
} }
private class TestShaderManager : ShaderManager private class TestShaderManager : ShaderManager

View File

@ -1,12 +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.
#nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -20,29 +19,36 @@ namespace osu.Game.Tests.Skins
public partial class TestSceneBeatmapSkinResources : OsuTestScene public partial class TestSceneBeatmapSkinResources : OsuTestScene
{ {
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; } = null!;
private IWorkingBeatmap beatmap; [Test]
public void TestRetrieveOggAudio()
[BackgroundDependencyLoader]
private void load()
{ {
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-beatmap.osz"), "ogg-beatmap.osz")).GetResultSafely(); IWorkingBeatmap beatmap = null!;
imported?.PerformRead(s => AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"ogg-beatmap.osz"));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"sample")) != null);
AddAssert("track is non-null", () =>
{ {
beatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]); using (var track = beatmap.LoadTrack())
return track is not TrackVirtual;
}); });
} }
[Test] [Test]
public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); public void TestRetrievalWithConflictingFilenames()
[Test]
public void TestRetrieveOggTrack() => AddAssert("track is non-null", () =>
{ {
using (var track = beatmap.LoadTrack()) IWorkingBeatmap beatmap = null!;
return track is not TrackVirtual;
}); AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"conflicting-filenames-beatmap.osz"));
AddAssert("texture is non-null", () => beatmap.Skin.GetTexture(@"spinner-osu") != null);
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
}
private IWorkingBeatmap importBeatmapFromArchives(string filename)
{
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
}
} }
} }

View File

@ -31,17 +31,24 @@ namespace osu.Game.Tests.Skins
[Resolved] [Resolved]
private SkinManager skins { get; set; } = null!; private SkinManager skins { get; set; } = null!;
private ISkin skin = null!; [Test]
public void TestRetrieveOggSample()
[BackgroundDependencyLoader]
private void load()
{ {
var imported = skins.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-skin.osk"), "ogg-skin.osk")).GetResultSafely(); ISkin skin = null!;
skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
AddStep("import skin", () => skin = importSkinFromArchives(@"ogg-skin.osk"));
AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo(@"sample")) != null);
} }
[Test] [Test]
public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null); public void TestRetrievalWithConflictingFilenames()
{
ISkin skin = null!;
AddStep("import skin", () => skin = importSkinFromArchives(@"conflicting-filenames-skin.osk"));
AddAssert("texture is non-null", () => skin.GetTexture(@"spinner-osu") != null);
AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
}
[Test] [Test]
public void TestSampleRetrievalOrder() public void TestSampleRetrievalOrder()
@ -78,6 +85,12 @@ namespace osu.Game.Tests.Skins
}); });
} }
private Skin importSkinFromArchives(string filename)
{
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
return imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
}
private class TestSkin : Skin private class TestSkin : Skin
{ {
public const string SAMPLE_NAME = "test-sample"; public const string SAMPLE_NAME = "test-sample";

View File

@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Audio
private WaveformTestBeatmap beatmap; private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider; private RoundedSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider; private RoundedSliderBar<int> highPassSlider;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Audio
Text = $"Low Pass: {lowPassFilter.Cutoff}hz", Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
lowPassSlider = new OsuSliderBar<int> lowPassSlider = new RoundedSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Audio
Text = $"High Pass: {highPassFilter.Cutoff}hz", Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
highPassSlider = new OsuSliderBar<int> highPassSlider = new RoundedSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Background
{ {
public partial class TestSceneTriangleBorderShader : OsuTestScene public partial class TestSceneTriangleBorderShader : OsuTestScene
{ {
private readonly TriangleBorder border; private readonly TestTriangle triangle;
public TestSceneTriangleBorderShader() public TestSceneTriangleBorderShader()
{ {
@ -25,11 +25,11 @@ namespace osu.Game.Tests.Visual.Background
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkGreen Colour = Color4.DarkGreen
}, },
border = new TriangleBorder triangle = new TestTriangle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(100) Size = new Vector2(200)
} }
}; };
} }
@ -38,12 +38,13 @@ namespace osu.Game.Tests.Visual.Background
{ {
base.LoadComplete(); base.LoadComplete();
AddSliderStep("Thickness", 0f, 1f, 0.02f, t => border.Thickness = t); AddSliderStep("Thickness", 0f, 1f, 0.15f, t => triangle.Thickness = t);
AddSliderStep("Texel size", 0f, 0.1f, 0f, t => triangle.TexelSize = t);
} }
private partial class TriangleBorder : Sprite private partial class TestTriangle : Sprite
{ {
private float thickness = 0.02f; private float thickness = 0.15f;
public float Thickness public float Thickness
{ {
@ -55,6 +56,18 @@ namespace osu.Game.Tests.Visual.Background
} }
} }
private float texelSize;
public float TexelSize
{
get => texelSize;
set
{
texelSize = value;
Invalidate(Invalidation.DrawNode);
}
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders, IRenderer renderer) private void load(ShaderManager shaders, IRenderer renderer)
{ {
@ -62,29 +75,32 @@ namespace osu.Game.Tests.Visual.Background
Texture = renderer.WhitePixel; Texture = renderer.WhitePixel;
} }
protected override DrawNode CreateDrawNode() => new TriangleBorderDrawNode(this); protected override DrawNode CreateDrawNode() => new TriangleDrawNode(this);
private class TriangleBorderDrawNode : SpriteDrawNode private class TriangleDrawNode : SpriteDrawNode
{ {
public new TriangleBorder Source => (TriangleBorder)base.Source; public new TestTriangle Source => (TestTriangle)base.Source;
public TriangleBorderDrawNode(TriangleBorder source) public TriangleDrawNode(TestTriangle source)
: base(source) : base(source)
{ {
} }
private float thickness; private float thickness;
private float texelSize;
public override void ApplyState() public override void ApplyState()
{ {
base.ApplyState(); base.ApplyState();
thickness = Source.thickness; thickness = Source.thickness;
texelSize = Source.texelSize;
} }
public override void Draw(IRenderer renderer) public override void Draw(IRenderer renderer)
{ {
TextureShader.GetUniform<float>("thickness").UpdateValue(ref thickness); TextureShader.GetUniform<float>("thickness").UpdateValue(ref thickness);
TextureShader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
base.Draw(renderer); base.Draw(renderer);
} }

View File

@ -5,6 +5,7 @@ using osu.Game.Graphics.Backgrounds;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK;
namespace osu.Game.Tests.Visual.Background namespace osu.Game.Tests.Visual.Background
{ {
@ -25,7 +26,10 @@ namespace osu.Game.Tests.Visual.Background
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ColourLight = Color4.White, ColourLight = Color4.White,
ColourDark = Color4.Gray ColourDark = Color4.Gray,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.9f)
} }
}; };
} }
@ -35,6 +39,8 @@ namespace osu.Game.Tests.Visual.Background
base.LoadComplete(); base.LoadComplete();
AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s);
AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s));
AddToggleStep("Masking", m => triangles.Masking = m);
} }
} }
} }

View File

@ -8,12 +8,14 @@ using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.Background namespace osu.Game.Tests.Visual.Background
{ {
public partial class TestSceneTrianglesV2Background : OsuTestScene public partial class TestSceneTrianglesV2Background : OsuTestScene
{ {
private readonly TrianglesV2 triangles; private readonly TrianglesV2 triangles;
private readonly TrianglesV2 maskedTriangles;
private readonly Box box; private readonly Box box;
public TestSceneTrianglesV2Background() public TestSceneTrianglesV2Background()
@ -31,12 +33,20 @@ namespace osu.Game.Tests.Visual.Background
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5), Spacing = new Vector2(0, 10),
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Masked"
},
new Container new Container
{ {
Size = new Vector2(500, 100), Size = new Vector2(500, 100),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true, Masking = true,
CornerRadius = 40, CornerRadius = 40,
Children = new Drawable[] Children = new Drawable[]
@ -54,9 +64,43 @@ namespace osu.Game.Tests.Visual.Background
} }
} }
}, },
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Non-masked"
},
new Container new Container
{ {
Size = new Vector2(500, 100), Size = new Vector2(500, 100),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red
},
maskedTriangles = new TrianglesV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both
}
}
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Gradient comparison box"
},
new Container
{
Size = new Vector2(500, 100),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true, Masking = true,
CornerRadius = 40, CornerRadius = 40,
Child = box = new Box Child = box = new Box
@ -75,14 +119,16 @@ namespace osu.Game.Tests.Visual.Background
AddSliderStep("Spawn ratio", 0f, 10f, 1f, s => AddSliderStep("Spawn ratio", 0f, 10f, 1f, s =>
{ {
triangles.SpawnRatio = s; triangles.SpawnRatio = maskedTriangles.SpawnRatio = s;
triangles.Reset(1234); triangles.Reset(1234);
maskedTriangles.Reset(1234);
}); });
AddSliderStep("Thickness", 0f, 1f, 0.02f, t => triangles.Thickness = t); AddSliderStep("Thickness", 0f, 1f, 0.02f, t => triangles.Thickness = maskedTriangles.Thickness = t);
AddStep("White colour", () => box.Colour = triangles.Colour = Color4.White); AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White);
AddStep("Vertical gradient", () => box.Colour = triangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red));
AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red));
AddToggleStep("Masking", m => maskedTriangles.Masking = m);
} }
} }
} }

View File

@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("move mouse to common point", () => AddStep("move mouse to common point", () =>
{ {
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre; var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos); InputManager.MoveMouseTo(pos);
}); });
AddStep("right click", () => InputManager.Click(MouseButton.Right)); AddStep("right click", () => InputManager.Click(MouseButton.Right));

View File

@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("move mouse to controlpoint", () => AddStep("move mouse to controlpoint", () =>
{ {
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre; var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos); InputManager.MoveMouseTo(pos);
}); });
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } = null!; private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty; private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps() public override void SetUpSteps()
@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName && beatmap.DifficultyName == secondDifficultyName
&& set != null && set != null
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified)); && set.PerformRead(s =>
s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
}); });
} }
@ -327,6 +332,56 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2));
} }
[Test]
public void TestCopyDifficultyDoesNotChangeCollections()
{
string originalDifficultyName = Guid.NewGuid().ToString();
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName);
AddStep("save beatmap", () => Editor.Save());
string originalMd5 = string.Empty;
BeatmapCollection collection = null!;
AddStep("setup a collection with original beatmap", () =>
{
collection = new BeatmapCollection("test copy");
collection.BeatmapMD5Hashes.Add(originalMd5 = EditorBeatmap.BeatmapInfo.MD5Hash);
realm.Write(r =>
{
r.Add(collection);
});
});
AddAssert("collection contains original beatmap", () =>
!string.IsNullOrEmpty(originalMd5) && collection.BeatmapMD5Hashes.Contains(originalMd5));
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != originalDifficultyName;
});
AddStep("save without changes", () => Editor.Save());
AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
&& collection.BeatmapMD5Hashes.Contains(originalMd5));
AddStep("clean up collection", () =>
{
realm.Write(r =>
{
r.Remove(collection);
});
});
}
[Test] [Test]
public void TestCreateMultipleNewDifficultiesSucceeds() public void TestCreateMultipleNewDifficultiesSucceeds()
{ {

View File

@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -28,10 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene public partial class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene
{ {
private ISkin currentBeatmapSkin; private ISkin currentBeatmapSkin = null!;
[Resolved] [Resolved]
private SkinManager skinManager { get; set; } private SkinManager skinManager { get; set; } = null!;
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
@ -57,15 +56,15 @@ namespace osu.Game.Tests.Visual.Gameplay
protected bool AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType target, ISkin expectedSource) protected bool AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType target, ISkin expectedSource)
{ {
var actualComponentsContainer = Player.ChildrenOfType<SkinnableTargetContainer>().First(s => s.Target == target) var targetContainer = Player.ChildrenOfType<SkinnableTargetContainer>().First(s => s.Target == target);
.ChildrenOfType<SkinnableTargetComponentsContainer>().SingleOrDefault(); var actualComponentsContainer = targetContainer.ChildrenOfType<Container>().SingleOrDefault(c => c.Parent == targetContainer);
if (actualComponentsContainer == null) if (actualComponentsContainer == null)
return false; return false;
var actualInfo = actualComponentsContainer.CreateSkinnableInfo(); var actualInfo = actualComponentsContainer.CreateSkinnableInfo();
var expectedComponentsContainer = (SkinnableTargetComponentsContainer)expectedSource.GetDrawableComponent(new GlobalSkinComponentLookup(target)); var expectedComponentsContainer = expectedSource.GetDrawableComponent(new GlobalSkinComponentLookup(target)) as Container;
if (expectedComponentsContainer == null) if (expectedComponentsContainer == null)
return false; return false;
@ -92,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay
return almostEqual(actualInfo, expectedInfo); return almostEqual(actualInfo, expectedInfo);
} }
private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => private static bool almostEqual(SkinnableInfo info, SkinnableInfo? other) =>
other != null other != null
&& info.Type == other.Type && info.Type == other.Type
&& info.Anchor == other.Anchor && info.Anchor == other.Anchor
@ -102,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay
&& Precision.AlmostEquals(info.Rotation, other.Rotation) && Precision.AlmostEquals(info.Rotation, other.Rotation)
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual)); && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset();
@ -111,7 +110,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private readonly ISkin beatmapSkin; private readonly ISkin beatmapSkin;
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin) public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin)
: base(beatmap, storyboard, referenceClock, audio) : base(beatmap, storyboard, referenceClock, audio)
{ {
this.beatmapSkin = beatmapSkin; this.beatmapSkin = beatmapSkin;

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Screens.Play.Break;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneLetterboxOverlay : OsuTestScene
{
public TestSceneLetterboxOverlay()
{
AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both
},
new LetterboxOverlay()
});
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
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.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -89,7 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay
Player.OnUpdate += _ => Player.OnUpdate += _ =>
{ {
double currentTime = Player.GameplayClockContainer.CurrentTime; double currentTime = Player.GameplayClockContainer.CurrentTime;
alwaysGoingForward &= currentTime >= lastTime - 500; bool goingForward = currentTime >= lastTime - 500;
alwaysGoingForward &= goingForward;
if (!goingForward)
Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})");
lastTime = currentTime; lastTime = currentTime;
}; };
}); });

View File

@ -1,36 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene public partial class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene
{ {
protected TestReplayPlayer Player; protected TestReplayPlayer Player = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("Initialise player", () => Player = CreatePlayer(new OsuRuleset()));
AddStep("Load player", () => LoadScreen(Player));
AddUntilStep("player loaded", () => Player.IsLoaded);
}
[Test] [Test]
public void TestPause() public void TestPauseViaSpace()
{ {
loadPlayerWithBeatmap();
double? lastTime = null; double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Pause playback", () => InputManager.Key(Key.Space)); AddStep("Pause playback with space", () => InputManager.Key(Key.Space));
AddAssert("player not exited", () => Player.IsCurrentScreen());
AddUntilStep("Time stopped progressing", () =>
{
double current = Player.GameplayClockContainer.CurrentTime;
bool changed = lastTime != current;
lastTime = current;
return !changed;
});
AddWaitStep("wait some", 10);
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
}
[Test]
public void TestPauseViaSpaceWithSkip()
{
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo = { AudioLeadIn = 60000 }
});
AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible);
AddStep("Skip with space", () => InputManager.Key(Key.Space));
AddAssert("Player not paused", () => !Player.DrawableRuleset.IsPaused.Value);
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Pause playback with space", () => InputManager.Key(Key.Space));
AddAssert("player not exited", () => Player.IsCurrentScreen());
AddUntilStep("Time stopped progressing", () =>
{
double current = Player.GameplayClockContainer.CurrentTime;
bool changed = lastTime != current;
lastTime = current;
return !changed;
});
AddWaitStep("wait some", 10);
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
}
[Test]
public void TestPauseViaMiddleMouse()
{
loadPlayerWithBeatmap();
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Pause playback with middle mouse", () => InputManager.Click(MouseButton.Middle));
AddAssert("player not exited", () => Player.IsCurrentScreen());
AddUntilStep("Time stopped progressing", () => AddUntilStep("Time stopped progressing", () =>
{ {
@ -49,6 +111,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSeekBackwards() public void TestSeekBackwards()
{ {
loadPlayerWithBeatmap();
double? lastTime = null; double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@ -65,6 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSeekForwards() public void TestSeekForwards()
{ {
loadPlayerWithBeatmap();
double? lastTime = null; double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@ -78,12 +144,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500); AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
} }
protected TestReplayPlayer CreatePlayer(Ruleset ruleset) private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
{ {
Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo); AddStep("create player", () =>
{
CreatePlayer(new OsuRuleset(), beatmap);
});
AddStep("Load player", () => LoadScreen(Player));
AddUntilStep("player loaded", () => Player.IsLoaded);
}
protected void CreatePlayer(Ruleset ruleset, IBeatmap? beatmap = null)
{
Beatmap.Value = beatmap != null
? CreateWorkingBeatmap(beatmap)
: CreateWorkingBeatmap(ruleset.RulesetInfo);
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() }; SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestReplayPlayer(false); Player = new TestReplayPlayer(false);
} }
} }
} }

View File

@ -9,11 +9,11 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay

View File

@ -7,9 +7,9 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {

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