1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 02:09:52 +08:00

Compare commits

..

1055 Commits

482 changed files with 10587 additions and 3909 deletions
+1 -10
View File
@@ -121,21 +121,12 @@ jobs:
build-only-ios:
name: Build only (iOS)
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
runs-on: macos-12
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
# see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
# remove once all workflow VMs use Xcode 14.1
- name: Set Xcode Version
shell: bash
run: |
sudo xcode-select -s "/Applications/Xcode_14.1.app"
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
+2 -2
View File
@@ -1,7 +1,7 @@
<!-- Contains required properties for osu!framework projects. -->
<Project>
<PropertyGroup Label="C#">
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -17,7 +17,7 @@
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<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" />
</ItemGroup>
<PropertyGroup Label="Code Analysis">
-6
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)
-234
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
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<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="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<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="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<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="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<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="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
+43 -5
View File
@@ -3,15 +3,53 @@
#
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
$CSPROJ="osu.Game/osu.Game.csproj"
$GAME_CSPROJ="osu.Game/osu.Game.csproj"
$ANDROID_PROPS="osu.Android.props"
$IOS_PROPS="osu.iOS.props"
$SLN="osu.sln"
dotnet remove $CSPROJ package ppy.osu.Framework;
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj;
dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
dotnet remove $GAME_CSPROJ reference ppy.osu.Framework;
dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android;
dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS;
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj `
../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj `
../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj `
../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj;
dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj;
dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
(Get-Content "osu.Android.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.Android.props"
(Get-Content "osu.iOS.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.iOS.props"
# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
(Get-Content "osu.iOS.props") |
Foreach-Object {
if ($_ -match "</Project>")
{
" <Import Project=`"`$(MSBuildThisFileDirectory)../osu-framework/osu.Framework.iOS.props`"/>"
}
$_
} | Set-Content "osu.iOS.props"
$TMP=New-TemporaryFile
$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json
$TMP=New-TemporaryFile
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force
$SLNF=Get-Content "osu.Android.slnf" | ConvertFrom-Json
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.Android.slnf" -Force
$SLNF=Get-Content "osu.iOS.slnf" | ConvertFrom-Json
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.iOS.slnf" -Force
+33 -6
View File
@@ -5,14 +5,41 @@
#
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
CSPROJ="osu.Game/osu.Game.csproj"
GAME_CSPROJ="osu.Game/osu.Game.csproj"
ANDROID_PROPS="osu.Android.props"
IOS_PROPS="osu.iOS.props"
SLN="osu.sln"
dotnet remove $CSPROJ package ppy.osu.Framework
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj
dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
dotnet remove $GAME_CSPROJ reference ppy.osu.Framework
dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android
dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj \
../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj \
../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj \
../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj
dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.Android.props && rm osu.Android.props.bak
sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.iOS.props && rm osu.iOS.props.bak
# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
sed -i.bak '/<\/Project>/i\
<Import Project=\"$(MSBuildThisFileDirectory)../osu-framework/osu.Framework.iOS.props\"/>\
' ./osu.iOS.props && rm osu.iOS.props.bak
SLNF="osu.Desktop.slnf"
tmp=$(mktemp)
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj"]' osu.Desktop.slnf > $tmp
mv -f $tmp $SLNF
mv -f $tmp osu.Desktop.slnf
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj"]' osu.Android.slnf > $tmp
mv -f $tmp osu.Android.slnf
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj"]' osu.iOS.slnf > $tmp
mv -f $tmp osu.iOS.slnf
-2
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
-147
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
-1
View File
@@ -1 +0,0 @@
git_url('https://github.com/peppy/apple-certificates')
-7
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'
-109
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).
+5 -1
View File
@@ -8,9 +8,13 @@
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.120.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.326.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+63
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);
}
}
}
+6 -22
View File
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -14,7 +13,6 @@ using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.OS;
using Android.Provider;
using Android.Views;
using osu.Framework.Android;
using osu.Game.Database;
@@ -131,28 +129,14 @@ namespace osu.Android
await Task.WhenAll(uris.Select(async 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);
var task = await AndroidImportTask.Create(ContentResolver!, uri).ConfigureAwait(false);
if (cursor == 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)
if (task != null)
{
tasks.Add(new ImportTask(copy, filename));
lock (tasks)
{
tasks.Add(task);
}
}
})).ConfigureAwait(false);
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>
+6 -3
View File
@@ -98,7 +98,7 @@ namespace osu.Desktop
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));
if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0)
@@ -169,7 +169,7 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo;
}
@@ -183,9 +183,12 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.WatchingReplay watching:
return watching.BeatmapInfo.ToString();
case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
}
+14 -6
View File
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
@@ -29,6 +28,7 @@ namespace osu.Desktop
internal partial class OsuGameDesktop : OsuGame
{
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
public OsuGameDesktop(string[]? args = null)
: base(args)
@@ -123,6 +123,7 @@ namespace osu.Desktop
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
}
public override void SetHost(GameHost host)
@@ -137,7 +138,17 @@ namespace osu.Desktop
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
desktopWindow.DragDrop += f =>
{
// on macOS, URL associations are handled via SDL_DROPFILE events.
if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
{
HandleLink(f);
return;
}
fileDrop(new[] { f });
};
}
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
@@ -149,10 +160,6 @@ namespace osu.Desktop
{
lock (importableFiles)
{
string firstExtension = Path.GetExtension(filePaths.First());
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
importableFiles.AddRange(filePaths);
Logger.Log($"Adding {filePaths.Length} files for import");
@@ -181,6 +188,7 @@ namespace osu.Desktop
{
base.Dispose(isDisposing);
osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
}
private class SDL2BatteryInfo : BatteryInfo
+2 -2
View File
@@ -26,8 +26,8 @@
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.1.14" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
@@ -0,0 +1,123 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
namespace osu.Game.Benchmarks
{
public class BenchmarkCarouselFilter : BenchmarkTest
{
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
{
Ruleset = new RulesetInfo
{
ShortName = "osu",
OnlineID = 0
},
StarRating = 4.0d,
Difficulty = new BeatmapDifficulty
{
ApproachRate = 5.0f,
DrainRate = 3.0f,
CircleSize = 2.0f,
},
Metadata = new BeatmapMetadata
{
Artist = "The Artist",
ArtistUnicode = "check unicode too",
Title = "Title goes here",
TitleUnicode = "Title goes here",
Author = { Username = "The Author" },
Source = "unit tests",
Tags = "look for tags too",
},
DifficultyName = "version as well",
Length = 2500,
BPM = 160,
BeatDivisor = 12,
Status = BeatmapOnlineStatus.Loved
};
private CarouselBeatmap carouselBeatmap = null!;
private FilterCriteria criteria1 = null!;
private FilterCriteria criteria2 = null!;
private FilterCriteria criteria3 = null!;
private FilterCriteria criteria4 = null!;
private FilterCriteria criteria5 = null!;
private FilterCriteria criteria6 = null!;
public override void SetUp()
{
var beatmap = getExampleBeatmap();
beatmap.OnlineID = 20201010;
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
carouselBeatmap = new CarouselBeatmap(beatmap);
criteria1 = new FilterCriteria();
criteria2 = new FilterCriteria
{
Ruleset = new RulesetInfo { ShortName = "catch" }
};
criteria3 = new FilterCriteria
{
Ruleset = new RulesetInfo { OnlineID = 6 },
AllowConvertedBeatmaps = true,
BPM = new FilterCriteria.OptionalRange<double>
{
IsUpperInclusive = false,
Max = 160d
}
};
criteria4 = new FilterCriteria
{
Ruleset = new RulesetInfo { OnlineID = 6 },
AllowConvertedBeatmaps = true,
SearchText = "an artist"
};
criteria5 = new FilterCriteria
{
Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = "the author AND then something else" }
};
criteria6 = new FilterCriteria { SearchText = "20201010" };
}
[Benchmark]
public void CarouselBeatmapFilter()
{
carouselBeatmap.Filter(criteria1);
}
[Benchmark]
public void CriteriaMatchingSpecificRuleset()
{
carouselBeatmap.Filter(criteria2);
}
[Benchmark]
public void CriteriaMatchingRangeMax()
{
carouselBeatmap.Filter(criteria3);
}
[Benchmark]
public void CriteriaMatchingTerms()
{
carouselBeatmap.Filter(criteria4);
}
[Benchmark]
public void CriteriaMatchingCreator()
{
carouselBeatmap.Filter(criteria5);
}
[Benchmark]
public void CriteriaMatchingBeatmapIDs()
{
carouselBeatmap.Filter(criteria6);
}
}
}
@@ -7,9 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
@@ -1,17 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
if (withModifiedSkin)
{
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit());
CreateTest();
}
@@ -60,26 +60,24 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherHyperStateReverted()
{
DrawableCatchHitObject drawableObject1 = null;
DrawableCatchHitObject drawableObject2 = null;
JudgementResult result1 = null;
JudgementResult result2 = null;
AddStep("catch hyper fruit", () =>
{
attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1);
result1 = attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } });
});
AddStep("catch normal fruit", () =>
{
attemptCatch(new Fruit(), out drawableObject2, out result2);
result2 = attemptCatch(new Fruit());
});
AddStep("revert second result", () =>
{
catcher.OnRevertResult(drawableObject2, result2);
catcher.OnRevertResult(result2);
});
checkHyperDash(true);
AddStep("revert first result", () =>
{
catcher.OnRevertResult(drawableObject1, result1);
catcher.OnRevertResult(result1);
});
checkHyperDash(false);
}
@@ -87,16 +85,15 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherAnimationStateReverted()
{
DrawableCatchHitObject drawableObject = null;
JudgementResult result = null;
AddStep("catch kiai fruit", () =>
{
attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
result = attemptCatch(new TestKiaiFruit());
});
checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () =>
{
catcher.OnRevertResult(drawableObject, result);
catcher.OnRevertResult(result);
});
checkState(CatcherAnimationState.Idle);
}
@@ -268,23 +265,19 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
private void attemptCatch(CatchHitObject hitObject)
{
attemptCatch(() => hitObject, 1);
}
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
{
for (int i = 0; i < count; i++)
attemptCatch(hitObject(), out _, out _);
attemptCatch(hitObject());
}
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
private JudgementResult attemptCatch(CatchHitObject hitObject)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableObject = createDrawableObject(hitObject);
result = createResult(hitObject);
var drawableObject = createDrawableObject(hitObject);
var result = createResult(hitObject);
applyResult(drawableObject, result);
return result;
}
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<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="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
+1
View File
@@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
new CatchModHidden(),
new CatchModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Conversion:
@@ -4,6 +4,7 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -27,12 +28,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is GlobalSkinComponentLookup targetComponent)
if (lookup is SkinComponentsContainerLookup containerLookup)
{
switch (targetComponent.Lookup)
switch (containerLookup.Target)
{
case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as SkinnableTargetComponentsContainer;
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container;
if (providesComboCounter && components != null)
{
@@ -63,12 +63,12 @@ namespace osu.Game.Rulesets.Catch.UI
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
}
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
if (!result.Type.AffectsCombo() || !result.HasResult)
return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
updateCombo(result.ComboAtJudgement, null);
}
private void updateCombo(int newCombo, Color4? hitObjectColour)
+2 -2
View File
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(JudgementResult result)
=> CatcherArea.OnRevertResult(result);
}
}
+3 -3
View File
@@ -254,7 +254,7 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
@@ -268,8 +268,8 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
}
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
caughtObjectContainer.RemoveAll(d => d.HitObject == result.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == result.HitObject, false);
}
/// <summary>
+3 -3
View File
@@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.OnNewResult(hitObject, result);
}
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
comboDisplay.OnRevertResult(hitObject, result);
Catcher.OnRevertResult(hitObject, result);
comboDisplay.OnRevertResult(result);
Catcher.OnRevertResult(result);
}
protected override void Update()
@@ -1,17 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}
@@ -0,0 +1,67 @@
// 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.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneObjectPlacement : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Test]
public void TestPlacementBeforeTrackStart()
{
AddStep("Seek to 0", () => EditorClock.Seek(0));
AddStep("Select note", () => InputManager.Key(Key.Number2));
AddStep("Hover negative span", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
});
AddStep("Click", () => InputManager.Click(MouseButton.Left));
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
}
[Test]
public void TestSeekOnNotePlacement()
{
double? initialTime = null;
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true));
placeObject();
AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking);
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime));
}
[Test]
public void TestNoSeekOnNotePlacement()
{
double? initialTime = null;
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false));
placeObject();
AddAssert("not seeking", () => !EditorClock.IsSeeking);
AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime));
}
private void placeObject()
{
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
AddStep("place note", () => InputManager.Click(MouseButton.Left));
}
}
}
@@ -1,30 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestScenePlacementBeforeTrackStart : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Test]
public void TestPlacement()
{
AddStep("Seek to 0", () => EditorClock.Seek(0));
AddStep("Select note", () => InputManager.Key(Key.Number2));
AddStep("Hover negative span", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
});
AddStep("Click", () => InputManager.Click(MouseButton.Left));
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@@ -14,4 +14,6 @@ Hit200: mania/hit200@2x
Hit300: mania/hit300@2x
Hit300g: mania/hit300g@2x
StageLeft: mania/stage-left
StageRight: mania/stage-right
StageRight: mania/stage-right
NoteImage0L: LongNoteTailWang
NoteImage1L: LongNoteTailWang
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<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="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
+1
View File
@@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
new ManiaModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Conversion:
@@ -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));
}
@@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
private double? releaseTime;
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
public DrawableHoldNote()
: this(null)
{
@@ -238,6 +236,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
};
// Position and resize the body to lie half-way under the head and the tail notes.
// The rationale for this is account for heads/tails with corner radius.
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
@@ -376,7 +375,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void OnFree()
{
slidingSample.Samples = null;
slidingSample.ClearSamples();
base.OnFree();
}
}
@@ -15,16 +15,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public partial class DrawableHoldNoteTail : DrawableNote
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
private const double release_window_lenience = 1.5;
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
public DrawableHoldNoteTail()
: this(null)
@@ -40,14 +33,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
// Factor in the release lenience
timeOffset /= release_window_lenience;
timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
if (!userTriggered)
{
@@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public TailNote Tail { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
/// <summary>
/// The time between ticks of this hold.
/// </summary>
@@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public class TailNote : Note
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
public const double RELEASE_WINDOW_LENIENCE = 1.5;
public override Judgement CreateJudgement() => new ManiaJudgement();
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * RELEASE_WINDOW_LENIENCE;
}
}
@@ -43,9 +43,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
largeFaint = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
Masking = true,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Blending = BlendingParameters.Additive,
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void load(IScrollingInfo scrollingInfo)
{
RelativeSizeAxes = Axes.X;
Height = ArgonNotePiece.NOTE_HEIGHT;
Height = ArgonNotePiece.NOTE_HEIGHT * ArgonNotePiece.NOTE_ACCENT_RATIO;
Masking = true;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
@@ -20,10 +20,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
public partial class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
protected readonly IBindable<bool> IsHitting = new Bindable<bool>();
private Drawable background = null!;
private Box foreground = null!;
private ArgonHoldNoteHittingLayer hittingLayer = null!;
public ArgonHoldBodyPiece()
{
@@ -32,7 +31,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
// Without this, the width of the body will be slightly larger than the head/tail.
Masking = true;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
Blending = BlendingParameters.Additive;
}
[BackgroundDependencyLoader(true)]
@@ -41,12 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
foreground = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Alpha = 0,
},
hittingLayer = new ArgonHoldNoteHittingLayer()
};
if (drawableObject != null)
@@ -54,44 +47,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(holdNote.AccentColour);
IsHitting.BindTo(holdNote.IsHitting);
hittingLayer.AccentColour.BindTo(holdNote.AccentColour);
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(colour =>
{
background.Colour = colour.NewValue.Darken(1.2f);
foreground.Colour = colour.NewValue.Opacity(0.2f);
background.Colour = colour.NewValue.Darken(0.6f);
}, true);
IsHitting.BindValueChanged(hitting =>
{
const float animation_length = 50;
foreground.ClearTransforms();
if (hitting.NewValue)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foreground.BeginDelayedSequence(synchronisedOffset))
{
foreground.FadeTo(1, animation_length).Then()
.FadeTo(0.5f, animation_length)
.Loop();
}
}
else
{
foreground.FadeOut(animation_length);
}
});
}
public void Recycle()
{
foreground.ClearTransforms();
foreground.Alpha = 0;
hittingLayer.Recycle();
}
}
}
@@ -0,0 +1,20 @@
// 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 osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
internal partial class ArgonHoldNoteHeadPiece : ArgonNotePiece
{
protected override Drawable CreateIcon() => new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 2,
Size = new Vector2(20, 5),
};
}
}
@@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osuTK.Graphics;
using Box = osu.Framework.Graphics.Shapes.Box;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonHoldNoteHittingLayer : Box
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> IsHitting = new Bindable<bool>();
public ArgonHoldNoteHittingLayer()
{
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
Alpha = 0;
}
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindValueChanged(colour =>
{
Colour = colour.NewValue.Lighten(0.2f).Opacity(0.3f);
}, true);
IsHitting.BindValueChanged(hitting =>
{
const float animation_length = 80;
ClearTransforms();
if (hitting.NewValue)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (BeginDelayedSequence(synchronisedOffset))
{
this.FadeTo(1, animation_length, Easing.OutSine).Then()
.FadeTo(0.5f, animation_length, Easing.InSine)
.Loop();
}
}
else
{
this.FadeOut(animation_length);
}
}, true);
}
public void Recycle()
{
ClearTransforms();
Alpha = 0;
}
}
}
@@ -8,62 +8,78 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
internal partial class ArgonHoldNoteTailPiece : CompositeDrawable
{
[Resolved]
private DrawableHitObject? drawableObject { get; set; }
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly Box colouredBox;
private readonly Box shadow;
private readonly Box foreground;
private readonly ArgonHoldNoteHittingLayer hittingLayer;
private readonly Box foregroundAdditive;
public ArgonHoldNoteTailPiece()
{
RelativeSizeAxes = Axes.X;
Height = ArgonNotePiece.NOTE_HEIGHT;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
{
shadow = new Box
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.82f,
Masking = true,
RelativeSizeAxes = Axes.X,
Height = ArgonNotePiece.NOTE_HEIGHT,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
colouredBox = new Box
new Box
{
RelativeSizeAxes = Axes.Both,
}
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Colour4.Black),
// Avoid ugly single pixel overlap.
Height = 0.9f,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
foreground = new Box
{
RelativeSizeAxes = Axes.Both,
},
hittingLayer = new ArgonHoldNoteHittingLayer(),
foregroundAdditive = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Height = 0.5f,
},
},
},
}
},
new Circle
{
RelativeSizeAxes = Axes.X,
Height = ArgonNotePiece.CORNER_RADIUS * 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
};
}
[BackgroundDependencyLoader(true)]
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
@@ -72,24 +88,45 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(onAccentChanged, true);
drawableObject.HitObjectApplied += hitObjectApplied;
}
}
private void hitObjectApplied(DrawableHitObject drawableHitObject)
{
var holdNoteTail = (DrawableHoldNoteTail)drawableHitObject;
hittingLayer.Recycle();
hittingLayer.AccentColour.UnbindBindings();
hittingLayer.AccentColour.BindTo(holdNoteTail.HoldNote.AccentColour);
hittingLayer.IsHitting.UnbindBindings();
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHitting);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
? Anchor.TopCentre
: Anchor.BottomCentre;
Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1);
}
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
colouredBox.Colour = ColourInfo.GradientVertical(
accent.NewValue,
accent.NewValue.Darken(0.1f)
);
foreground.Colour = accent.NewValue.Darken(0.6f); // matches body
shadow.Colour = accent.NewValue.Darken(0.5f);
foregroundAdditive.Colour = ColourInfo.GradientVertical(
accent.NewValue.Opacity(0.4f),
accent.NewValue.Opacity(0)
);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.HitObjectApplied -= hitObjectApplied;
}
}
}
@@ -19,14 +19,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
internal partial class ArgonNotePiece : CompositeDrawable
{
public const float NOTE_HEIGHT = 42;
public const float NOTE_ACCENT_RATIO = 0.82f;
public const float CORNER_RADIUS = 3.4f;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly Box colouredBox;
private readonly Box shadow;
public ArgonNotePiece()
{
@@ -36,18 +35,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
CornerRadius = CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
shadow = new Box
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Colour4.Black)
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.82f,
Height = NOTE_ACCENT_RATIO,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
@@ -65,18 +65,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
RelativeSizeAxes = Axes.X,
Height = CORNER_RADIUS * 2,
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 4,
Icon = FontAwesome.Solid.AngleDown,
Size = new Vector2(20),
Scale = new Vector2(1, 0.7f)
}
CreateIcon(),
};
}
protected virtual Drawable CreateIcon() => new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 4,
// TODO: replace with a non-squashed version.
// The 0.7f height scale should be removed.
Icon = FontAwesome.Solid.AngleDown,
Size = new Vector2(20),
Scale = new Vector2(1, 0.7f)
};
[BackgroundDependencyLoader(true)]
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
{
@@ -95,6 +99,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
? Anchor.TopCentre
: Anchor.BottomCentre;
Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1);
}
private void onAccentChanged(ValueChangedEvent<Color4> accent)
@@ -103,8 +109,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
accent.NewValue.Lighten(0.1f),
accent.NewValue
);
shadow.Colour = accent.NewValue.Darken(0.5f);
}
}
}
@@ -50,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new ArgonHoldNoteTailPiece();
case ManiaSkinComponents.HoldNoteHead:
return new ArgonHoldNoteHeadPiece();
case ManiaSkinComponents.Note:
return new ArgonNotePiece();
@@ -69,12 +71,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetDrawableComponent(lookup);
}
private static readonly Color4 colour_special_column = new Color4(169, 106, 255, 255);
private const int total_colours = 6;
private static readonly Color4 colour_yellow = new Color4(255, 197, 40, 255);
private static readonly Color4 colour_orange = new Color4(252, 109, 1, 255);
private static readonly Color4 colour_pink = new Color4(213, 35, 90, 255);
private static readonly Color4 colour_purple = new Color4(203, 60, 236, 255);
private static readonly Color4 colour_cyan = new Color4(72, 198, 255, 255);
private static readonly Color4 colour_green = new Color4(100, 192, 92, 255);
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
{
int column = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(column);
int columnIndex = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(columnIndex);
switch (maniaLookup.Lookup)
{
@@ -87,53 +100,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(
stage.IsSpecialColumn(column) ? 120 : 60
stage.IsSpecialColumn(columnIndex) ? 120 : 60
));
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
Color4 colour;
const int total_colours = 7;
if (stage.IsSpecialColumn(column))
colour = new Color4(159, 101, 255, 255);
else
{
switch (column % total_colours)
{
case 0:
colour = new Color4(240, 216, 0, 255);
break;
case 1:
colour = new Color4(240, 101, 0, 255);
break;
case 2:
colour = new Color4(240, 0, 130, 255);
break;
case 3:
colour = new Color4(192, 0, 240, 255);
break;
case 4:
colour = new Color4(0, 96, 240, 255);
break;
case 5:
colour = new Color4(0, 226, 240, 255);
break;
case 6:
colour = new Color4(0, 240, 96, 255);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var colour = getColourForLayout(columnIndex, stage);
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
}
@@ -141,5 +113,203 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetConfig<TLookup, TValue>(lookup);
}
private Color4 getColourForLayout(int columnIndex, StageDefinition stage)
{
// Account for cases like dual-stage (assume that all stages have the same column count for now).
columnIndex %= stage.Columns;
// For now, these are defined per column count as per https://user-images.githubusercontent.com/50823728/218038463-b450f46c-ef21-4551-b133-f866be59970c.png
// See https://github.com/ppy/osu/discussions/21996 for discussion.
switch (stage.Columns)
{
case 1:
return colour_yellow;
case 2:
switch (columnIndex)
{
case 0: return colour_green;
case 1: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
case 3:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
default: throw new ArgumentOutOfRangeException();
}
case 4:
switch (columnIndex)
{
case 0: return colour_yellow;
case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 5:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
case 3: return colour_green;
case 4: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
case 6:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
case 3: return colour_cyan;
case 4: return colour_purple;
case 5: return colour_pink;
default: throw new ArgumentOutOfRangeException();
}
case 7:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_cyan;
case 2: return colour_pink;
case 3: return colour_special_column;
case 4: return colour_green;
case 5: return colour_cyan;
case 6: return colour_green;
default: throw new ArgumentOutOfRangeException();
}
case 8:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_yellow;
case 5: return colour_orange;
case 6: return colour_pink;
case 7: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 9:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_special_column;
case 5: return colour_yellow;
case 6: return colour_orange;
case 7: return colour_pink;
case 8: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 10:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_cyan;
case 5: return colour_green;
case 6: return colour_yellow;
case 7: return colour_orange;
case 8: return colour_pink;
case 9: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
}
// fallback for unhandled scenarios
if (stage.IsSpecialColumn(columnIndex))
return colour_special_column;
switch (columnIndex % total_colours)
{
case 0: return colour_yellow;
case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_purple;
case 4: return colour_cyan;
case 5: return colour_green;
default: throw new ArgumentOutOfRangeException();
}
}
}
}
@@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@@ -34,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable? lightContainer;
private Drawable? light;
private LegacyNoteBodyStyle? bodyStyle;
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)
return;
@@ -91,15 +102,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
d.Anchor = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
d.FillMode = FillMode.Stretch;
// Todo: Wrap
// Todo: Wrap?
});
if (bodySprite != null)
InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
}
protected override void LoadComplete()
@@ -160,8 +167,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
if (bodySprite != null)
{
bodySprite.Origin = Anchor.BottomCentre;
bodySprite.Scale = new Vector2(1, -1);
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Anchor = Anchor.BottomCentre; // needs to be flipped due to scale flip in Update.
}
if (light != null)
@@ -172,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null)
{
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = Vector2.One;
bodySprite.Anchor = Anchor.TopCentre;
}
if (light != null)
@@ -203,6 +210,33 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
// here we go...
switch (bodyStyle)
{
case LegacyNoteBodyStyle.Stretch:
// this is how lazer works by default. nothing required.
if (bodySprite != null)
bodySprite.Scale = new Vector2(1, scaleDirection);
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, scaleDirection * 32800 / sprite.DrawHeight);
}
break;
}
}
protected override void Dispose(bool isDisposing)
@@ -1,17 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public partial class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
{
private Slider slider;
private PathControlPointVisualiser visualiser;
private PathControlPointVisualiser<Slider> visualiser;
[SetUp]
public void Setup() => Schedule(() =>
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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,
Origin = Anchor.Centre
@@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}
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) =>
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) =>
AddStep($"move mouse to {relativePosition}", () =>
@@ -202,12 +202,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
moveMouseToControlPoint(2);
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));
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));
assertControlPointType(2, PathType.PerfectCurve);
@@ -236,12 +236,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
moveMouseToControlPoint(3);
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));
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.
// 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 TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(Slider slider)
: base(slider)
@@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(Slider slider)
: base(slider)
@@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestMovingUnsnappedSliderNodesSnaps()
{
PathControlPointPiece sliderEnd = null;
PathControlPointPiece<Slider> sliderEnd = null;
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
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);
});
AddStep("move slider end", () =>
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
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;
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("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);
});
AddStep("quick delete", () =>
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
private Slider? slider;
private PathControlPointVisualiser? visualiser;
private PathControlPointVisualiser<Slider>? visualiser;
private const double split_gap = 100;
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select added 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);
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select added 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);
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select added 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);
@@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutopilot : OsuModTestScene
{
[Test]
public void TestInstantResume()
{
CreateModTest(new ModTestData
{
Mod = new OsuModAutopilot(),
PassCondition = () => true,
Autoplay = false,
});
AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
AddStep("press pause", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait until paused", () => Player.GameplayClockContainer.IsPaused.Value);
AddStep("release pause", () => InputManager.ReleaseKey(Key.Escape));
AddStep("press resume", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait for resume", () => !Player.IsResuming);
AddAssert("resumed", () => !Player.GameplayClockContainer.IsPaused.Value);
}
}
}
@@ -5,9 +5,11 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
[Resolved]
private OsuConfigManager config { get; set; }
[Test]
public void TestHits()
{
@@ -56,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Tests
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)
{
var playfield = new TestOsuPlayfield();
@@ -4,29 +4,38 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public partial class TestSceneHitCircleKiai : TestSceneHitCircle
public partial class TestSceneHitCircleKiai : TestSceneHitCircle, IBeatSyncProvider
{
private ControlPointInfo controlPoints { get; set; }
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPoints = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPoints.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
ControlPointInfo = controlPoints
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => new ChannelAmplitudes();
ControlPointInfo IBeatSyncProvider.ControlPoints => controlPoints;
IClock IBeatSyncProvider.Clock => Clock;
}
}
@@ -0,0 +1,156 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneHitCircleLateFade : OsuTestScene
{
private float? alphaAtMiss;
[Test]
public void TestHitCircleClassicMod()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = new Mod[] { new OsuModClassic() };
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestHitCircleClassicAndFullHiddenMods()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModClassic() };
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestHitCircleClassicAndApproachCircleOnlyHiddenMods()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }, new OsuModClassic() };
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestHitCircleNoMod()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
}
[Test]
public void TestSliderClassicMod()
{
AddStep("Create slider", () =>
{
SelectedMods.Value = new Mod[] { new OsuModClassic() };
createSlider();
});
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Head circle transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestSliderNoMod()
{
AddStep("Create slider", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
createSlider();
});
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
}
private void createCircle()
{
alphaAtMiss = null;
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
{
StartTime = Time.Current + 500,
Position = new Vector2(250)
});
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawableHitCircle);
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableHitCircle.OnNewResult += (_, _) =>
{
alphaAtMiss = drawableHitCircle.Alpha;
};
Child = drawableHitCircle;
}
private void createSlider()
{
alphaAtMiss = null;
DrawableSlider drawableSlider = new DrawableSlider(new Slider
{
StartTime = Time.Current + 500,
Position = new Vector2(250),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(0, 100),
})
});
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableSlider.OnLoadComplete += _ =>
{
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
{
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
};
};
Child = drawableSlider;
}
}
}
@@ -13,7 +13,14 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
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.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.Tests.Visual;
using osuTK;
@@ -22,7 +29,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public partial class TestSceneTouchInput : OsuManualInputManagerTestScene
public partial class TestSceneOsuTouchInput : OsuManualInputManagerTestScene
{
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@@ -33,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private OsuInputManager osuInputManager = null!;
private Container mainContent = null!;
[SetUpSteps]
public void SetUpSteps()
{
@@ -44,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
{
Child = new Container
Child = mainContent = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -54,13 +63,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Depth = float.MinValue,
X = -100,
},
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Depth = float.MinValue,
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]
public void TestSimpleInput()
{
@@ -101,6 +150,42 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1);
}
[Test]
public void TestPositionalTrackingAfterLongDistanceTravelled()
{
// When a single touch has already travelled enough distance on screen, it should remain as the positional
// tracking touch until released (unless a direct touch occurs).
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
// cover some distance
beginTouch(TouchSource.Touch1, new Vector2(0));
beginTouch(TouchSource.Touch1, new Vector2(9999));
beginTouch(TouchSource.Touch1, new Vector2(0));
beginTouch(TouchSource.Touch1, new Vector2(9999));
beginTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkNotPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// in this case, touch 2 should not become the positional tracking touch.
checkPosition(TouchSource.Touch1);
// even if the second touch moves on the screen, the original tracking touch is retained.
beginTouch(TouchSource.Touch2, new Vector2(0));
beginTouch(TouchSource.Touch2, new Vector2(9999));
beginTouch(TouchSource.Touch2, new Vector2(0));
beginTouch(TouchSource.Touch2, new Vector2(9999));
checkPosition(TouchSource.Touch1);
}
[Test]
public void TestPositionalInputUpdatesOnlyFromMostRecentTouch()
{
@@ -116,9 +201,224 @@ namespace osu.Game.Rulesets.Osu.Tests
endTouch(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);
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);
// 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]
@@ -263,6 +563,22 @@ namespace osu.Game.Rulesets.Osu.Tests
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) =>
AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source))));
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -8,34 +8,36 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
/// <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>
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;
private readonly Path path;
private readonly Slider slider;
private readonly T hitObject;
public int ControlPointIndex { get; set; }
private IBindable<Vector2> sliderPosition;
private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion;
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
{
this.slider = slider;
this.hitObject = hitObject;
ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
ControlPoint = slider.Path.ControlPoints[controlPointIndex];
ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
InternalChild = path = new SmoothPath
{
@@ -48,10 +50,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.LoadComplete();
sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateConnectingPath());
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
pathVersion = slider.Path.Version.GetBoundCopy();
pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updateConnectingPath());
updateConnectingPath();
@@ -62,16 +64,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
private void updateConnectingPath()
{
Position = slider.StackedPosition + ControlPoint.Position;
Position = hitObject.StackedPosition + ControlPoint.Position;
path.ClearVertices();
int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count)
return;
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);
}
@@ -29,11 +29,13 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
/// <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>
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<DragEvent> DragInProgress;
@@ -44,34 +46,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint;
private readonly Slider slider;
private readonly T hitObject;
private readonly Container marker;
private readonly Drawable markerRing;
[Resolved]
private OsuColour colours { get; set; }
private IBindable<Vector2> sliderPosition;
private IBindable<float> sliderScale;
private IBindable<Vector2> hitObjectPosition;
private IBindable<float> hitObjectScale;
[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;
// we don't want to run the path type update on construction as it may inadvertently change the slider.
cachePoints(slider);
// we don't want to run the path type update on construction as it may inadvertently change the hit object.
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.
// this avoids inadvertently changing the slider path type for batch operations.
sliderVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
// this avoids inadvertently changing the hit object path type for batch operations.
hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
{
cachePoints(slider);
cachePoints(hitObject);
updatePathType();
}));
@@ -120,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.LoadComplete();
sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
hitObjectPosition.BindValueChanged(_ => updateMarkerDisplay());
sliderScale = slider.ScaleBindable.GetBoundCopy();
sliderScale.BindValueChanged(_ => updateMarkerDisplay());
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
hitObjectScale.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();
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
/// <summary>
/// Handles correction of invalid path types.
@@ -239,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
private void updateMarkerDisplay()
{
Position = slider.StackedPosition + ControlPoint.Position;
Position = hitObject.StackedPosition + ControlPoint.Position;
markerRing.Alpha = IsSelected.Value ? 1 : 0;
@@ -249,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
colour = colour.Lighten(1);
marker.Colour = colour;
marker.Scale = new Vector2(slider.Scale);
marker.Scale = new Vector2(hitObject.Scale);
}
private Color4 getColourFromNodeType()
@@ -29,15 +29,16 @@ using osuTK.Input;
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.
internal readonly Container<PathControlPointPiece> Pieces;
internal readonly Container<PathControlPointConnectionPiece> Connections;
internal readonly Container<PathControlPointPiece<T>> Pieces;
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly Slider slider;
private readonly T hitObject;
private readonly bool allowSelection;
private InputManager inputManager;
@@ -48,17 +49,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved(CanBeNull = true)]
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;
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }
Connections = new Container<PathControlPointConnectionPiece<T>> { 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();
controlPoints.CollectionChanged += onControlPointsChanged;
controlPoints.BindTo(slider.Path.ControlPoints);
controlPoints.BindTo(hitObject.Path.ControlPoints);
}
/// <summary>
/// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece"/>s.
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
/// </summary>
public void SetSelectionTo(PathControlPoint pathControlPoint)
{
@@ -124,8 +125,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return true;
}
private bool isSplittable(PathControlPointPiece p) =>
// A slider can only be split on control points which connect two different slider segments.
private bool isSplittable(PathControlPointPiece<T> p) =>
// 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();
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];
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
Pieces.Add(new PathControlPointPiece<T>(hitObject, point).With(d =>
{
if (allowSelection)
d.RequestSelection = selectionRequested;
@@ -160,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
d.DragEnded = dragEnded;
}));
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
}
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)
piece.IsSelected.Toggle();
@@ -234,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
/// <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>
private void updatePathType(PathControlPointPiece piece, PathType? type)
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
{
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
@@ -252,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
break;
}
slider.Path.ExpectedDistance.Value = null;
hitObject.Path.ExpectedDistance.Value = null;
piece.ControlPoint.Type = type;
}
@@ -268,9 +269,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private void dragStarted(PathControlPoint controlPoint)
{
dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray();
dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray();
draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint);
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray();
draggedControlPointIndex = hitObject.Path.ControlPoints.IndexOf(controlPoint);
selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint));
Debug.Assert(draggedControlPointIndex >= 0);
@@ -280,25 +281,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private void dragInProgress(DragEvent e)
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
double oldStartTime = slider.StartTime;
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = hitObject.Position;
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]));
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;
slider.StartTime = result?.Time ?? slider.StartTime;
hitObject.Position += movementDelta;
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];
// Since control points are relative to the position of the slider, all points that are _not_ selected
var controlPoint = hitObject.Path.ControlPoints[i];
// 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.
// 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).
@@ -310,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
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)
{
@@ -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.
slider.SnapTo(snapProvider);
hitObject.SnapTo(snapProvider);
if (!slider.Path.HasValidLength)
if (!hitObject.Path.HasValidLength)
{
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
hitObject.Position = oldPosition;
hitObject.StartTime = oldStartTime;
// Snap the path length again to undo the invalid length.
slider.SnapTo(snapProvider);
hitObject.SnapTo(snapProvider);
return;
}
// 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++)
slider.Path.ControlPoints[i].Type = dragPathTypes[i];
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
}
private void dragEnded() => changeHandler?.EndChange();
@@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
public Vector2 PathStartLocation => body.PathOffset;
/// <summary>
/// Offset in absolute (local) coordinates from the end of the curve.
/// </summary>
public Vector2 PathEndLocation => body.PathEndOffset;
public SliderBodyPiece()
{
InternalChild = body = new ManualSliderBody
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece;
private PathControlPointVisualiser controlPointVisualiser;
private PathControlPointVisualiser<Slider> controlPointVisualiser;
private InputManager inputManager;
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(),
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
controlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, false)
};
setState(SliderPlacementState.Initial);
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected SliderCircleOverlay TailOverlay { get; private set; }
[CanBeNull]
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
@@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
if (ControlPointVisualiser == null)
{
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, true)
{
RemoveControlPointsRequested = removeControlPoints,
SplitControlPointsRequested = splitControlPoints
@@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
{
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
};
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
@@ -187,28 +187,19 @@ namespace osu.Game.Rulesets.Osu.Edit
if (b.IsSelected)
continue;
var hitObject = (OsuHitObject)b.Item;
var snapPositions = b.ScreenSpaceSnapPoints;
Vector2? snap = checkSnap(hitObject.Position);
if (snap == null && hitObject.Position != hitObject.EndPosition)
snap = checkSnap(hitObject.EndPosition);
if (!snapPositions.Any())
continue;
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
snapResult = new SnapResult(snap.Value, null, playfield);
snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true;
}
Vector2? checkSnap(Vector2 checkPos)
{
Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos);
if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius)
return checkScreenPos;
return null;
}
}
snapResult = null;
@@ -11,6 +11,7 @@ using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double flash_duration = 1000;
private DrawableRuleset<OsuHitObject> ruleset = null!;
private DrawableOsuRuleset ruleset = null!;
protected OsuAction? LastAcceptedAction { get; private set; }
@@ -42,8 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
ruleset = (DrawableOsuRuleset)drawableRuleset;
ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAccuracyChallenge : ModAccuracyChallenge
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}
@@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
@@ -55,11 +56,13 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
drawableRuleset.UseResumeOverlay = false;
}
}
}
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -11,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
@@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
[SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")]
public Bindable<bool> FadeHitCircleEarly { get; } = new Bindable<bool>(true);
private bool usingHiddenFading;
public void ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
@@ -51,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods
if (ClassicNoteLock.Value)
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
}
public void ApplyToDrawableHitObject(DrawableHitObject obj)
@@ -59,12 +68,32 @@ namespace osu.Game.Rulesets.Osu.Mods
{
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(head);
break;
case DrawableSliderTail tail:
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
break;
case DrawableHitCircle circle:
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(circle);
break;
}
}
private void applyEarlyFading(DrawableHitCircle circle)
{
circle.ApplyCustomUpdateState += (o, _) =>
{
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
{
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
o.Delay(okWindow).FadeOut(lateMissFadeTime);
}
};
}
}
}
+2 -1
View File
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// grab the input manager for future use.
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
osuInputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
}
public void ApplyToPlayer(Player player)
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableOsuJudgement : DrawableJudgement
{
protected SkinnableLighting Lighting { get; private set; }
internal SkinnableLighting Lighting { get; private set; }
[Resolved]
private OsuConfigManager config { get; set; }
@@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PathVersion.UnbindFrom(HitObject.Path.Version);
slidingSample.Samples = null;
slidingSample?.ClearSamples();
}
protected override void LoadSamples()
@@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.OnFree();
spinningSample.Samples = null;
spinningSample.ClearSamples();
}
protected override void LoadSamples()
@@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
}
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
/// <summary>
/// Apply a judgement result.
/// </summary>
@@ -10,7 +10,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class SkinnableLighting : SkinnableSprite
internal partial class SkinnableLighting : SkinnableSprite
{
private DrawableHitObject targetObject;
private JudgementResult targetResult;
+2 -2
View File
@@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime }
: new SpinnerBonusTick { StartTime = startTime });
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
}
}
@@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
/// <summary>
/// Duration of the <see cref="Spinner"/> containing this spinner tick.
/// </summary>
public double SpinnerDuration { get; set; }
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override double MaximumJudgementOffset => SpinnerDuration;
public class OsuSpinnerTickJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.SmallBonus;
+9
View File
@@ -9,8 +9,10 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
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)
=> 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)
: base(ruleset, 0, SimultaneousBindingMode.Unique)
{
+2 -1
View File
@@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking()
new OsuModStrictTracking(),
new OsuModAccuracyChallenge(),
};
case ModType.Conversion:
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
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<int> indexInCurrentCombo = new Bindable<int>();
private readonly FlashPiece flash;
private readonly Container kiaiContainer;
private Bindable<bool> configHitLighting = null!;
[Resolved]
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
{
Size = new Vector2(OUTER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
innerGradient = new Circle // renders the inner bright gradient
{
Size = new Vector2(INNER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
innerFill = new Circle // renders the inner dark fill
{
Size = new Vector2(INNER_FILL_SIZE),
Alpha = 1,
Anchor = 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
{
Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
@@ -96,12 +108,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
configHitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
}
protected override void LoadComplete()
@@ -117,20 +131,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
outerGradient.ClearTransforms(targetMember: nameof(Colour));
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
kiaiContainer.Colour = colour.NewValue;
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
// Accent colour may be changed many times during a paused gameplay state.
// 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);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
}
private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value);
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
@@ -140,7 +159,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
case ArmedState.Hit:
// Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
const double fade_out_time = 800;
const double flash_in_duration = 150;
const double resize_duration = 400;
@@ -171,20 +189,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
// gradient layers.
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.
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
using (BeginDelayedSequence(flash_in_duration / 12))
{
outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf);
outerGradient
.FadeColour(Color4.White, 80)
.Then()
.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;
}
}
@@ -215,6 +253,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Child.AlwaysPresent = true;
}
public bool HitLighting { get; set; }
protected override void Update()
{
base.Update();
@@ -223,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
Type = EdgeEffectType.Glow,
Colour = Colour,
Radius = OsuHitObject.OBJECT_RADIUS * 1.2f,
Radius = OsuHitObject.OBJECT_RADIUS * (HitLighting ? 1.2f : 0.6f),
};
}
}
@@ -35,14 +35,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public void SetRotation(float currentRotation)
{
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
if (Precision.AlmostEquals(0, Time.Elapsed))
return;
// If we've gone back in time, it's fine to work with a fresh set of records for now
if (records.Count > 0 && Time.Current < records.Last().Time)
records.Clear();
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time))
return;
if (records.Count > 0)
{
var record = records.Peek();
@@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary>
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
/// <summary>
/// Offset in absolute coordinates from the end of the curve.
/// </summary>
public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
/// <summary>
/// Used to colour the path.
/// </summary>
@@ -252,13 +252,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
renderer.SetBlend(BlendingParameters.Additive);
renderer.PushLocalMatrix(DrawInfo.Matrix);
TextureShader.Bind();
BindTextureShader(renderer);
texture.Bind();
for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
TextureShader.Unbind();
UnbindTextureShader(renderer);
renderer.PopLocalMatrix();
}
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset;
public override Vector2 PathEndOffset => snakedPathEndOffset;
/// <summary>
/// The top-left position of the path when fully snaked.
/// </summary>
@@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary>
private Vector2 snakedPathOffset;
/// <summary>
/// The offset of the end of path from <see cref="snakedPosition"/> when fully snaked.
/// </summary>
private Vector2 snakedPathEndOffset;
private DrawableSlider drawableSlider = null!;
[BackgroundDependencyLoader]
@@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0;
+20 -2
View File
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
@@ -255,15 +256,23 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Source.parts.CopyTo(parts, 0);
}
private IUniformBuffer<CursorTrailParameters> cursorTrailParameters;
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
cursorTrailParameters ??= renderer.CreateUniformBuffer<CursorTrailParameters>();
cursorTrailParameters.Data = cursorTrailParameters.Data with
{
FadeClock = time,
FadeExponent = fadeExponent
};
shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
shader.BindUniformBlock("m_CursorTrailParameters", cursorTrailParameters);
texture.Bind();
@@ -323,6 +332,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
base.Dispose(isDisposing);
vertexBatch?.Dispose();
cursorTrailParameters?.Dispose();
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private record struct CursorTrailParameters
{
public UniformFloat FadeClock;
public UniformFloat FadeExponent;
private readonly UniformPadding8 pad1;
}
}
@@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
@@ -10,6 +10,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
@@ -21,6 +22,15 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>();
/// <summary>
/// The distance (in local pixels) that a touch must move before being considered a permanent tracking touch.
/// After this distance is covered, any extra touches on the screen will be considered as button inputs, unless
/// a new touch directly interacts with a hit circle.
/// </summary>
private const float distance_before_position_tracking_lock_in = 100;
private TrackedTouch? positionTrackingTouch;
private readonly OsuInputManager osuInputManager;
private Bindable<bool> mouseDisabled = null!;
@@ -38,6 +48,9 @@ namespace osu.Game.Rulesets.Osu.UI
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
}
// Required to handle touches outside of the playfield when screen scaling is enabled.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override void OnTouchMove(TouchMoveEvent e)
{
base.OnTouchMove(e);
@@ -53,7 +66,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.
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.
handleTouchMovement(e);
@@ -64,10 +85,53 @@ namespace osu.Game.Rulesets.Osu.UI
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 (and didn't travel across the screen too far).
if (!positionTrackingTouch.DirectTouch && positionTrackingTouch.DistanceTravelled < distance_before_position_tracking_lock_in)
{
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 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.Action is OsuAction touchAction)
{
osuInputManager.KeyBindingContainer.TriggerReleased(touchAction);
positionTrackingTouch.Action = null;
}
}
private void handleTouchMovement(TouchEvent touchEvent)
{
if (touchEvent is TouchMoveEvent moveEvent)
{
var trackedTouch = trackedTouches.Single(t => t.Source == touchEvent.Touch.Source);
trackedTouch.DistanceTravelled += moveEvent.Delta.Length;
}
// Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != trackedTouches.Last().Source)
if (touchEvent.Touch.Source != positionTrackingTouch?.Source)
return;
if (!osuInputManager.AllowUserCursorMovement)
@@ -83,6 +147,9 @@ namespace osu.Game.Rulesets.Osu.UI
if (tracked.Action is OsuAction action)
osuInputManager.KeyBindingContainer.TriggerReleased(action);
if (positionTrackingTouch == tracked)
positionTrackingTouch = null;
trackedTouches.Remove(tracked);
base.OnTouchUp(e);
@@ -92,12 +159,23 @@ namespace osu.Game.Rulesets.Osu.UI
{
public readonly TouchSource Source;
public readonly OsuAction? Action;
public OsuAction? Action;
public TrackedTouch(TouchSource source, OsuAction? action)
/// <summary>
/// Whether the touch was on a hit circle receptor.
/// </summary>
public readonly bool DirectTouch;
/// <summary>
/// The total distance on screen travelled by this touch (in local pixels).
/// </summary>
public float DistanceTravelled;
public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch)
{
Source = source;
Action = action;
DirectTouch = directTouch;
}
}
}
@@ -4,6 +4,7 @@
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description>
<LangVersion>10</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Nuget">

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