1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 02:32:55 +08:00

Merge branch 'master' into random-mod-slider-rotation

This commit is contained in:
Henry Lin 2022-05-26 22:51:34 +08:00 committed by GitHub
commit e205aeff38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
777 changed files with 17299 additions and 7602 deletions

View File

@ -2,14 +2,8 @@
"version": 1, "version": 1,
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-format": {
"version": "3.1.37601",
"commands": [
"dotnet-format"
]
},
"jetbrains.resharper.globaltools": { "jetbrains.resharper.globaltools": {
"version": "2020.3.2", "version": "2022.1.1",
"commands": [ "commands": [
"jb" "jb"
] ]
@ -27,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2022.320.0", "version": "2022.417.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -1,6 +1,14 @@
# EditorConfig is awesome: http://editorconfig.org # EditorConfig is awesome: http://editorconfig.org
root = true root = true
[*.{csproj,props,targets}]
charset = utf-8-bom
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.cs] [*.cs]
end_of_line = crlf end_of_line = crlf
insert_final_newline = true insert_final_newline = true
@ -8,8 +16,19 @@ indent_style = space
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
#license header
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
#Roslyn naming styles #Roslyn naming styles
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members #camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_style.camelcase.capitalization = camel_case
@ -172,23 +191,11 @@ csharp_style_prefer_index_operator = false:silent
csharp_style_prefer_range_operator = false:silent csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers [*.{yaml,yml}]
# Suppress: EC112 insert_final_newline = true
indent_style = space
#Private method is unused indent_size = 2
dotnet_diagnostic.IDE0051.severity = silent trim_trailing_whitespace = true
#Private member is unused
dotnet_diagnostic.IDE0052.severity = silent
#Rules for disposable
dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none
dotnet_diagnostic.IDE0069.severity = none
#Disable operator overloads requiring alternate named methods
dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
dotnet_diagnostic.OLOC001.words_in_name = 5
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.

View File

@ -9,7 +9,7 @@ body:
Important to note that your issue may have already been reported before. Please check: Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues. - Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful. - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown - type: dropdown
attributes: attributes:
@ -48,20 +48,28 @@ body:
Attaching log files is required for every reported bug. See instructions below on how to find them. Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
### Desktop platforms
If the game has not yet been closed since you found the bug: If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder" 1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there 2. Then open the `logs` folder located there
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. The default places to find the logs on desktop platforms are as follows:
The default places to find the logs are as follows:
- `%AppData%/osu/logs` *on Windows* - `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS* - `~/.local/share/osu/logs` *on Linux & macOS*
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there. If you have selected a custom location for the game files, you can find the `logs` folder there.
### Mobile platforms
The places to find the logs on mobile platforms are as follows:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below. After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea - type: textarea

View File

@ -2,6 +2,59 @@ on: [push, pull_request]
name: Continuous Integration name: Continuous Integration
jobs: jobs:
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: Restore inspectcode cache
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }}
- name: Dotnet code style
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
test: test:
name: Test name: Test
runs-on: ${{matrix.os.fullname}} runs-on: ${{matrix.os.fullname}}
@ -25,15 +78,6 @@ jobs:
with: with:
dotnet-version: "6.0.x" dotnet-version: "6.0.x"
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
# https://github.com/ppy/osu-framework/issues/4349
# Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
- name: Install libavformat-dev
if: ${{matrix.os.fullname == 'ubuntu-latest'}}
run: |
sudo apt-get update && \
sudo apt-get -y install libavformat-dev
- name: Compile - name: Compile
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
@ -94,51 +138,3 @@ jobs:
# Build just the main game for now. # Build just the main game for now.
- name: Build - name: Build
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors

26
.github/workflows/sentry-release.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Add Release to Sentry
on:
push:
tags:
- '*'
jobs:
sentry_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ppy
SENTRY_PROJECT: osu
SENTRY_URL: https://sentry.ppy.sh/
with:
environment: production
version: ${{ github.ref }}

2
.gitignore vendored
View File

@ -340,3 +340,5 @@ inspectcode
# Fody (pulled in by Realm) - schema file # Fody (pulled in by Realm) - schema file
FodyWeavers.xsd FodyWeavers.xsd
**/FodyWeavers.xml **/FodyWeavers.xml
.idea/.idea.osu.Desktop/.idea/misc.xml

55
.globalconfig Normal file
View File

@ -0,0 +1,55 @@
is_global = true
# .NET Code Style
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
# IDE0001: Simplify names
dotnet_diagnostic.IDE0001.severity = warning
# IDE0002: Simplify member access
dotnet_diagnostic.IDE0002.severity = warning
# IDE0003: Remove qualification
dotnet_diagnostic.IDE0003.severity = warning
# IDE0004: Remove unnecessary cast
dotnet_diagnostic.IDE0004.severity = warning
# IDE0005: Remove unnecessary imports
dotnet_diagnostic.IDE0005.severity = warning
# IDE0034: Simplify default literal
dotnet_diagnostic.IDE0034.severity = warning
# IDE0036: Sort modifiers
dotnet_diagnostic.IDE0036.severity = warning
# IDE0040: Add accessibility modifier
dotnet_diagnostic.IDE0040.severity = warning
# IDE0049: Use keyword for type name
dotnet_diagnostic.IDE0040.severity = warning
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = warning
# IDE0051: Private method is unused
dotnet_diagnostic.IDE0051.severity = silent
# IDE0052: Private member is unused
dotnet_diagnostic.IDE0052.severity = silent
# IDE0073: File header
dotnet_diagnostic.IDE0073.severity = warning
# IDE0130: Namespace mismatch with folder
dotnet_diagnostic.IDE0130.severity = warning
# IDE1006: Naming style
dotnet_diagnostic.IDE1006.severity = warning
#Disable operator overloads requiring alternate named methods
dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>
</project>

View File

@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead. M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.

View File

@ -1,4 +1,4 @@
<!-- Contains required properties for osu!framework projects. --> <!-- Contains required properties for osu!framework projects. -->
<Project> <Project>
<PropertyGroup Label="C#"> <PropertyGroup Label="C#">
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
@ -26,14 +26,6 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Project">
<!--
NU1701:
DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway.
This is required due to https://github.com/NuGet/Home/issues/5740
-->
<NoWarn>$(NoWarn);NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Nuget"> <PropertyGroup Label="Nuget">
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Authors>ppy Pty Ltd</Authors> <Authors>ppy Pty Ltd</Authors>
@ -42,7 +34,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company> <Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2021 ppy Pty Ltd</Copyright> <Copyright>Copyright (c) 2022 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags> <PackageTags>osu game</PackageTags>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -5,7 +5,7 @@ dotnet tool restore
# - cmd: dotnet format --dry-run --check # - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE exit $LASTEXITCODE

View File

@ -2,5 +2,5 @@
dotnet tool restore dotnet tool restore
dotnet CodeFileSanity dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors

View File

@ -1,4 +1,4 @@
Copyright (c) 2021 ppy Pty Ltd <contact@ppy.sh>. Copyright (c) 2022 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img width="500px" src="assets/lazer.png"> <img width="500" alt="osu! logo" src="assets/lazer.png">
</p> </p>
# osu! # osu!
@ -8,6 +8,7 @@
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest) [![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/osu-web/localized.svg)](https://crowdin.com/project/osu-web)
A free-to-win rhythm game. Rhythm is just a *click* away! A free-to-win rhythm game. Rhythm is just a *click* away!
@ -31,7 +32,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:** **Latest build:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
| ------------- | ------------- | ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. - The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
@ -104,6 +105,8 @@ When it comes to contributing to the project, the two main things you can do to
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible. Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
## Licence ## Licence

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl> <PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2021 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2022 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description> <Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags> <PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>

View File

@ -1,4 +1,4 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
<OutputPath>bin\$(Configuration)</OutputPath> <OutputPath>bin\$(Configuration)</OutputPath>
@ -51,11 +51,11 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.525.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="Realm" Version="10.11.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,8 +4,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc; using osu.Desktop.LegacyIpc;
using osu.Framework; using osu.Framework;
using osu.Framework.Development; using osu.Framework.Development;
@ -63,8 +61,6 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
{ {
host.ExceptionThrown += handleException;
if (!host.IsPrimaryInstance) if (!host.IsPrimaryInstance)
{ {
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
@ -123,26 +119,13 @@ namespace osu.Desktop
tools.RemoveUninstallerRegistryEntry(); tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) => }, onEveryRun: (version, tools, firstRun) =>
{ {
tools.SetProcessAppUserModelId(); // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly.
//
// This may turn out to be non-required after an alternative solution is implemented.
// see https://github.com/clowd/Clowd.Squirrel/issues/24
// tools.SetProcessAppUserModelId();
}); });
} }
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
/// <summary>
/// Allow a maximum of one unhandled exception, per second of execution.
/// </summary>
/// <param name="arg"></param>
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
}
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component public class ElevatedPrivilegesChecker : Component
{ {
[Resolved] [Resolved]
private NotificationOverlay notifications { get; set; } private INotificationOverlay notifications { get; set; }
private bool elevated; private bool elevated;

View File

@ -25,7 +25,7 @@ namespace osu.Desktop.Updater
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{ {
private UpdateManager updateManager; private UpdateManager updateManager;
private NotificationOverlay notificationOverlay; private INotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@ -39,9 +39,9 @@ namespace osu.Desktop.Updater
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(NotificationOverlay notification) private void load(INotificationOverlay notifications)
{ {
notificationOverlay = notification; notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
} }

View File

@ -24,8 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" /> <PackageReference Include="Clowd.Squirrel" Version="2.9.40" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
@ -33,7 +32,6 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.0.175" /> <PackageReference Include="DiscordRichPresence" Version="1.0.175" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">

View File

@ -11,12 +11,13 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description> <description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes> <releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2021 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2022 ppy Pty Ltd</copyright>
<language>en-AU</language> <language>en-AU</language>
</metadata> </metadata>
<files> <files>
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/> <file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
<file src="**.dll" target="lib\net45\"/> <file src="**.dll" target="lib\net45\"/>
<file src="**.config" target="lib\net45\"/> <file src="**.config" target="lib\net45\"/>
<file src="**.json" target="lib\net45\"/>
</files> </files>
</package> </package>

View File

@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="nunit" Version="3.13.2" /> <PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
</ItemGroup> </ItemGroup>

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.0505463516206195d, "diffcalc-test")] [TestCase(4.0505463516206195d, 127, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(5.1696411260785498d, "diffcalc-test")] [TestCase(5.1696411260785498d, 127, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new CatchModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);

View File

@ -29,13 +29,14 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected CatchSelectionBlueprintTestScene() protected CatchSelectionBlueprintTestScene()
{ {
EditorBeatmap = new EditorBeatmap(new CatchBeatmap var catchBeatmap = new CatchBeatmap
{ {
BeatmapInfo = BeatmapInfo =
{ {
Ruleset = new CatchRuleset().RulesetInfo, Ruleset = new CatchRuleset().RulesetInfo,
} }
}) { Difficulty = { CircleSize = 0 } }; };
EditorBeatmap = new EditorBeatmap(catchBeatmap) { Difficulty = { CircleSize = 0 } };
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{ {
BeatLength = 100 BeatLength = 100

View File

@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMoveStep(end_time, 0); AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left); AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0); AddMoveStep(start_time, 0);
AddAssert("duration is positive", () => ((BananaShower)CurrentBlueprint.HitObject).Duration > 0);
AddClickStep(MouseButton.Right); AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time)); AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time)); AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
{ {
private const double velocity = 0.5; private const double velocity_factor = 0.5;
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
var playable = base.GetPlayableBeatmap(); var playable = base.GetPlayableBeatmap();
playable.Difficulty.SliderTickRate = 5; playable.Difficulty.SliderTickRate = 5;
playable.Difficulty.SliderMultiplier = velocity * 10; playable.Difficulty.SliderMultiplier = velocity_factor * 10;
return playable; return playable;
} }
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
} }
[Test] [Test]
public void TestVelocityLimit() public void TestSliderVelocityChange()
{ {
double[] times = { 100, 300 }; double[] times = { 100, 300 };
float[] positions = { 200, 500 }; float[] positions = { 200, 500 };
addPlacementSteps(times, positions); addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300 }); addPathCheckStep(times, positions);
}
[Test] AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
public void TestPreviousVerticesAreFixed()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 200, 400, 100, 500 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
} }
[Test] [Test]
public void TestClampedPositionIsRestored() public void TestClampedPositionIsRestored()
{ {
double[] times = { 100, 300, 500 }; double[] times = { 100, 300, 500 };
float[] positions = { 200, 200, 0, 250 }; float[] positions = { 200, 200, -3000, 250 };
addMoveAndClickSteps(times[0], positions[0]); addMoveAndClickSteps(times[0], positions[0]);
addMoveAndClickSteps(times[1], positions[1]); addMoveAndClickSteps(times[1], positions[1]);
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPathCheckStep(times, new float[] { 200, 200, 250 }); addPathCheckStep(times, new float[] { 200, 200, 250 });
} }
[Test]
public void TestFirstVertexIsFixed()
{
double[] times = { 100, 200 };
float[] positions = { 100, 300 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 100, 150 });
}
[Test] [Test]
public void TestOutOfOrder() public void TestOutOfOrder()
{ {

View File

@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
} }
[Test] [Test]
public void TestClampedPositionIsRestored() public void TestSliderVelocityChange()
{ {
const double velocity = 0.25; double[] times = { 100, 300 };
double[] times = { 100, 500, 700 }; float[] positions = { 200, 300 };
float[] positions = { 100, 100, 100 }; addBlueprintStep(times, positions);
addBlueprintStep(times, positions, velocity); AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddMouseMoveStep(times[1], 200); AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
addVertexCheckStep(3, 1, times[1], 200);
addVertexCheckStep(3, 2, times[2], 150);
AddMouseMoveStep(times[1], 100);
addVertexCheckStep(3, 1, times[1], 100);
// Stored position is restored.
addVertexCheckStep(3, 2, times[2], positions[2]);
AddMouseMoveStep(times[1], 300);
addDragEndStep();
addDragStartStep(times[1], 300);
AddMouseMoveStep(times[1], 100);
// Position is different because a changed position is committed when the previous drag is ended.
addVertexCheckStep(3, 2, times[2], 250);
} }
[Test] [Test]
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150); addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150); addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 220); addAddVertexSteps(90, 200);
addVertexCheckStep(4, 1, times[0], positions[0]); addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(750, 180); addAddVertexSteps(750, 180);
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
var path = new JuiceStreamPath(); var path = new JuiceStreamPath();
for (int i = 1; i < times.Length; i++) for (int i = 1; i < times.Length; i++)
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]); path.Add(times[i] - times[0], positions[i] - positions[0]);
var sliderPath = new SliderPath(); var sliderPath = new SliderPath();
path.ConvertToSliderPath(sliderPath, 0); path.ConvertToSliderPath(sliderPath, 0, velocity);
addBlueprintStep(times[0], positions[0], sliderPath, velocity); addBlueprintStep(times[0], positions[0], sliderPath, velocity);
} }
@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () => private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
{ {
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity; double expectedTime = time - hitObject.StartTime;
float expectedX = x - hitObject.OriginalX; float expectedX = x - hitObject.OriginalX;
var vertices = getVertices(); var vertices = getVertices();
return vertices.Count == count && return vertices.Count == count &&
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) && Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
Precision.AlmostEquals(vertices[index].X, expectedX); Precision.AlmostEquals(vertices[index].X, expectedX);
}); });

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
case 0: case 0:
{ {
double distance = rng.NextDouble() * scale * 2 - scale; double time = rng.NextDouble() * scale * 2 - scale;
if (integralValues) if (integralValues)
distance = Math.Round(distance); time = Math.Round(time);
float oldX = path.PositionAtDistance(distance); float oldX = path.PositionAtTime(time);
int index = path.InsertVertex(distance); int index = path.InsertVertex(time);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1)); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX)); Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
break; break;
} }
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
case 1: case 1:
{ {
int index = rng.Next(path.Vertices.Count); int index = rng.Next(path.Vertices.Count);
double distance = path.Vertices[index].Distance; double time = path.Vertices[index].Time;
float newX = (float)(rng.NextDouble() * scale * 2 - scale); float newX = (float)(rng.NextDouble() * scale * 2 - scale);
if (integralValues) if (integralValues)
newX = MathF.Round(newX); newX = MathF.Round(newX);
path.SetVertexPosition(index, newX); path.SetVertexPosition(index, newX);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount)); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
Assert.That(path.Vertices[index].X, Is.EqualTo(newX)); Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
break; break;
} }
} }
assertInvariants(path.Vertices, checkSlope); assertInvariants(path.Vertices);
} }
} }
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(10, 5); path.Add(10, 5);
path.Add(20, -5); path.Add(20, -5);
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1); int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
Assert.That(removeCount, Is.EqualTo(1)); Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[] Assert.That(path.Vertices, Is.EqualTo(new[]
{ {
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
})); }));
} }
[Test] [TestCase(10)]
public void TestRandomConvertFromSliderPath() [TestCase(0.1)]
public void TestRandomConvertFromSliderPath(double velocity)
{ {
var rng = new Random(1); var rng = new Random(1);
var path = new JuiceStreamPath(); var path = new JuiceStreamPath();
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
else else
sliderPath.ExpectedDistance.Value = null; sliderPath.ExpectedDistance.Value = null;
path.ConvertFromSliderPath(sliderPath); path.ConvertFromSliderPath(sliderPath, velocity);
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0)); Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3)); Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices);
double[] sampleDistances = Enumerable.Range(0, 10) double[] sampleTimes = Enumerable.Range(0, 10)
.Select(_ => rng.NextDouble() * sliderPath.Distance) .Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
.ToArray(); .ToArray();
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
} }
path.ResampleVertices(sampleDistances); path.ResampleVertices(sampleTimes);
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices);
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
} }
} }
} }
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
do do
{ {
double distance = rng.NextDouble() * 1e3; double time = rng.NextDouble() * 1e3;
float x = (float)(rng.NextDouble() * 1e3); float x = (float)(rng.NextDouble() * 1e3);
path.Add(distance, x); path.Add(time, x);
} while (rng.Next(5) != 0); } while (rng.Next(5) != 0);
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT); float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
path.ConvertToSliderPath(sliderPath, sliderStartY); double requiredVelocity = path.ComputeRequiredVelocity();
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); double velocity = Math.Clamp(requiredVelocity, 1, 100);
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true); path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
foreach (var point in sliderPath.ControlPoints) foreach (var point in sliderPath.ControlPoints)
{ {
@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
} }
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
// The path is preserved only if required velocity is used.
if (velocity < requiredVelocity) continue;
Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
double distance = rng.NextDouble() * path.Distance; double time = rng.NextDouble() * path.Duration;
float expected = path.PositionAtDistance(distance); float expected = path.PositionAtTime(time);
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3)); Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
} }
} }
} }
@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(20, 0); path.Add(20, 0);
checkNewId(); checkNewId();
path.RemoveVertices((v, _) => v.Distance == 20); path.RemoveVertices((v, _) => v.Time == 20);
checkNewId(); checkNewId();
path.ResampleVertices(new double[] { 5, 10, 15 }); path.ResampleVertices(new double[] { 5, 10, 15 });
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Clear(); path.Clear();
checkNewId(); checkNewId();
path.ConvertFromSliderPath(new SliderPath()); path.ConvertFromSliderPath(new SliderPath(), 1);
checkNewId(); checkNewId();
void checkNewId() void checkNewId()
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
} }
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope) private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
{ {
Assert.That(vertices, Is.Not.Empty); Assert.That(vertices, Is.Not.Empty);
for (int i = 0; i < vertices.Count; i++) for (int i = 0; i < vertices.Count; i++)
{ {
Assert.That(double.IsFinite(vertices[i].Distance)); Assert.That(double.IsFinite(vertices[i].Time));
Assert.That(float.IsFinite(vertices[i].X)); Assert.That(float.IsFinite(vertices[i].X));
} }
for (int i = 1; i < vertices.Count; i++) for (int i = 1; i < vertices.Count; i++)
{ {
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance)); Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
if (!checkSlope) continue;
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
} }
} }
} }

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -5,10 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Catch.Beatmaps namespace osu.Game.Rulesets.Catch.Beatmaps
{ {
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public void ApplyPositionOffsets(IBeatmap beatmap) public void ApplyPositionOffsets(IBeatmap beatmap)
{ {
var rng = new FastRandom(RNG_SEED); var rng = new LegacyRandom(RNG_SEED);
float? lastPosition = null; float? lastPosition = null;
double lastStartTime = 0; double lastStartTime = 0;
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
initialiseHyperDash(beatmap); initialiseHyperDash(beatmap);
} }
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, LegacyRandom rng)
{ {
float offsetPosition = hitObject.OriginalX; float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime; double startTime = hitObject.StartTime;
@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
/// <param name="position">The position which the offset should be applied to.</param> /// <param name="position">The position which the offset should be applied to.</param>
/// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param> /// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param>
/// <param name="rng">The random number generator.</param> /// <param name="rng">The random number generator.</param>
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng) private static void applyRandomOffset(ref float position, double maxOffset, LegacyRandom rng)
{ {
bool right = rng.NextBool(); bool right = rng.NextBool();
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))); float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -13,11 +14,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
private readonly TimeSpanOutline outline; private readonly TimeSpanOutline outline;
private double placementStartTime;
private double placementEndTime;
public BananaShowerPlacementBlueprint() public BananaShowerPlacementBlueprint()
{ {
InternalChild = outline = new TimeSpanOutline(); InternalChild = outline = new TimeSpanOutline();
} }
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -38,13 +49,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
case PlacementState.Active: case PlacementState.Active:
if (e.Button != MouseButton.Right) break; if (e.Button != MouseButton.Right) break;
// If the duration is negative, swap the start and the end time to make the duration positive.
if (HitObject.Duration < 0)
{
HitObject.StartTime = HitObject.EndTime;
HitObject.Duration = -HitObject.Duration;
}
EndPlacement(HitObject.Duration > 0); EndPlacement(HitObject.Duration > 0);
return true; return true;
} }
@ -61,13 +65,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
switch (PlacementActive) switch (PlacementActive)
{ {
case PlacementState.Waiting: case PlacementState.Waiting:
HitObject.StartTime = time; placementStartTime = placementEndTime = time;
break; break;
case PlacementState.Active: case PlacementState.Active:
HitObject.EndTime = time; placementEndTime = time;
break; break;
} }
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
} }
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public int VertexCount => path.Vertices.Count; public int VertexCount => path.Vertices.Count;
protected readonly Func<float, double> PositionToDistance; protected readonly Func<float, double> PositionToTime;
protected IReadOnlyList<VertexState> VertexStates => vertexStates; protected IReadOnlyList<VertexState> VertexStates => vertexStates;
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull] [CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatSnapProvider beatSnapProvider { get; set; }
protected EditablePath(Func<float, double> positionToDistance) protected EditablePath(Func<float, double> positionToTime)
{ {
PositionToDistance = positionToDistance; PositionToTime = positionToTime;
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
while (InternalChildren.Count < path.Vertices.Count) while (InternalChildren.Count < path.Vertices.Count)
AddInternal(new VertexPiece()); AddInternal(new VertexPiece());
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
for (int i = 0; i < VertexCount; i++) for (int i = 0; i < VertexCount; i++)
{ {
var piece = (VertexPiece)InternalChildren[i]; var piece = (VertexPiece)InternalChildren[i];
var vertex = path.Vertices[i]; var vertex = path.Vertices[i];
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor)); piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
piece.UpdateFrom(vertexStates[i]); piece.UpdateFrom(vertexStates[i]);
} }
} }
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void InitializeFromHitObject(JuiceStream hitObject) public void InitializeFromHitObject(JuiceStream hitObject)
{ {
var sliderPath = hitObject.Path; var sliderPath = hitObject.Path;
path.ConvertFromSliderPath(sliderPath); path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear)) if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
{ {
path.ResampleVertices(hitObject.NestedHitObjects path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity)); .Select(h => h.StartTime - hitObject.StartTime));
} }
vertexStates.Clear(); vertexStates.Clear();
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject) public void UpdateHitObjectFromPath(JuiceStream hitObject)
{ {
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY); // The SV setting may need to be changed for the current path.
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity();
// The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
// but it should be always rounded up to satisfy the required minimum velocity condition.
//
// This is rounded to integers instead of using the precision of the bindable
// because it results in a smaller number of non-redundant control points.
//
// The value is clamped here by the bindable min and max values.
// In case the required velocity is too large, the path is not preserved.
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
if (beatSnapProvider == null) return; if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity; double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
} }
@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
protected int AddVertex(double distance, float x) protected int AddVertex(double time, float x)
{ {
int index = path.InsertVertex(distance); int index = path.InsertVertex(time);
path.SetVertexPosition(index, x); path.SetVertexPosition(index, x);
vertexStates.Insert(index, new VertexState()); vertexStates.Insert(index, new VertexState());
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
return true; return true;
} }
protected void MoveSelectedVertices(double distanceDelta, float xDelta) protected void MoveSelectedVertices(double timeDelta, float xDelta)
{ {
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well. // Because the vertex list may be reordered due to time change, the state list must be reordered as well.
previousVertexStates.Clear(); previousVertexStates.Clear();
previousVertexStates.AddRange(vertexStates); previousVertexStates.AddRange(vertexStates);
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 1; i < vertexCount; i++) for (int i = 1; i < vertexCount; i++)
{ {
var state = previousVertexStates[i]; var state = previousVertexStates[i];
double distance = state.VertexBeforeChange.Distance; double time = state.VertexBeforeChange.Time;
if (state.IsSelected) if (state.IsSelected)
distance += distanceDelta; time += timeDelta;
int newIndex = path.InsertVertex(Math.Max(0, distance)); int newIndex = path.InsertVertex(Math.Max(0, time));
vertexStates.Insert(newIndex, state); vertexStates.Insert(newIndex, state);
} }

View File

@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
/// </summary> /// </summary>
private JuiceStreamPathVertex lastVertex; private JuiceStreamPathVertex lastVertex;
public PlacementEditablePath(Func<float, double> positionToDistance) public PlacementEditablePath(Func<float, double> positionToTime)
: base(positionToDistance) : base(positionToTime)
{ {
} }
public void AddNewVertex() public void AddNewVertex()
{ {
var endVertex = Vertices[^1]; var endVertex = Vertices[^1];
int index = AddVertex(endVertex.Distance, endVertex.X); int index = AddVertex(endVertex.Time, endVertex.X);
for (int i = 0; i < VertexCount; i++) for (int i = 0; i < VertexCount; i++)
{ {
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void MoveLastVertex(Vector2 screenSpacePosition) public void MoveLastVertex(Vector2 screenSpacePosition)
{ {
Vector2 position = ToRelativePosition(screenSpacePosition); Vector2 position = ToRelativePosition(screenSpacePosition);
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance; double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
float xDelta = position.X - lastVertex.X; float xDelta = position.X - lastVertex.X;
MoveSelectedVertices(distanceDelta, xDelta); MoveSelectedVertices(timeDelta, xDelta);
} }
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
private readonly Path drawablePath; private readonly Path drawablePath;
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>(); private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
public ScrollingPath() public ScrollingPath()
{ {
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{ {
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
computeDistanceXs(hitObject); computeTimeXs(hitObject);
drawablePath.Vertices = vertices drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor))) .Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
.ToArray(); .ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero); drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
} }
private void computeDistanceXs(JuiceStream hitObject) private void computeTimeXs(JuiceStream hitObject)
{ {
vertices.Clear(); vertices.Clear();
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (sliderVertices.Count == 0) if (sliderVertices.Count == 0)
return; return;
double distance = 0; double time = 0;
Vector2 lastPosition = Vector2.Zero; Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++) for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{ {
foreach (var position in sliderVertices) foreach (var position in sliderVertices)
{ {
distance += Vector2.Distance(lastPosition, position); time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
lastPosition = position; lastPosition = position;
vertices.Add((distance, position.X)); vertices.Add((time, position.X));
} }
sliderVertices.Reverse(); sliderVertices.Reverse();

View File

@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull] [CanBeNull]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToDistance) public SelectionEditablePath(Func<float, double> positionToTime)
: base(positionToDistance) : base(positionToTime)
{ {
} }
public void AddVertex(Vector2 relativePosition) public void AddVertex(Vector2 relativePosition)
{ {
double distance = Math.Max(0, PositionToDistance(relativePosition.Y)); double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(distance, relativePosition.X); int index = AddVertex(time, relativePosition.X);
selectOnly(index); selectOnly(index);
} }
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition); Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y); double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
float xDelta = mousePosition.X - dragStartPosition.X; float xDelta = mousePosition.X - dragStartPosition.X;
MoveSelectedVertices(distanceDelta, xDelta); MoveSelectedVertices(timeDelta, xDelta);
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new PlacementEditablePath(positionToDistance) editablePath = new PlacementEditablePath(positionToTime)
}; };
} }
@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
base.LoadComplete(); base.LoadComplete();
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
BeginPlacement();
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
@ -119,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
lastEditablePathId = editablePath.PathId; lastEditablePathId = editablePath.PathId;
} }
private double positionToDistance(float relativeYPosition) private double positionToTime(float relativeYPosition)
{ {
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity; return time - HitObject.StartTime;
} }
} }
} }

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToDistance) editablePath = new SelectionEditablePath(positionToTime)
}; };
} }
@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius); return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
} }
private double positionToDistance(float relativeYPosition) private double positionToTime(float relativeYPosition)
{ {
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity; return time - HitObject.StartTime;
} }
private void initializeJuiceStreamPath() private void initializeJuiceStreamPath()

View File

@ -6,6 +6,7 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
@ -24,7 +25,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject> public class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
{ {
private const float distance_snap_radius = 50; private const float distance_snap_radius = 50;
@ -42,6 +43,10 @@ namespace osu.Game.Rulesets.Catch.Edit
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder LayerBelowRuleset.Add(new PlayfieldBorder
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -85,16 +90,20 @@ namespace osu.Game.Rulesets.Catch.Edit
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
}); });
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{ {
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
result.ScreenSpacePosition.X = screenSpacePosition.X; result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlagFast(SnapType.Grids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{ {
result = snapResult; result = snapResult;
} }
}
return result; return result;
} }

View File

@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; } public int RepeatCount { get; set; }
[JsonIgnore] [JsonIgnore]
public double Velocity { get; private set; } private double velocityFactor;
[JsonIgnore] [JsonIgnore]
public double TickDistance { get; private set; } private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.
@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// However, the <see cref="SliderPath"/> representation is difficult to work with. /// However, the <see cref="SliderPath"/> representation is difficult to work with.
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s. /// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
/// </para> /// </para>
/// <para>
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
/// and this slope condition is always maintained as an invariant.
/// </para>
/// </summary> /// </summary>
public class JuiceStreamPath public class JuiceStreamPath
{ {
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
public int InvalidationID { get; private set; } = 1; public int InvalidationID { get; private set; } = 1;
/// <summary> /// <summary>
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>. /// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
/// </summary> /// </summary>
public double Distance => vertices[^1].Distance - vertices[0].Distance; public double Duration => vertices[^1].Time - vertices[0].Time;
/// <remarks> /// <remarks>
/// This list should always be non-empty. /// This list should always be non-empty.
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
}; };
/// <summary> /// <summary>
/// Compute the x-position of the path at the given <paramref name="distance"/>. /// Compute the x-position of the path at the given <paramref name="time"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned, /// When the given time is outside of the path, the x position at the corresponding endpoint is returned,
/// </remarks> /// </remarks>
public float PositionAtDistance(double distance) public float PositionAtTime(double time)
{ {
int index = vertexIndexAtDistance(distance); int index = vertexIndexAtTime(time);
return positionAtDistance(distance, index); return positionAtTime(time, index);
} }
/// <summary> /// <summary>
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
/// <summary> /// <summary>
/// Insert a vertex at given <paramref name="distance"/>. /// Insert a vertex at given <paramref name="time"/>.
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex. /// The <see cref="PositionAtTime"/> is used as the position of the new vertex.
/// Thus, the set of points of the path is not changed (up to floating-point precision). /// Thus, the set of points of the path is not changed (up to floating-point precision).
/// </summary> /// </summary>
/// <returns>The index of the new vertex.</returns> /// <returns>The index of the new vertex.</returns>
public int InsertVertex(double distance) public int InsertVertex(double time)
{ {
if (!double.IsFinite(distance)) if (!double.IsFinite(time))
throw new ArgumentOutOfRangeException(nameof(distance)); throw new ArgumentOutOfRangeException(nameof(time));
int index = vertexIndexAtDistance(distance); int index = vertexIndexAtTime(time);
float x = positionAtDistance(distance, index); float x = positionAtTime(time, index);
vertices.Insert(index, new JuiceStreamPathVertex(distance, x)); vertices.Insert(index, new JuiceStreamPathVertex(time, x));
invalidate(); invalidate();
return index; return index;
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary> /// <summary>
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>. /// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
/// </summary> /// </summary>
public void SetVertexPosition(int index, float newX) public void SetVertexPosition(int index, float newX)
{ {
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
if (!float.IsFinite(newX)) if (!float.IsFinite(newX))
throw new ArgumentOutOfRangeException(nameof(newX)); throw new ArgumentOutOfRangeException(nameof(newX));
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX); vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
{
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
}
for (int i = index + 1; i < vertices.Count; i++)
{
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
}
vertices[index] = newVertex;
invalidate(); invalidate();
} }
/// <summary> /// <summary>
/// Add a new vertex at given <paramref name="distance"/> and position. /// Add a new vertex at given <paramref name="time"/> and position.
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
/// </summary> /// </summary>
public void Add(double distance, float x) public void Add(double time, float x)
{ {
int index = InsertVertex(distance); int index = InsertVertex(time);
SetVertexPosition(index, x); SetVertexPosition(index, x);
} }
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
/// <summary> /// <summary>
/// Recreate this path by using difference set of vertices at given distances. /// Recreate this path by using difference set of vertices at given time points.
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path. /// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved. /// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
/// </summary> /// </summary>
public void ResampleVertices(IEnumerable<double> sampleDistances) public void ResampleVertices(IEnumerable<double> sampleTimes)
{ {
var sampledVertices = new List<JuiceStreamPathVertex>(); var sampledVertices = new List<JuiceStreamPathVertex>();
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
if (!double.IsFinite(distance)) if (!double.IsFinite(time))
throw new ArgumentOutOfRangeException(nameof(sampleDistances)); throw new ArgumentOutOfRangeException(nameof(sampleTimes));
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance); double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
float x = PositionAtDistance(clampedDistance); float x = PositionAtTime(clampedTime);
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x)); sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
} }
sampledVertices.Sort(); sampledVertices.Sort();
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <remarks> /// <remarks>
/// Duplicated vertices are automatically removed. /// Duplicated vertices are automatically removed.
/// </remarks> /// </remarks>
public void ConvertFromSliderPath(SliderPath sliderPath) public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
{ {
var sliderPathVertices = new List<Vector2>(); var sliderPathVertices = new List<Vector2>();
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1); sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
double distance = 0; double time = 0;
vertices.Clear(); vertices.Clear();
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X)); vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
for (int i = 1; i < sliderPathVertices.Count; i++) for (int i = 1; i < sliderPathVertices.Count; i++)
{ {
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]); time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
if (!Precision.AlmostEquals(vertices[^1].Distance, distance)) if (!Precision.AlmostEquals(vertices[^1].Time, time))
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X)); Add(time, sliderPathVertices[i].X);
} }
invalidate(); invalidate();
} }
/// <summary>
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
/// </summary>
public double ComputeRequiredVelocity()
{
double maximumSlope = 0;
for (int i = 1; i < vertices.Count; i++)
{
double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X);
double timeDifference = vertices[i].Time - vertices[i - 1].Time;
// A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero.
if (Precision.AlmostEquals(timeDifference, 0))
continue;
maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference);
}
return maximumSlope;
}
/// <summary> /// <summary>
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>. /// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>. /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
///
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
/// </summary> /// </summary>
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY) public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
{ {
const float margin = 1; const float margin = 1;
// Note: these two variables and `sliderPath` are modified by the local functions. // Note: these two variables and `sliderPath` are modified by the local functions.
double currentDistance = 0; double currentTime = 0;
Vector2 lastPosition = new Vector2(vertices[0].X, 0); Vector2 lastPosition = new Vector2(vertices[0].X, 0);
sliderPath.ControlPoints.Clear(); sliderPath.ControlPoints.Clear();
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
sliderPath.ControlPoints[^1].Type = PathType.Linear; sliderPath.ControlPoints[^1].Type = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X; float deltaX = vertices[i].X - lastPosition.X;
double length = vertices[i].Distance - currentDistance; double length = (vertices[i].Time - currentTime) * velocity;
// Should satisfy `deltaX^2 + deltaY^2 = length^2`. // Should satisfy `deltaX^2 + deltaY^2 = length^2`.
// By invariants, the expression inside the `sqrt` is (almost) non-negative. // The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX)); double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
// When `deltaY` is small, one segment is always enough. // When `deltaY` is small, one segment is always enough.
@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
Vector2 nextPosition = new Vector2(nextX, nextY); Vector2 nextPosition = new Vector2(nextX, nextY);
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition)); sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
currentDistance += Vector2.Distance(lastPosition, nextPosition); currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
lastPosition = nextPosition; lastPosition = nextPosition;
} }
} }
/// <summary> /// <summary>
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted. /// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
/// </summary> /// </summary>
private int vertexIndexAtDistance(double distance) private int vertexIndexAtTime(double time)
{ {
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed. // The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity)); int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
return i < 0 ? ~i : i; return i < 0 ? ~i : i;
} }
/// <summary> /// <summary>
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>. /// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
/// </summary> /// </summary>
private float positionAtDistance(double distance, int index) private float positionAtTime(double time, int index)
{ {
if (index <= 0) if (index <= 0)
return vertices[0].X; return vertices[0].X;
if (index >= vertices.Count) if (index >= vertices.Count)
return vertices[^1].X; return vertices[^1].X;
double length = vertices[index].Distance - vertices[index - 1].Distance; double duration = vertices[index].Time - vertices[index - 1].Time;
if (Precision.AlmostEquals(length, 0)) if (Precision.AlmostEquals(duration, 0))
return vertices[index].X; return vertices[index].X;
float deltaX = vertices[index].X - vertices[index - 1].X; float deltaX = vertices[index].X - vertices[index - 1].X;
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length)); return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
}
/// <summary>
/// Check the two vertices can connected directly while satisfying the slope condition.
/// </summary>
private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
{
double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
return xDistance <= length + allowance;
}
/// <summary>
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
/// </summary>
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
{
float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
} }
private void invalidate() => InvalidationID++; private void invalidate() => InvalidationID++;

View File

@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex> public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
{ {
public readonly double Distance; public readonly double Time;
public readonly float X; public readonly float X;
public JuiceStreamPathVertex(double distance, float x) public JuiceStreamPathVertex(double time, float x)
{ {
Distance = distance; Time = time;
X = x; X = x;
} }
public int CompareTo(JuiceStreamPathVertex other) public int CompareTo(JuiceStreamPathVertex other)
{ {
int c = Distance.CompareTo(other.Distance); int c = Time.CompareTo(other.Time);
return c != 0 ? c : X.CompareTo(other.X); return c != 0 ? c : X.CompareTo(other.X);
} }
public override string ToString() => $"({Distance}, {X})"; public override string ToString() => $"({Time}, {X})";
} }
} }

View File

@ -90,6 +90,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyHitExplosion(); return new LegacyHitExplosion();
return null; return null;
default:
throw new UnsupportedSkinComponentException(component);
} }
} }

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -98,37 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
set => InternalChild = value; set => InternalChild = value;
} }
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new System.NotImplementedException();
}
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
{
throw new System.NotImplementedException();
}
public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -45,6 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
(typeof(EditorBeatmap), editorBeatmap), (typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap), (typeof(IBeatSnapProvider), editorBeatmap),
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
}, },
Child = new ComposeScreen { State = { Value = Visibility.Visible } }, Child = new ComposeScreen { State = { Value = Visibility.Visible } },
}; };

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, "diffcalc-test")] [TestCase(2.3449735700206298d, 151, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, "diffcalc-test")] [TestCase(2.7879104989252959d, 151, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new ManiaModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -11,8 +11,8 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osu.Game.Utils;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Beatmaps namespace osu.Game.Rulesets.Mania.Beatmaps
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private readonly int originalTargetColumns; private readonly int originalTargetColumns;
// Internal for testing purposes // Internal for testing purposes
internal FastRandom Random { get; private set; } internal LegacyRandom Random { get; private set; }
private Pattern lastPattern = new Pattern(); private Pattern lastPattern = new Pattern();
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
IBeatmapDifficultyInfo difficulty = original.Difficulty; IBeatmapDifficultyInfo difficulty = original.Difficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed); Random = new LegacyRandom(seed);
return base.ConvertBeatmap(original, cancellationToken); return base.ConvertBeatmap(original, cancellationToken);
} }
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary> /// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{ {
public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{ {
} }

View File

@ -8,12 +8,12 @@ using System.Linq;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType; private PatternType convertType;
public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{ {
convertType = PatternType.None; convertType = PatternType.None;

View File

@ -2,13 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Linq; using System.Linq;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly int endTime; private readonly int endTime;
private readonly PatternType convertType; private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{ {
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0); endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);

View File

@ -9,11 +9,11 @@ using osuTK;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly PatternType convertType; private readonly PatternType convertType;
public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density,
PatternType lastStair, IBeatmap originalBeatmap) PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{ {

View File

@ -5,8 +5,8 @@ using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary> /// <summary>
/// The random number generator to use. /// The random number generator to use.
/// </summary> /// </summary>
protected readonly FastRandom Random; protected readonly LegacyRandom Random;
/// <summary> /// <summary>
/// The beatmap which <see cref="HitObject"/> is being converted from. /// The beatmap which <see cref="HitObject"/> is being converted from.
/// </summary> /// </summary>
protected readonly IBeatmap OriginalBeatmap; protected readonly IBeatmap OriginalBeatmap;
protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern) : base(hitObject, beatmap, previousPattern)
{ {
if (random == null) throw new ArgumentNullException(nameof(random)); if (random == null) throw new ArgumentNullException(nameof(random));

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
private const double individual_decay_base = 0.125; private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30; private const double overall_decay_base = 0.30;
private const double release_threshold = 24;
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1; protected override double StrainDecayBase => 1;
@ -37,31 +38,43 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
var maniaCurrent = (ManiaDifficultyHitObject)current; var maniaCurrent = (ManiaDifficultyHitObject)current;
double endTime = maniaCurrent.EndTime; double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column; int column = maniaCurrent.BaseObject.Column;
double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
bool isOverlapping = false;
// Fill up the holdEndTimes array // Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i) for (int i = 0; i < holdEndTimes.Length; ++i)
{ {
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut... // The current note is overlapped if a previous note or end is overlapping the current note body
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
holdAddition = 0;
// We give a slight bonus to everything if something is held meanwhile // We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25; holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
// Decay individual strains // Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base); individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
} }
holdEndTimes[column] = endTime; holdEndTimes[column] = endTime;
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
// ^
// 1.0 + - - - - - -+-----------
// | /
// 0.5 + - - - - -/ Sigmoid Curve
// | /|
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
// Increase individual strain in own column // Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor; individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column]; individualStrain = individualStrains[column];

View File

@ -5,7 +5,10 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
base.UpdateTimeAndPosition(result); base.UpdateTimeAndPosition(result);
if (result.Playfield is Column col)
{
// Apply an offset to better align with the visual grid.
// This should only be applied during placement, as during selection / drag operations the movement is relative
// to the initial point of interaction rather than the grid.
switch (col.ScrollingInfo.Direction.Value)
{
case ScrollingDirection.Down:
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2);
break;
case ScrollingDirection.Up:
result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2);
break;
}
if (PlacementActive == PlacementState.Waiting) if (PlacementActive == PlacementState.Waiting)
Column = result.Playfield as Column; Column = col;
} }
} }
private float getNoteHeight(Column resultPlayfield) =>
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
}
} }

View File

@ -1,15 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
Playfield.GetColumnByPosition(screenSpacePosition); Playfield.GetColumnByPosition(screenSpacePosition);
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
switch (ScrollingInfo.Direction.Value)
{
case ScrollingDirection.Down:
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
break;
case ScrollingDirection.Up:
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
break;
}
return result;
}
private float getNoteHeight() =>
Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{ {
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods); drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
@ -112,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Edit
} }
else else
{ {
var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
if (result.Time is double time) if (result.Time is double time)
beatSnapGrid.SelectionTimeRange = (time, time); beatSnapGrid.SelectionTimeRange = (time, time);
else else

View File

@ -1,95 +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;
namespace osu.Game.Rulesets.Mania.MathUtils
{
/// <summary>
/// A PRNG specified in http://heliosphan.org/fastrandom.html.
/// </summary>
internal class FastRandom
{
private const double int_to_real = 1.0 / (int.MaxValue + 1.0);
private const uint int_mask = 0x7FFFFFFF;
private const uint y = 842502087;
private const uint z = 3579807591;
private const uint w = 273326509;
internal uint X { get; private set; }
internal uint Y { get; private set; } = y;
internal uint Z { get; private set; } = z;
internal uint W { get; private set; } = w;
public FastRandom(int seed)
{
X = (uint)seed;
}
public FastRandom()
: this(Environment.TickCount)
{
}
/// <summary>
/// Generates a random unsigned integer within the range [<see cref="uint.MinValue"/>, <see cref="uint.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public uint NextUInt()
{
uint t = X ^ (X << 11);
X = Y;
Y = Z;
Z = W;
return W = W ^ (W >> 19) ^ t ^ (t >> 8);
}
/// <summary>
/// Generates a random integer value within the range [0, <see cref="int.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public int Next() => (int)(int_mask & NextUInt());
/// <summary>
/// Generates a random integer value within the range [0, <paramref name="upperBound"/>).
/// </summary>
/// <param name="upperBound">The upper bound.</param>
/// <returns>The random value.</returns>
public int Next(int upperBound) => (int)(NextDouble() * upperBound);
/// <summary>
/// Generates a random integer value within the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The lower bound of the range.</param>
/// <param name="upperBound">The upper bound of the range.</param>
/// <returns>The random value.</returns>
public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
/// <summary>
/// Generates a random double value within the range [0, 1).
/// </summary>
/// <returns>The random value.</returns>
public double NextDouble() => int_to_real * Next();
private uint bitBuffer;
private int bitIndex = 32;
/// <summary>
/// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls.
/// </summary>
/// <returns>The random value.</returns>
public bool NextBool()
{
if (bitIndex == 32)
{
bitBuffer = NextUInt();
bitIndex = 1;
return (bitBuffer & 1) == 1;
}
bitIndex++;
return ((bitBuffer >>= 1) & 1) == 1;
}
}
}

View File

@ -116,9 +116,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
case ManiaSkinComponents.StageForeground: case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground(); return new LegacyStageForeground();
}
break; default:
throw new UnsupportedSkinComponentException(component);
}
} }
return base.GetDrawableComponent(component); return base.GetDrawableComponent(component);

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -7,11 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -24,19 +23,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene
{ {
private const double beat_length = 100; private const float beat_length = 100;
private static readonly Vector2 grid_position = new Vector2(512, 384); private static readonly Vector2 grid_position = new Vector2(512, 384);
[Cached(typeof(EditorBeatmap))] [Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly EditorClock editorClock;
[Cached] [Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
[Cached(typeof(IPositionSnapProvider))] [Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider(); private readonly OsuHitObjectComposer snapProvider = new OsuHitObjectComposer(new OsuRuleset())
{
// Just used for the snap implementation, so let's hide from vision.
AlwaysPresent = true,
Alpha = 0,
};
private TestOsuDistanceSnapGrid grid; private OsuDistanceSnapGrid grid;
public TestSceneOsuDistanceSnapGrid() public TestSceneOsuDistanceSnapGrid()
{ {
@ -47,14 +56,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo
} }
}); });
editorClock = new EditorClock(editorBeatmap);
base.Content.Children = new Drawable[]
{
snapProvider,
Content
};
} }
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
editorBeatmap.Difficulty.SliderMultiplier = 1; editorBeatmap.Difficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
snapProvider.DistanceSpacingMultiplier.Value = 1;
Children = new Drawable[] Children = new Drawable[]
{ {
@ -63,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray Colour = Color4.SlateGray
}, },
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }), grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
}; };
}); });
@ -81,25 +101,52 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor); AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor);
} }
[TestCase(1.0f)]
[TestCase(2.0f)]
[TestCase(0.5f)]
public void TestDistanceSpacing(float multiplier)
{
AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier);
}
[Test] [Test]
public void TestCursorInCentre() public void TestCursorInCentre()
{ {
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position))); AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
assertSnappedDistance((float)beat_length); assertSnappedDistance(beat_length);
}
[Test]
public void TestCursorAlmostInCentre()
{
AddStep("move mouse to almost centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position) + new Vector2(1)));
assertSnappedDistance(beat_length);
} }
[Test] [Test]
public void TestCursorBeforeMovementPoint() public void TestCursorBeforeMovementPoint()
{ {
AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f))); AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 1.45f)));
assertSnappedDistance((float)beat_length); assertSnappedDistance(beat_length);
} }
[Test] [Test]
public void TestCursorAfterMovementPoint() public void TestCursorAfterMovementPoint()
{ {
AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f))); AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 1.55f)));
assertSnappedDistance((float)beat_length * 2); assertSnappedDistance(beat_length * 2);
}
[TestCase(0.5f, beat_length * 2)]
[TestCase(1, beat_length * 2)]
[TestCase(1.5f, beat_length * 1.5f)]
[TestCase(2f, beat_length * 2)]
public void TestDistanceSpacingAdjust(float multiplier, float expectedDistance)
{
AddStep($"Set distance spacing to {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier);
AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2)));
assertSnappedDistance(expectedDistance);
} }
[Test] [Test]
@ -114,13 +161,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray Colour = Color4.SlateGray
}, },
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }), grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
}; };
}); });
AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f))); AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 3f)));
assertSnappedDistance((float)beat_length * 2); assertSnappedDistance(beat_length);
} }
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () => private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
@ -136,6 +183,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private readonly Drawable cursor; private readonly Drawable cursor;
private InputManager inputManager;
public override bool HandlePositionalInput => true;
public SnappingCursorContainer() public SnappingCursorContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -152,49 +203,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
base.LoadComplete(); base.LoadComplete();
updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); inputManager = GetContainingInputManager();
} }
protected override bool OnMouseMove(MouseMoveEvent e) protected override void Update()
{ {
base.OnMouseMove(e); base.Update();
cursor.Position = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
updatePosition(e.ScreenSpaceMousePosition); }
return true;
}
private void updatePosition(Vector2 screenSpacePosition)
{
cursor.Position = GetSnapPosition.Invoke(screenSpacePosition);
}
}
private class TestOsuDistanceSnapGrid : OsuDistanceSnapGrid
{
public new float DistanceSpacing => base.DistanceSpacing;
public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
: base(hitObject, nextHitObject)
{
}
}
private class SnapProvider : IPositionSnapProvider
{
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
} }
} }
} }

View File

@ -1,114 +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;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor
{
[Resolved]
private OsuConfigManager config { get; set; }
[Test]
public void TestHitCircleAnimationDisable()
{
HitCircle hitCircle = null;
DrawableHitCircle drawableHitCircle = null;
AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0));
toggleAnimations(true);
seekSmoothlyTo(() => hitCircle.StartTime + 10);
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
assertFutureTransforms(() => drawableHitCircle.CirclePiece, true);
AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1));
toggleAnimations(false);
seekSmoothlyTo(() => hitCircle.StartTime + 10);
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
assertFutureTransforms(() => drawableHitCircle.CirclePiece, false);
AddAssert("hit circle has longer fade-out applied", () =>
{
var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha));
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
});
}
[Test]
public void TestSliderAnimationDisable()
{
Slider slider = null;
DrawableSlider drawableSlider = null;
DrawableSliderRepeat sliderRepeat = null;
AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0));
toggleAnimations(true);
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
retrieveDrawables();
assertFutureTransforms(() => sliderRepeat, true);
AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1));
toggleAnimations(false);
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
retrieveDrawables();
assertFutureTransforms(() => sliderRepeat.Arrow, false);
seekSmoothlyTo(() => slider.GetEndTime());
AddAssert("slider has longer fade-out applied", () =>
{
var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha));
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
});
void retrieveDrawables() =>
AddStep("retrieve drawables", () =>
{
drawableSlider = (DrawableSlider)getDrawableObjectFor(slider);
sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType<SliderRepeat>().First());
});
}
private HitCircle getHitCircle(int index)
=> EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(index);
private Slider getSliderWithRepeats(int index)
=> EditorBeatmap.HitObjects.OfType<Slider>().Where(s => s.RepeatCount >= 1).ElementAt(index);
private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
=> this.ChildrenOfType<DrawableHitObject>().Single(ho => ho.HitObject == hitObject);
private IEnumerable<Transform> getTransformsRecursively(Drawable drawable)
=> drawable.ChildrenOfType<Drawable>().SelectMany(d => d.Transforms);
private void toggleAnimations(bool enabled)
=> AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
private void seekSmoothlyTo(Func<double> targetTime)
{
AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
}
private void assertFutureTransforms(Func<Drawable> getDrawable, bool hasFutureTransforms)
=> AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
() => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
}
}

View File

@ -0,0 +1,108 @@
// 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 enable
using System;
using System.Diagnostics;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene
{
private static readonly object?[][] texture_priority_cases =
{
// default priority lookup
new object?[]
{
// available textures
new[] { @"hitcircle", @"hitcircleoverlay" },
// priority lookup prefix
null,
// expected circle and overlay
@"hitcircle", @"hitcircleoverlay",
},
// custom priority lookup
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"sliderstartcircle", @"sliderstartcircleoverlay",
},
// when no sprites are available for the specified prefix, fall back to "hitcircle"/"hitcircleoverlay".
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
},
// when a circle is available for the specified prefix but no overlay exists, no overlay is displayed.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle" },
@"sliderstartcircle",
@"sliderstartcircle", null
},
// when no circle is available for the specified prefix but an overlay exists, the overlay is ignored.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
}
};
[TestCaseSource(nameof(texture_priority_cases))]
public void TestTexturePriorities(string[] textureFilenames, string priorityLookup, string? expectedCircle, string? expectedOverlay)
{
TestLegacyMainCirclePiece piece = null!;
AddStep("load circle piece", () =>
{
var skin = new Mock<ISkinSource>();
// shouldn't be required as GetTexture(string) calls GetTexture(string, WrapMode, WrapMode) by default,
// but moq doesn't handle that well, therefore explicitly requiring to use `CallBase`:
// https://github.com/moq/moq4/issues/972
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[] { (typeof(ISkinSource), skin.Object) },
Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
};
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2);
});
AddAssert("check circle sprite", () => piece.CircleSprite?.Texture?.AssetName == expectedCircle);
AddAssert("check overlay sprite", () => piece.OverlaySprite?.Texture?.AssetName == expectedOverlay);
}
private class TestLegacyMainCirclePiece : LegacyMainCirclePiece
{
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
: base(priorityLookupPrefix, false)
{
}
}
}
}

View File

@ -16,31 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public class TestSceneOsuModAlternate : OsuModTestScene public class TestSceneOsuModAlternate : OsuModTestScene
{ {
[Test]
public void TestInputAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
[Test] [Test]
public void TestInputAlternating() => CreateModTest(new ModTestData public void TestInputAlternating() => CreateModTest(new ModTestData
{ {
@ -116,17 +91,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
} }
}); });
/// <summary>
/// Ensures alternation is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputSingularAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// first press during intro.
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
// press same key at hitobject and ensure it has been hit.
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
/// <summary>
/// Ensures alternation is reset before the first hitobject after a break.
/// </summary>
[Test] [Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{ {
Mod = new OsuModAlternate(), Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2, PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false, Autoplay = false,
Beatmap = new Beatmap Beatmap = new Beatmap
{ {
Breaks = new List<BreakPeriod> Breaks = new List<BreakPeriod>
{ {
new BreakPeriod(500, 2250), new BreakPeriod(500, 2000),
}, },
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -138,16 +146,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new HitCircle new HitCircle
{ {
StartTime = 2500, StartTime = 2500,
Position = new Vector2(100), Position = new Vector2(500, 100),
} },
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
} }
}, },
ReplayFrames = new List<ReplayFrame> ReplayFrames = new List<ReplayFrame>
{ {
// first press to start alternate lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)), new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton), // press same key after break but before hit object.
new OsuReplayFrame(2501, new Vector2(100)), new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(2251, new Vector2(300, 100)),
// press same key at second hitobject and ensure it has been hit.
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(500, 100)),
// press same key at third hitobject and ensure it has been missed.
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(3001, new Vector2(500, 100)),
} }
}); });
} }

View File

@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public class TestSceneOsuModAimAssist : OsuModTestScene public class TestSceneOsuModMagnetised : OsuModTestScene
{ {
[TestCase(0.1f)] [TestCase(0.1f)]
[TestCase(0.5f)] [TestCase(0.5f)]
[TestCase(1)] [TestCase(1)]
public void TestAimAssist(float strength) public void TestMagnetised(float strength)
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new OsuModAimAssist Mod = new OsuModMagnetised
{ {
AssistStrength = { Value = strength }, AttractionStrength = { Value = strength },
}, },
PassCondition = () => true, PassCondition = () => true,
Autoplay = false, Autoplay = false,

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
MuteComboCount = { Value = 0 }, MuteComboCount = { Value = 0 },
}, },
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 && PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 &&
Player.ChildrenOfType<Metronome>().SingleOrDefault()?.AggregateVolume.Value == 1.0, Player.ChildrenOfType<MetronomeBeat>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
}); });
/// <summary> /// <summary>

View File

@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, "diffcalc-test")] [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, "zero-length-sliders")] [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, "diffcalc-test")] [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, "zero-length-sliders")] [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);

View File

@ -70,7 +70,9 @@ namespace osu.Game.Rulesets.Osu.Tests
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo()); var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin) var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = dho = new DrawableSlider(prepareObject(new Slider Child = dho = new DrawableSlider(prepareObject(new Slider

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps namespace osu.Game.Rulesets.Osu.Beatmaps
@ -20,13 +21,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Circle Count", Name = BeatmapsetsStrings.ShowStatsCountCircles,
Content = circles.ToString(), Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Slider Count", Name = BeatmapsetsStrings.ShowStatsCountSliders,
Content = sliders.ToString(), Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
}, },

View File

@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
@ -125,6 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
new OsuModEasy(), new OsuModEasy(),
new OsuModHardRock(), new OsuModHardRock(),
new OsuModFlashlight(), new OsuModFlashlight(),
new MultiMod(new OsuModFlashlight(), new OsuModHidden())
}; };
} }
} }

View File

@ -219,9 +219,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0; double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
if (score.Mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));

View File

@ -4,6 +4,7 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
@ -85,6 +86,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
setDistances(clockRate); setDistances(clockRate);
} }
public double OpacityAt(double time, bool hidden)
{
if (time > BaseObject.StartTime)
{
// Consider a hitobject as being invisible when its start time is passed.
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
// but this is an approximation and such a case is unlikely to be hit where this function is used.
return 0.0;
}
double fadeInStartTime = BaseObject.StartTime - BaseObject.TimePreempt;
double fadeInDuration = BaseObject.TimeFadeIn;
if (hidden)
{
// Taken from OsuModHidden.
double fadeOutStartTime = BaseObject.StartTime - BaseObject.TimePreempt + BaseObject.TimeFadeIn;
double fadeOutDuration = BaseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
return Math.Min
(
Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0),
1.0 - Math.Clamp((time - fadeOutStartTime) / fadeOutDuration, 0.0, 1.0)
);
}
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
}
private void setDistances(double clockRate) private void setDistances(double clockRate)
{ {
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -17,13 +19,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public Flashlight(Mod[] mods) public Flashlight(Mod[] mods)
: base(mods) : base(mods)
{ {
hidden = mods.Any(m => m is OsuModHidden);
} }
private double skillMultiplier => 0.07; private double skillMultiplier => 0.05;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0; protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
private readonly bool hidden;
private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2;
private double currentStrain; private double currentStrain;
private double strainValueOf(DifficultyHitObject current) private double strainValueOf(DifficultyHitObject current)
@ -61,13 +69,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// We also want to nerf stacks so that only the first object of the stack is accounted for. // We also want to nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0); double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; // Bonus based on how visible the object is.
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
} }
lastObj = currentObj; lastObj = currentObj;
} }
return Math.Pow(smallDistNerf * result, 2.0); result = Math.Pow(smallDistNerf * result, 2.0);
// Additional bonus for Hidden due to there being no approach circles.
if (hidden)
result *= 1.0 + hidden_bonus;
return result;
} }
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);

View File

@ -38,7 +38,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double difficulty = 0; double difficulty = 0;
double weight = 1; double weight = 1;
List<double> strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
List<double> strains = peaks.OrderByDescending(d => d).ToList();
// We are reducing the highest strains first to account for extreme difficulty spikes // We are reducing the highest strains first to account for extreme difficulty spikes
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++) for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Screens.Edit;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
{
public class HitCircleOverlapMarker : BlueprintPiece<HitCircle>
{
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
public const double FADE_OUT_EXTENSION = 700;
private readonly RingPiece ring;
[Resolved]
private EditorClock editorClock { get; set; }
public HitCircleOverlapMarker()
{
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
InternalChildren = new Drawable[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
},
ring = new RingPiece
{
BorderThickness = 4,
}
};
}
[Resolved]
private ISkinSource skin { get; set; }
public override void UpdateFrom(HitCircle hitObject)
{
base.UpdateFrom(hitObject);
Scale = new Vector2(hitObject.Scale);
if (hitObject is IHasComboInformation combo)
ring.BorderColour = combo.GetComboColour(skin);
double editorTime = editorClock.CurrentTime;
double hitObjectTime = hitObject.StartTime;
bool hasReachedObject = editorTime >= hitObjectTime;
if (hasReachedObject)
{
float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In);
float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
ring.Scale = new Vector2(1 + 0.1f * ringScale);
Alpha = 0.9f * (1 - alpha);
}
else
Alpha = 0;
}
public override void Hide()
{
// intentional no op so we are not hidden when not selected.
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -14,11 +15,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected new DrawableHitCircle DrawableObject => (DrawableHitCircle)base.DrawableObject; protected new DrawableHitCircle DrawableObject => (DrawableHitCircle)base.DrawableObject;
protected readonly HitCirclePiece CirclePiece; protected readonly HitCirclePiece CirclePiece;
private readonly HitCircleOverlapMarker marker;
public HitCircleSelectionBlueprint(HitCircle circle) public HitCircleSelectionBlueprint(HitCircle circle)
: base(circle) : base(circle)
{ {
InternalChild = CirclePiece = new HitCirclePiece(); InternalChildren = new Drawable[]
{
marker = new HitCircleOverlapMarker(),
CirclePiece = new HitCirclePiece(),
};
} }
protected override void Update() protected override void Update()
@ -26,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
base.Update(); base.Update();
CirclePiece.UpdateFrom(HitObject); CirclePiece.UpdateFrom(HitObject);
marker.UpdateFrom(HitObject);
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);

View File

@ -1,19 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{ {
public abstract class OsuSelectionBlueprint<T> : HitObjectSelectionBlueprint<T> public abstract class OsuSelectionBlueprint<T> : HitObjectSelectionBlueprint<T>
where T : OsuHitObject where T : OsuHitObject
{ {
[Resolved]
private EditorClock editorClock { get; set; }
protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject; protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject;
protected override bool AlwaysShowWhenSelected => true; protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject) protected OsuSelectionBlueprint(T hitObject)
: base(hitObject) : base(hitObject)
{ {

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> RemoveControlPointsRequested; public Action<List<PathControlPoint>> RemoveControlPointsRequested;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPositionSnapProvider snapProvider { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
public PathControlPointVisualiser(Slider slider, bool allowSelection) public PathControlPointVisualiser(Slider slider, bool allowSelection)
{ {
@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
// 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 slider changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = snapProvider?.SnapScreenSpacePositionToValidTime(newHeadPosition); var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position; Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -13,20 +14,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly Slider slider; private readonly Slider slider;
private readonly SliderPosition position; private readonly SliderPosition position;
private readonly HitCircleOverlapMarker marker;
public SliderCircleOverlay(Slider slider, SliderPosition position) public SliderCircleOverlay(Slider slider, SliderPosition position)
{ {
this.slider = slider; this.slider = slider;
this.position = position; this.position = position;
InternalChild = CirclePiece = new HitCirclePiece(); InternalChildren = new Drawable[]
{
marker = new HitCircleOverlapMarker(),
CirclePiece = new HitCirclePiece(),
};
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle); var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
CirclePiece.UpdateFrom(circle);
marker.UpdateFrom(circle);
}
public override void Hide()
{
CirclePiece.Hide();
}
public override void Show()
{
CirclePiece.Show();
} }
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength; private int currentSegmentLength;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
public SliderPlacementBlueprint() public SliderPlacementBlueprint()
: base(new Objects.Slider()) : base(new Objects.Slider())
@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider() private void updateSlider()
{ {
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject); bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle); headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; } private IPlacementHandler placementHandler { get; set; }
@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Move the control points from the insertion index onwards to make room for the insertion // Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint); controlPoints.Insert(insertionIndex, pathControlPoint);
HitObject.SnapTo(composer); HitObject.SnapTo(snapProvider);
return pathControlPoint; return pathControlPoint;
} }
@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
// Snap the slider to the current beat divisor before checking length validity. // Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(composer); HitObject.SnapTo(snapProvider);
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)

View File

@ -2,16 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
@ -20,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class DrawableOsuEditorRuleset : DrawableOsuRuleset public class DrawableOsuEditorRuleset : DrawableOsuRuleset
{ {
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
@ -37,80 +23,12 @@ namespace osu.Game.Rulesets.Osu.Edit
private class OsuEditorPlayfield : OsuPlayfield private class OsuEditorPlayfield : OsuPlayfield
{ {
private Bindable<bool> hitAnimations;
protected override GameplayCursorContainer CreateCursor() => null; protected override GameplayCursorContainer CreateCursor() => null;
public OsuEditorPlayfield() public OsuEditorPlayfield()
{ {
HitPolicy = new AnyOrderHitPolicy(); HitPolicy = new AnyOrderHitPolicy();
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations);
}
protected override void OnNewDrawableHitObject(DrawableHitObject d)
{
d.ApplyCustomUpdateState += updateState;
}
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle || hitAnimations.Value)
return;
if (hitObject is DrawableHitCircle circle)
{
using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime))
{
circle.ApproachCircle
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
.Expire();
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
}
}
if (hitObject is IHasMainCirclePiece mainPieceContainer)
{
// clear any explode animation logic.
// this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables.
ScheduleAfterChildren(() =>
{
if (hitObject.HitObject == null) return;
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true);
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true);
});
}
if (hitObject is DrawableSliderRepeat repeat)
{
repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true);
repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true);
}
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
switch (hitObject)
{
case DrawableSlider _:
case DrawableHitCircle _:
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire();
break;
}
}
} }
} }
} }

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{ {
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1)
{ {
Masking = true; Masking = true;
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -24,7 +25,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuHitObjectComposer : HitObjectComposer<OsuHitObject> public class OsuHitObjectComposer : DistancedHitObjectComposer<OsuHitObject>
{ {
public OsuHitObjectComposer(Ruleset ruleset) public OsuHitObjectComposer(Ruleset ruleset)
: base(ruleset) : base(ruleset)
@ -59,11 +60,6 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
LayerBelowRuleset.AddRange(new Drawable[] LayerBelowRuleset.AddRange(new Drawable[]
{ {
new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
},
distanceSnapGridContainer = new Container distanceSnapGridContainer = new Container
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
@ -128,20 +124,13 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{ {
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult; return snapResult;
return new SnapResult(screenSpacePosition, null); if (snapType.HasFlagFast(SnapType.Grids))
}
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{ {
var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition);
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap;
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
@ -153,8 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition)); Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition)); return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
} }
}
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuSelectionHandler : EditorSelectionHandler public class OsuSelectionHandler : EditorSelectionHandler
{ {
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPositionSnapProvider? positionSnapProvider { get; set; } private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary> /// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation. /// During a transform, the initial origin is stored so it can be used throughout the operation.
@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// Snap the slider's length to the current beat divisor // Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks. // to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(positionSnapProvider); slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling. //if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Edit
point.Position = oldControlPoints.Dequeue(); point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(positionSnapProvider); slider.SnapTo(snapProvider);
} }
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)

View File

@ -2,33 +2,43 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override string Name => @"Alternate"; public override string Name => @"Alternate";
public override string Acronym => @"AL"; public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!"; public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
private double firstObjectValidJudgementTime;
private IBindable<bool> isBreakTime;
private const double flash_duration = 1000; private const double flash_duration = 1000;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed; private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset; private DrawableRuleset<OsuHitObject> ruleset;
@ -39,29 +49,30 @@ namespace osu.Game.Rulesets.Osu.Mods
ruleset = drawableRuleset; ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var firstHitObject = ruleset.Objects.FirstOrDefault(); var periods = new List<Period>();
firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock; gameplayClock = drawableRuleset.FrameStableClock;
} }
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
isBreakTime.ValueChanged += e =>
{
if (e.NewValue)
lastActionPressed = null;
};
}
private bool checkCorrectAction(OsuAction action) private bool checkCorrectAction(OsuAction action)
{ {
if (isBreakTime.Value) if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
return true; {
lastActionPressed = null;
if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
return true; return true;
}
switch (action) switch (action)
{ {

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
private DrawableOsuBlinds blinds; private DrawableOsuBlinds blinds;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) }; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true); public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120; private const double default_follow_delay = 120;

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