Merge branch 'master' into revert-result-in-playfield

This commit is contained in:
Bartłomiej Dach 2023-02-16 21:54:29 +01:00
commit 3ee046000e
No known key found for this signature in database
267 changed files with 5301 additions and 2029 deletions

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

@ -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

@ -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

@ -374,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

@ -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

@ -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

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

@ -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

@ -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

@ -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

@ -22,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!;
@ -57,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);
@ -68,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)
@ -87,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);
@ -96,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

@ -2,13 +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 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> public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IApplicableToDrawableHitObject
{ {
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
@ -18,5 +20,11 @@ namespace osu.Game.Rulesets.Taiko.Mods
var playfield = (TaikoPlayfield)drawableRuleset.Playfield; var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true; playfield.ClassicHitTargetPosition.Value = true;
} }
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableTaikoHitObject hit)
hit.SnapJudgementLocation = true;
}
} }
} }

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

@ -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
{ {
@ -194,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[]
@ -201,9 +209,8 @@ namespace osu.Game.Rulesets.Taiko
HitResult.Great, HitResult.Great,
HitResult.Ok, HitResult.Ok,
HitResult.SmallTickHit,
HitResult.SmallBonus, HitResult.SmallBonus,
HitResult.LargeBonus,
}; };
} }
@ -212,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

@ -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)
} }
}; };
} }
@ -36,6 +40,7 @@ namespace osu.Game.Tests.Visual.Background
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)); 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

@ -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

@ -1,10 +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.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Timing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -14,16 +16,23 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Storyboards;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{ {
private TestGameplaySampleTriggerSource sampleTriggerSource; private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
private Beatmap beatmap; private Beatmap beatmap = null!;
[Resolved]
private AudioManager audio { get; set; } = null!;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {
@ -39,12 +48,13 @@ namespace osu.Game.Tests.Visual.Gameplay
const double start_offset = 8000; const double start_offset = 8000;
const double spacing = 2000; const double spacing = 2000;
// intentionally start objects a bit late so we can test the case of no alive objects.
double t = start_offset; double t = start_offset;
beatmap.HitObjects.AddRange(new[] beatmap.HitObjects.AddRange(new[]
{ {
new HitCircle new HitCircle
{ {
// intentionally start objects a bit late so we can test the case of no alive objects.
StartTime = t += spacing, StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
}, },
@ -80,42 +90,66 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestCorrectHitObject() public void TestCorrectHitObject()
{ {
HitObjectLifetimeEntry nextObjectEntry = null; waitForAliveObjectIndex(null);
checkValidObjectIndex(0);
AddAssert("no alive objects", () => getNextAliveObject() == null); seekBeforeIndex(0);
waitForAliveObjectIndex(0);
checkValidObjectIndex(0);
AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]); AddAssert("first object not hit", () => getNextAliveObject()?.Entry?.Result?.HasResult != true);
AddUntilStep("get next object", () => AddStep("hit first object", () =>
{ {
var nextDrawableObject = getNextAliveObject(); var next = getNextAliveObject();
if (nextDrawableObject != null) if (next != null)
{ {
nextObjectEntry = nextDrawableObject.Entry; Debug.Assert(next.Entry?.Result?.HasResult != true);
InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
return true; InputManager.MoveMouseTo(next.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
} }
return false;
}); });
AddUntilStep("hit first hitobject", () => AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
{
InputManager.Click(MouseButton.Left);
return nextObjectEntry.Result?.HasResult == true;
});
AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]); checkValidObjectIndex(1);
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]); // Still object 1 as it's not hit yet.
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]); seekBeforeIndex(1);
waitForAliveObjectIndex(1);
checkValidObjectIndex(1);
AddUntilStep("no alive objects", () => getNextAliveObject() == null); seekBeforeIndex(2);
AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]); waitForAliveObjectIndex(2);
checkValidObjectIndex(2);
seekBeforeIndex(3);
waitForAliveObjectIndex(3);
checkValidObjectIndex(3);
AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
waitForAliveObjectIndex(null);
checkValidObjectIndex(3);
} }
private DrawableHitObject getNextAliveObject() => private void seekBeforeIndex(int index) =>
AddStep($"seek to just before object {index}", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[index].StartTime - 100));
private void waitForAliveObjectIndex(int? index)
{
if (index == null)
AddUntilStep("wait for no alive objects", getNextAliveObject, () => Is.Null);
else
AddUntilStep($"wait for next alive to be {index}", () => getNextAliveObject()?.HitObject, () => Is.EqualTo(beatmap.HitObjects[index.Value]));
}
private void checkValidObjectIndex(int index) =>
AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
private DrawableHitObject? getNextAliveObject() =>
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(); Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
[Test] [Test]

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
{ {

View File

@ -8,11 +8,11 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
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.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input; using osuTK.Input;

View File

@ -16,6 +16,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -85,6 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>(); ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => MultiplayerClient.RoomJoined); AddUntilStep("wait for join", () => MultiplayerClient.RoomJoined);
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
} }
[Test] [Test]

View File

@ -15,7 +15,6 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -44,14 +43,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest]
/*
* TearDown : System.TimeoutException : "wait for ongoing operation to complete" timed out
* --TearDown
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
*/
public void TestItemAddedToTheEndOfQueue() public void TestItemAddedToTheEndOfQueue()
{ {
addItem(() => OtherBeatmap); addItem(() => OtherBeatmap);
@ -64,7 +55,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestNextItemSelectedAfterGameplayFinish() public void TestNextItemSelectedAfterGameplayFinish()
{ {
addItem(() => OtherBeatmap); addItem(() => OtherBeatmap);
@ -82,7 +72,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestItemsNotClearedWhenSwitchToHostOnlyMode() public void TestItemsNotClearedWhenSwitchToHostOnlyMode()
{ {
addItem(() => OtherBeatmap); addItem(() => OtherBeatmap);
@ -98,7 +87,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestCorrectItemSelectedAfterNewItemAdded() public void TestCorrectItemSelectedAfterNewItemAdded()
{ {
addItem(() => OtherBeatmap); addItem(() => OtherBeatmap);
@ -106,7 +94,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestCorrectRulesetSelectedAfterNewItemAdded() public void TestCorrectRulesetSelectedAfterNewItemAdded()
{ {
addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo); addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo);
@ -124,7 +111,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestCorrectModsSelectedAfterNewItemAdded() public void TestCorrectModsSelectedAfterNewItemAdded()
{ {
addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() }); addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() });
@ -153,7 +139,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null); AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded); AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded);
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
if (ruleset != null) if (ruleset != null)
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset); AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);

View File

@ -50,14 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest]
/*
* TearDown : System.TimeoutException : "wait for ongoing operation to complete" timed out
* --TearDown
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
*/
public void TestItemStillSelectedAfterChangeToSameBeatmap() public void TestItemStillSelectedAfterChangeToSameBeatmap()
{ {
selectNewItem(() => InitialBeatmap); selectNewItem(() => InitialBeatmap);
@ -66,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestItemStillSelectedAfterChangeToOtherBeatmap() public void TestItemStillSelectedAfterChangeToOtherBeatmap()
{ {
selectNewItem(() => OtherBeatmap); selectNewItem(() => OtherBeatmap);
@ -75,7 +66,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestOnlyLastItemChangedAfterGameplayFinished() public void TestOnlyLastItemChangedAfterGameplayFinished()
{ {
RunGameplay(); RunGameplay();
@ -90,7 +80,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
[FlakyTest] // See above
public void TestAddItemsAsHost() public void TestAddItemsAsHost()
{ {
addItem(() => OtherBeatmap); addItem(() => OtherBeatmap);
@ -115,7 +104,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
BeatmapInfo otherBeatmap = null; BeatmapInfo otherBeatmap = null;
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
@ -131,7 +119,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap())); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
} }

View File

@ -8,8 +8,8 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays.Settings.Sections; using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework; using osu.Framework;
@ -14,6 +15,8 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
@ -23,11 +26,13 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
private HeadlessGameHost ipcSenderHost = null!; private HeadlessGameHost ipcSenderHost = null!;
private OsuSchemeLinkIPCChannel osuSchemeLinkIPCReceiver = null!;
private OsuSchemeLinkIPCChannel osuSchemeLinkIPCSender = null!; private OsuSchemeLinkIPCChannel osuSchemeLinkIPCSender = null!;
private ArchiveImportIPCChannel archiveImportIPCSender = null!;
private const int requested_beatmap_set_id = 1; private const int requested_beatmap_set_id = 1;
protected override TestOsuGame CreateTestGame() => new IpcGame(LocalStorage, API);
[Resolved] [Resolved]
private GameHost gameHost { get; set; } = null!; private GameHost gameHost { get; set; } = null!;
@ -56,11 +61,11 @@ namespace osu.Game.Tests.Visual.Navigation
return false; return false;
}; };
}); });
AddStep("create IPC receiver channel", () => osuSchemeLinkIPCReceiver = new OsuSchemeLinkIPCChannel(gameHost, Game)); AddStep("create IPC sender channels", () =>
AddStep("create IPC sender channel", () =>
{ {
ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true }); ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true });
osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost);
archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost);
}); });
} }
@ -72,15 +77,50 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("beatmap overlay showing content", () => Game.ChildrenOfType<BeatmapSetOverlay>().FirstOrDefault()?.Header.BeatmapSet.Value.OnlineID == requested_beatmap_set_id); AddUntilStep("beatmap overlay showing content", () => Game.ChildrenOfType<BeatmapSetOverlay>().FirstOrDefault()?.Header.BeatmapSet.Value.OnlineID == requested_beatmap_set_id);
} }
[Test]
public void TestArchiveImportLinkIPCChannel()
{
string? beatmapFilepath = null;
AddStep("import beatmap via IPC", () => archiveImportIPCSender.ImportAsync(beatmapFilepath = TestResources.GetQuickTestBeatmapForImport()).WaitSafely());
AddUntilStep("import complete notification was presented", () => Game.Notifications.ChildrenOfType<ProgressCompletionNotification>().Count(), () => Is.EqualTo(1));
AddAssert("original file deleted", () => File.Exists(beatmapFilepath), () => Is.False);
}
public override void TearDownSteps() public override void TearDownSteps()
{ {
AddStep("dispose IPC receiver", () => osuSchemeLinkIPCReceiver.Dispose()); AddStep("dispose IPC senders", () =>
AddStep("dispose IPC sender", () =>
{ {
osuSchemeLinkIPCSender.Dispose(); osuSchemeLinkIPCSender.Dispose();
archiveImportIPCSender.Dispose();
ipcSenderHost.Dispose(); ipcSenderHost.Dispose();
}); });
base.TearDownSteps(); base.TearDownSteps();
} }
private partial class IpcGame : TestOsuGame
{
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
public IpcGame(Storage storage, IAPIProvider api, string[]? args = null)
: base(storage, api, args)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
}
}
} }
} }

View File

@ -563,6 +563,18 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists)); AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
} }
[Test]
public void TestBeatmapListingLinkSearchOnInitialOpen()
{
BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType<BeatmapListingOverlay>().FirstOrDefault();
AddStep("open beatmap overlay with test query", () => Game.SearchBeatmapSet("test"));
AddUntilStep("wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible);
AddAssert("beatmap overlay sorted by relevance", () => getBeatmapListingOverlay().ChildrenOfType<BeatmapListingSortTabControl>().Single().Current.Value == SortCriteria.Relevance);
}
[Test] [Test]
public void TestMainOverlaysClosesNotificationOverlay() public void TestMainOverlaysClosesNotificationOverlay()
{ {

View File

@ -12,11 +12,11 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;

View File

@ -0,0 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public partial class TestSceneGroupBadges : OsuTestScene
{
public TestSceneGroupBadges()
{
var groups = new[]
{
new APIUser(),
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
}
},
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
}
},
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#0066FF", ShortName = "PPY", Name = "peppy" },
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
}
},
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#0066FF", ShortName = "PPY", Name = "peppy" },
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#999999", ShortName = "ALM", Name = "osu! Alumni" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators (Probationary)", Playmodes = new[] { "osu", "taiko" }, IsProbationary = true }
}
}
};
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.DarkGray
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(40),
Children = new[]
{
new FillFlowContainer<GroupBadgeFlow>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
ChildrenEnumerable = groups.Select(g => new GroupBadgeFlow { User = { Value = g } })
},
}
}
};
}
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestEditActivity() public void TestEditActivity()
{ {
AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo())); AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));

View File

@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -107,14 +109,16 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set online status", () => status.Value = new UserStatusOnline()); AddStep("set online status", () => status.Value = new UserStatusOnline());
AddStep("idle", () => activity.Value = null); AddStep("idle", () => activity.Value = null);
AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
AddStep("editing", () => activity.Value = new UserActivity.Editing(null)); AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null));
AddStep("modding", () => activity.Value = new UserActivity.Modding()); AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null));
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null));
} }
[Test] [Test]
@ -132,6 +136,14 @@ namespace osu.Game.Tests.Visual.Online
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId)); private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId));
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
User = new APIUser
{
Username = name,
}
};
private partial class TestUserListPanel : UserListPanel private partial class TestUserListPanel : UserListPanel
{ {
public TestUserListPanel(APIUser user) public TestUserListPanel(APIUser user)

View File

@ -90,7 +90,9 @@ namespace osu.Game.Tests.Visual.Online
{ {
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" }, new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } }, new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } } new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko", "fruits", "mania" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators (Probationary)", Playmodes = new[] { "osu", "taiko", "fruits", "mania" }, IsProbationary = true }
}, },
ProfileOrder = new[] ProfileOrder = new[]
{ {
@ -119,6 +121,12 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
}, },
}, },
TournamentBanner = new TournamentBanner
{
Id = 13926,
TournamentId = 35,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
},
Badges = new[] Badges = new[]
{ {
new Badge new Badge

View File

@ -24,17 +24,26 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
public partial class TestSceneAccuracyCircle : OsuTestScene public partial class TestSceneAccuracyCircle : OsuTestScene
{ {
[TestCase(0.2, ScoreRank.D)] [TestCase(0)]
[TestCase(0.5, ScoreRank.D)] [TestCase(0.2)]
[TestCase(0.75, ScoreRank.C)] [TestCase(0.5)]
[TestCase(0.85, ScoreRank.B)] [TestCase(0.6999)]
[TestCase(0.925, ScoreRank.A)] [TestCase(0.7)]
[TestCase(0.975, ScoreRank.S)] [TestCase(0.75)]
[TestCase(0.9999, ScoreRank.S)] [TestCase(0.7999)]
[TestCase(1, ScoreRank.X)] [TestCase(0.8)]
public void TestRank(double accuracy, ScoreRank rank) [TestCase(0.85)]
[TestCase(0.8999)]
[TestCase(0.9)]
[TestCase(0.925)]
[TestCase(0.9499)]
[TestCase(0.95)]
[TestCase(0.975)]
[TestCase(0.9999)]
[TestCase(1)]
public void TestRank(double accuracy)
{ {
var score = createScore(accuracy, rank); var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy));
addCircleStep(score); addCircleStep(score);
} }

View File

@ -0,0 +1,146 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Select.FooterV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
{
private FooterButtonRandomV2 randomButton = null!;
private FooterButtonModsV2 modsButton = null!;
private bool nextRandomCalled;
private bool previousRandomCalled;
private DummyOverlay overlay = null!;
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUp]
public void SetUp() => Schedule(() =>
{
nextRandomCalled = false;
previousRandomCalled = false;
FooterV2 footer;
Children = new Drawable[]
{
footer = new FooterV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
overlay = new DummyOverlay()
};
footer.AddButton(modsButton = new FooterButtonModsV2(), overlay);
footer.AddButton(randomButton = new FooterButtonRandomV2
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true
});
footer.AddButton(new FooterButtonOptionsV2());
overlay.Hide();
});
[Test]
public void TestState()
{
AddToggleStep("set options enabled state", state => this.ChildrenOfType<FooterButtonV2>().Last().Enabled.Value = state);
}
[Test]
public void TestFooterRandom()
{
AddStep("press F2", () => InputManager.Key(Key.F2));
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRandomViaMouse()
{
AddStep("click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRewind()
{
AddStep("press Shift+F2", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.F2);
InputManager.ReleaseKey(Key.F2);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaShiftMouseLeft()
{
AddStep("shift + click button", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaMouseRight()
{
AddStep("right click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Right);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestOverlayPresent()
{
AddStep("Press F1", () =>
{
InputManager.MoveMouseTo(modsButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible);
AddStep("Hide", () => overlay.Hide());
}
private partial class DummyOverlay : ShearedOverlayContainer
{
public DummyOverlay()
: base(OverlayColourScheme.Green)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "An overlay";
}
}
}
}

View File

@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep($"Set {name} slider to {value}", () => AddStep($"Set {name} slider to {value}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name) this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.ChildrenOfType<OsuSliderBar<float>>().First().Current.Value = value); .ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = value);
} }
private void checkBindableAtValue(string name, float? expectedValue) private void checkBindableAtValue(string name, float? expectedValue)
@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddAssert($"Slider {name} at {expectedValue}", () => AddAssert($"Slider {name} at {expectedValue}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name) this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.ChildrenOfType<OsuSliderBar<float>>().First().Current.Value == expectedValue); .ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value == expectedValue);
} }
private void setBeatmapWithDifficultyParameters(float value) private void setBeatmapWithDifficultyParameters(float value)

View File

@ -270,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen(); createScreen();
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
AddStep("set setting", () => modSelectOverlay.ChildrenOfType<OsuSliderBar<float>>().First().Current.Value = 8); AddStep("set setting", () => modSelectOverlay.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8); AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneShearedSliderBar : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly BindableDouble current = new BindableDouble(5)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 15
};
[BackgroundDependencyLoader]
private void load()
{
Child = new ShearedSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = current,
RelativeSizeAxes = Axes.X,
Width = 0.4f
};
}
}
}

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