1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:33:30 +08:00

Merge remote-tracking branch 'upstream/master' into fallback-to-skin-combo-colours

This commit is contained in:
Salman Ahmed 2019-12-12 12:37:51 +03:00
commit a9125f6544
442 changed files with 6579 additions and 2738 deletions

View File

@ -111,9 +111,15 @@ csharp_preserve_single_line_statements = true
#Roslyn language styles
#Style - this. qualification
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
#Style - type names
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
csharp_style_var_when_type_is_apparent = true:none
csharp_style_var_for_built_in_types = true:none
csharp_style_var_elsewhere = true:silent
@ -126,51 +132,57 @@ csharp_preferred_modifier_order = public,private,protected,internal,new,abstract
# Skipped because roslyn cannot separate +-*/ with << >>
#Style - expression bodies
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_accessors = true:warning
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_operators = true:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_operators = true:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_local_functions = true:silent
#Style - expression preferences
dotnet_style_object_initializer = true:warning
dotnet_style_collection_initializer = true:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_compound_assignment = true:silent
dotnet_style_prefer_compound_assignment = true:warning
#Style - null/type checks
dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning
csharp_style_pattern_matching_over_is_with_cast_check = true:silent
csharp_style_pattern_matching_over_as_with_null_check = true:silent
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
csharp_style_throw_expression = true:silent
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_conditional_delegate_call = true:warning
#Style - unused
dotnet_style_readonly_field = true:silent
dotnet_code_quality_unused_parameters = non_public:silent
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_style_unused_value_assignment_preference = discard_variable:silent
csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:silent
csharp_style_deconstructed_variable_declaration = true:silent
csharp_style_inlined_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
#Style - other C# 7.x features
csharp_style_expression_bodied_local_functions = true:silent
dotnet_style_prefer_inferred_tuple_names = true:warning
csharp_prefer_simple_default_expression = true:warning
csharp_style_pattern_local_over_anonymous_function = true:silent
csharp_style_pattern_local_over_anonymous_function = true:warning
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent
csharp_style_prefer_index_operator = false:none
csharp_style_prefer_range_operator = false:none
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers
# Suppress: EC112
#Field can be readonly
dotnet_diagnostic.IDE0044.severity = silent
#Private method is unused
dotnet_diagnostic.IDE0051.severity = silent
#Private member is unused

View File

@ -0,0 +1,6 @@
M:System.Object.Equals(System.Object,System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable<T> or EqualityComparer<T>.Default instead.
M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable<T> or EqualityComparer<T>.Default instead.
M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable<T> or EqualityComparer<T>.Default instead.
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.

View File

@ -1,7 +1,8 @@
<!-- Contains required properties for osu!framework projects. -->
<Project>
<PropertyGroup Label="C#">
<LangVersion>7.3</LangVersion>
<LangVersion>8.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
@ -14,12 +15,21 @@
<ItemGroup Label="Resources">
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="2.9.8" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
</ItemGroup>
<PropertyGroup Label="Documentation">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Project">
<!-- 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>NU1701</NoWarn>
<NoWarn>$(NoWarn);NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Nuget">
<IsPackable>false</IsPackable>
<Authors>ppy Pty Ltd</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/ppy/osu</PackageProjectUrl>

4
build.ps1 → InspectCode.ps1 Executable file → Normal file
View File

@ -22,6 +22,6 @@ if ($Experimental) { $cakeArguments += "-experimental" }
$cakeArguments += $ScriptArgs
dotnet tool restore
dotnet cake ./build/build.cake --bootstrap
dotnet cake ./build/build.cake $cakeArguments
dotnet cake ./build/InspectCode.cake --bootstrap
dotnet cake ./build/InspectCode.cake $cakeArguments
exit $LASTEXITCODE

View File

@ -4,13 +4,16 @@
# osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)]()
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename "osu!lazer". Pew pew.
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status
This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable osu! client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table.
This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table.
We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below.
@ -20,7 +23,8 @@ Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh
- A desktop platform with the [.NET Core SDK 3.0](https://www.microsoft.com/net/learn/get-started) or higher installed.
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore30&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
## Running osu!
@ -55,44 +59,50 @@ git pull
### Building
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided [below](#contributing).
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
- Visual Studio / Rider users should load the project via one of the platform-specific .slnf files, rather than the main .sln. This will allow access to template run configurations.
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
- Visual Studio Code users must run the `Restore` task before any build attempt.
You can also build and run osu! from the command-line with a single command:
You can also build and run *osu!* from the command-line with a single command:
```shell
dotnet run --project osu.Desktop
```
If you are not interested in debugging osu!, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document.
If you are not interested in debugging *osu!*, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document.
If the build fails, try to restore NuGet packages with `dotnet restore`.
_Due to a historical feature gap between .NET Core and Xamarin, running `dotnet` CLI from the root directory will not work for most commands. This can be resolved by specifying a target `.csproj` or the helper project at `build/Desktop.proj`. Configurations have been provided to work around this issue for all supported IDEs mentioned above._
### Testing with resource/framework modifications
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be achieved by running some commands as documented on the [osu-resources](https://github.com/ppy/osu-resources/wiki/Testing-local-resources-checkout-with-other-projects) and [osu-framework](https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects) wiki pages.
### Code analysis
Code analysis can be run with `powershell ./build.ps1` or `build.sh`. This is currently only supported under Windows due to [ReSharper CLI shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
Before committing your code, please run a code formatter. This can be achieved by running `dotnet format` in the command line, or using the `Format code` command in your IDE.
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`, which is [only supported on Windows](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted.
We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label).
Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as pain-free as possible.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, 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.
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
The osu! client code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
*osu!*'s code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
Please note that this *does not cover* the usage of the "osu!" or "ppy" branding in any software, resources, advertising or promotion, as this is protected by trademark law.

View File

@ -1,6 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2019
test: off
build_script:
- cmd: PowerShell -Version 2.0 .\build.ps1
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
before_build:
- ps: dotnet --info # Useful when version mismatch between CI and local
- ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
after_build:
- ps: dotnet tool restore
- ps: dotnet format --dry-run --check
- ps: .\InspectCode.ps1
test:
assemblies:
except:
- '**\*Android*'
- '**\*iOS*'
- 'build\**\*'

View File

@ -1,10 +1,21 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2019
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: $(APPVEYOR_REPO_TAG_NAME)
before_build:
- ps: dotnet --info # Useful when version mismatch between CI and local
- ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
test: off
skip_non_tags: true
build_script:
- cmd: PowerShell -Version 2.0 .\build.ps1
configuration: Release
build:
project: build\Desktop.proj # Skipping Xamarin Release that's slow and covered by fastlane
parallel: true
verbosity: minimal
publish_nuget: true
deploy:
- provider: Environment
name: nuget

View File

@ -1,17 +0,0 @@
echo "Installing Cake.Tool..."
dotnet tool restore
# Parse arguments.
CAKE_ARGUMENTS=()
for i in "$@"; do
case $1 in
-s|--script) SCRIPT="$2"; shift ;;
--) shift; CAKE_ARGUMENTS+=("$@"); break ;;
*) CAKE_ARGUMENTS+=("$1") ;;
esac
shift
done
echo "Running build script..."
dotnet cake ./build/build.cake --bootstrap
dotnet cake ./build/build.cake "${CAKE_ARGUMENTS[@]}"

View File

@ -7,45 +7,29 @@ var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
// ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
var target = Argument("target", "Build");
var target = Argument("target", "CodeAnalysis");
var configuration = Argument("configuration", "Release");
var rootDirectory = new DirectoryPath("..");
var sln = rootDirectory.CombineWithFilePath("osu.sln");
var desktopBuilds = rootDirectory.CombineWithFilePath("build/Desktop.proj");
var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Compile")
.Does(() => {
DotNetCoreBuild(desktopBuilds.FullPath, new DotNetCoreBuildSettings {
Configuration = configuration,
});
});
Task("Test")
.IsDependentOn("Compile")
.Does(() => {
var testAssemblies = GetFiles(rootDirectory + "/**/*.Tests/bin/**/*.Tests.dll");
DotNetCoreVSTest(testAssemblies, new DotNetCoreVSTestSettings {
Logger = AppVeyor.IsRunningOnAppVeyor ? "Appveyor" : $"trx",
Parallel = true,
ToolTimeout = TimeSpan.FromMinutes(10),
});
});
// windows only because both inspectcore and nvika depend on net45
// windows only because both inspectcode and nvika depend on net45
Task("InspectCode")
.WithCriteria(IsRunningOnWindows())
.IsDependentOn("Compile")
.Does(() => {
InspectCode(desktopSlnf, new InspectCodeSettings {
CachesHome = "inspectcode",
OutputFile = "inspectcodereport.xml",
ArgumentCustomization = arg => {
if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output
arg.Append("--verbosity:WARN");
return arg;
},
});
int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
@ -61,13 +45,8 @@ Task("CodeFileSanity")
});
});
Task("DotnetFormat")
.Does(() => DotNetCoreTool(sln.FullPath, "format", "--dry-run --check"));
Task("Build")
Task("CodeAnalysis")
.IsDependentOn("CodeFileSanity")
.IsDependentOn("DotnetFormat")
.IsDependentOn("InspectCode")
.IsDependentOn("Test");
.IsDependentOn("InspectCode");
RunTarget(target);

View File

@ -1,5 +1,5 @@
{
"msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.19"
"Microsoft.Build.Traversal": "2.0.24"
}
}

View File

@ -1,5 +1,6 @@
<Project>
<PropertyGroup>
<PropertyGroup>
<LangVersion>8.0</LangVersion>
<OutputPath>bin\$(Configuration)</OutputPath>
<WarningLevel>4</WarningLevel>
<SchemaVersion>2.0</SchemaVersion>
@ -10,7 +11,7 @@
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidApplication>True</AndroidApplication>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AndroidSupportedAbis>armeabi-v7a;x86;arm64-v8a</AndroidSupportedAbis>
@ -53,6 +54,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1112.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1212.0" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="sh.ppy.osulazer" android:installLocation="auto" android:versionName="0.1.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

View File

@ -112,14 +112,14 @@ namespace osu.Desktop
{
protected override string LocateBasePath()
{
bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
string stableInstallPath;
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(String.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
if (checkExists(stableInstallPath))
return stableInstallPath;

View File

@ -0,0 +1,11 @@
{
"profiles": {
"osu! Desktop": {
"commandName": "Project"
},
"osu! Tournament": {
"commandName": "Project",
"commandLineArgs": "--tournament"
}
}
}

20
osu.Desktop/app.manifest Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="1.0.0.0" name="osu!" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
<applicationRequestMinimum>
<defaultAssemblyRequest permissionSetReference="Custom" />
<PermissionSet class="System.Security.PermissionSet" version="1" Unrestricted="true" ID="Custom" SameSite="site" />
</applicationRequestMinimum>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</asmv1:assembly>

View File

@ -8,6 +8,7 @@
<Title>osu!lazer</Title>
<Product>osu!lazer</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.0.0</Version>
<FileVersion>0.0.0</FileVersion>
</PropertyGroup>
@ -23,11 +24,11 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.6.0" />
<PackageReference Include="System.IO.Packaging" Version="4.7.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<!-- using a different name because package name cannot contain 'catch' -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" />
</manifest>

View File

@ -11,12 +11,12 @@ using System;
using System.Collections.Generic;
using osu.Game.Skinning;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osuTK.Graphics;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Catch.Tests
{
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.Blue
},
new SpriteText
new OsuSpriteText
{
Text = "custom"
}

View File

@ -8,6 +8,7 @@ using System;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@ -22,48 +23,44 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
{
var curveData = obj as IHasCurve;
var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo;
var endTime = obj as IHasEndTime;
var legacyOffset = obj as IHasLegacyLastTickOffset;
if (curveData != null)
switch (obj)
{
yield return new JuiceStream
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0
};
}
else if (endTime != null)
{
yield return new BananaShower
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
};
}
else
{
yield return new Fruit
{
StartTime = obj.StartTime,
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
};
case IHasCurve curveData:
return new JuiceStream
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
}.Yield();
case IHasEndTime endTime:
return new BananaShower
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
}.Yield();
default:
return new Fruit
{
StartTime = obj.StartTime,
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
}.Yield();
}
}

View File

@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Mods;
@ -78,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
catchObject.XOffset = MathHelper.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
}
@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
else
{
currentObject.DistanceToHyperDash = distanceToHyper;
lastExcess = MathHelper.Clamp(distanceToHyper, 0, halfCatcherWidth);
lastExcess = Math.Clamp(distanceToHyper, 0, halfCatcherWidth);
}
lastDirection = thisDirection;

View File

@ -10,7 +10,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osuTK;
namespace osu.Game.Rulesets.Catch.Difficulty
{
@ -35,12 +34,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
var legacyScore = Score as LegacyScoreInfo;
fruitsHit = legacyScore?.Count300 ?? Score.Statistics[HitResult.Perfect];
ticksHit = legacyScore?.Count100 ?? 0;
tinyTicksHit = legacyScore?.Count50 ?? 0;
tinyTicksMissed = legacyScore?.CountKatu ?? 0;
fruitsHit = Score?.GetCount300() ?? Score.Statistics[HitResult.Perfect];
ticksHit = Score?.GetCount100() ?? 0;
tinyTicksHit = Score?.GetCount50() ?? 0;
tinyTicksMissed = Score?.GetCountKatu() ?? 0;
misses = Score.Statistics[HitResult.Miss];
// Don't count scores made with supposedly unranked mods
@ -48,55 +45,53 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return 0;
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0f * Math.Max(1.0f, Attributes.StarRating / 0.0049f) - 4.0f, 2.0f) / 100000.0f;
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits();
// Longer maps are worth more
float lengthBonus =
0.95f + 0.4f * Math.Min(1.0f, numTotalHits / 3000.0f) +
(numTotalHits > 3000 ? (float)Math.Log10(numTotalHits / 3000.0f) * 0.5f : 0.0f);
double lengthBonus =
0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) +
(numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0);
// Longer maps are worth more
value *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
value *= Math.Pow(0.97f, misses);
value *= Math.Pow(0.97, misses);
// Combo scaling
float beatmapMaxCombo = Attributes.MaxCombo;
if (beatmapMaxCombo > 0)
value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
if (Attributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
float approachRate = (float)Attributes.ApproachRate;
float approachRateFactor = 1.0f;
if (approachRate > 9.0f)
approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9
else if (approachRate < 8.0f)
approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8
double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 9.0)
approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9
else if (Attributes.ApproachRate < 8.0)
approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8
value *= approachRateFactor;
if (mods.Any(m => m is ModHidden))
// Hiddens gives nothing on max approach rate, and more the lower it is
value *= 1.05f + 0.075f * (10.0f - Math.Min(10.0f, approachRate)); // 7.5% for each AR below 10
value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
value *= 1.35f * lengthBonus;
value *= 1.35 * lengthBonus;
// Scale the aim value with accuracy _slightly_
value *= Math.Pow(accuracy(), 5.5f);
value *= Math.Pow(accuracy(), 5.5);
// Custom multipliers for NoFail. SpunOut is not applicable.
if (mods.Any(m => m is ModNoFail))
value *= 0.90f;
value *= 0.90;
return value;
}
private float accuracy() => totalHits() == 0 ? 0 : MathHelper.Clamp((float)totalSuccessfulHits() / totalHits(), 0f, 1f);
private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
private int totalComboHits() => misses + ticksHit + fruitsHit;

View File

@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osuTK;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
if (lastPlayerPosition == null)
lastPlayerPosition = catchCurrent.LastNormalizedPosition;
float playerPosition = MathHelper.Clamp(
float playerPosition = Math.Clamp(
lastPlayerPosition.Value,
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)

View File

@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils
{
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;
private uint _x, _y = y, _z = z, _w = w;
private const uint y_initial = 842502087;
private const uint z_initial = 3579807591;
private const uint w_initial = 273326509;
private uint x, y = y_initial, z = z_initial, w = w_initial;
public FastRandom(int seed)
{
_x = (uint)seed;
x = (uint)seed;
}
public FastRandom()
@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Catch.MathUtils
/// <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);
uint t = x ^ (x << 11);
x = y;
y = z;
z = w;
return w = w ^ (w >> 19) ^ t ^ (t >> 8);
}
/// <summary>

View File

@ -1,12 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModRelax : ModRelax
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>
{
public override string Description => @"Use the mouse to control the catcher.";
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
}
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
{
private readonly CatcherArea.Catcher catcher;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public MouseInputHelper(CatchPlayfield playfield)
{
catcher = playfield.CatcherArea.MovableCatcher;
RelativeSizeAxes = Axes.Both;
}
//disable keyboard controls
public bool OnPressed(CatchAction action) => true;
public bool OnReleased(CatchAction action) => true;
protected override bool OnMouseMove(MouseMoveEvent e)
{
catcher.UpdatePosition(e.MousePosition.X / DrawSize.X);
return base.OnMouseMove(e);
}
}
}
}

View File

@ -4,8 +4,8 @@
using System;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
protected override void UpdateStateTransforms(ArmedState state)
{
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
var endTime = HitObject.GetEndTime();
using (BeginAbsoluteSequence(endTime, true))
{

View File

@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
const float small_pulp = large_pulp_3 / 2;
Vector2 positionAt(float angle, float distance) => new Vector2(
distance * (float)Math.Sin(angle * Math.PI / 180),
distance * (float)Math.Cos(angle * Math.PI / 180));
static Vector2 positionAt(float angle, float distance) => new Vector2(
distance * MathF.Sin(angle * MathF.PI / 180),
distance * MathF.Cos(angle * MathF.PI / 180));
switch (representation)
{
@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
base.Update();
border.Alpha = (float)MathHelper.Clamp((HitObject.StartTime - Time.Current) / 500, 0, 1);
border.Alpha = (float)Math.Clamp((HitObject.StartTime - Time.Current) / 500, 0, 1);
}
private Color4 colourForRepresentation(FruitVisualRepresentation representation)

View File

@ -116,12 +116,22 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration => EndTime - StartTime;
private SliderPath path;
private readonly SliderPath path = new SliderPath();
public SliderPath Path
{
get => path;
set => path = value;
set
{
path.ControlPoints.Clear();
path.ExpectedDistance.Value = null;
if (value != null)
{
path.ControlPoints.AddRange(value.ControlPoints);
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
}
public double Distance => Path.Distance;

View File

@ -2,23 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor<CatchHitObject>
public class CatchScoreProcessor : ScoreProcessor
{
public CatchScoreProcessor(DrawableRuleset<CatchHitObject> drawableRuleset)
: base(drawableRuleset)
public CatchScoreProcessor(IBeatmap beatmap)
: base(beatmap)
{
}
private float hpDrainRate;
protected override void ApplyBeatmap(Beatmap<CatchHitObject> beatmap)
protected override void ApplyBeatmap(IBeatmap beatmap)
{
base.ApplyBeatmap(beatmap);

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.UI
internal readonly CatcherArea CatcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || CatcherArea.ReceivePositionalInputAt(screenSpacePos);
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
{
Container explodingFruitContainer;

View File

@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Catch.UI
var additive = createCatcherSprite();
additive.Anchor = Anchor;
additive.OriginPosition = additive.OriginPosition + new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly.
additive.OriginPosition += new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly.
additive.Position = Position;
additive.Scale = Scale;
additive.Colour = HyperDashing ? Color4.Red : Color4.White;
@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.Y -= RNG.NextSingle() * diff;
}
fruit.X = MathHelper.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2);
fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2);
caughtFruit.Add(fruit);
}
@ -377,8 +377,7 @@ namespace osu.Game.Rulesets.Catch.UI
double dashModifier = Dashing ? 1 : 0.5;
double speed = BASE_SPEED * dashModifier * hyperDashModifier;
Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y);
X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1);
UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@ -452,6 +451,17 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.LifetimeStart = Time.Current;
fruit.Expire();
}
public void UpdatePosition(float position)
{
position = Math.Clamp(position, 0, 1);
if (position == X)
return;
Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
X = position;
}
}
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle")
InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.UI
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
}
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this);
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(Beatmap);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>catch the fruit. to the beat.</Description>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" />
</manifest>

View File

@ -9,7 +9,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests
yield return new ConvertValue
{
StartTime = hitObject.StartTime,
EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime,
EndTime = hitObject.GetEndTime(),
Column = ((ManiaHitObject)hitObject).Column
};
}

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
if (TargetColumns >= 10)
{
TargetColumns = TargetColumns / 2;
TargetColumns /= 2;
Dual = true;
}
}
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.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);
return base.ConvertBeatmap(original);
@ -156,37 +156,44 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
{
var endTimeData = original as IHasEndTime;
var distanceData = original as IHasDistance;
var positionData = original as IHasPosition;
Patterns.PatternGenerator conversion = null;
if (distanceData != null)
switch (original)
{
var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
conversion = generator;
for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration)
case IHasDistance _:
{
recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time);
var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
conversion = generator;
var positionData = original as IHasPosition;
for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration)
{
recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time);
}
break;
}
}
else if (endTimeData != null)
{
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
}
else if (positionData != null)
{
computeDensity(original.StartTime);
case IHasEndTime endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
break;
}
recordNote(original.StartTime, positionData.Position);
case IHasPosition positionData:
{
computeDensity(original.StartTime);
conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
recordNote(original.StartTime, positionData.Position);
break;
}
}
if (conversion == null)
@ -219,14 +226,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private Pattern generate()
{
var endTimeData = HitObject as IHasEndTime;
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
if (endTimeData != null)
if (HitObject is IHasEndTime endTimeData)
{
pattern.Add(new HoldNote
{
@ -237,7 +243,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
});
}
else if (positionData != null)
else if (HitObject is IHasXPosition)
{
pattern.Add(new Note
{
@ -257,9 +263,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// <returns></returns>
private IList<HitSampleInfo> sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;
if (curveData == null)
if (!(HitObject is IHasCurve curveData))
return HitObject.Samples;
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();

View File

@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects)
{
if (!Precision.AlmostEquals(EndTime, (obj as IHasEndTime)?.EndTime ?? obj.StartTime))
if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
intermediatePattern.Add(obj);
else
endTimePattern.Add(obj);
@ -364,7 +364,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
}
bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
@ -474,9 +474,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns></returns>
private IList<HitSampleInfo> sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;
if (curveData == null)
if (!(HitObject is IHasCurve curveData))
return HitObject.Samples;
double segmentTime = (EndTime - HitObject.StartTime) / spanCount;

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -88,15 +89,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public override IEnumerable<Pattern> Generate()
{
yield return generate();
}
private Pattern generate()
{
var pattern = new Pattern();
try
Pattern generateCore()
{
var pattern = new Pattern();
if (TotalColumns == 1)
{
addToPattern(pattern, 0);
@ -168,54 +164,56 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
if (convertType.HasFlag(PatternType.KeepSingle))
return pattern = generateRandomNotes(1);
return generateRandomNotes(1);
if (convertType.HasFlag(PatternType.Mirror))
{
if (ConversionDifficulty > 6.5)
return pattern = generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
if (ConversionDifficulty > 4)
return pattern = generateRandomPatternWithMirrored(0.12, 0.17, 0);
return generateRandomPatternWithMirrored(0.12, 0.17, 0);
return pattern = generateRandomPatternWithMirrored(0.12, 0, 0);
return generateRandomPatternWithMirrored(0.12, 0, 0);
}
if (ConversionDifficulty > 6.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
return pattern = generateRandomPattern(0.78, 0.42, 0, 0);
return generateRandomPattern(0.78, 0.42, 0, 0);
return pattern = generateRandomPattern(1, 0.62, 0, 0);
return generateRandomPattern(1, 0.62, 0, 0);
}
if (ConversionDifficulty > 4)
{
if (convertType.HasFlag(PatternType.LowProbability))
return pattern = generateRandomPattern(0.35, 0.08, 0, 0);
return generateRandomPattern(0.35, 0.08, 0, 0);
return pattern = generateRandomPattern(0.52, 0.15, 0, 0);
return generateRandomPattern(0.52, 0.15, 0, 0);
}
if (ConversionDifficulty > 2)
{
if (convertType.HasFlag(PatternType.LowProbability))
return pattern = generateRandomPattern(0.18, 0, 0, 0);
return generateRandomPattern(0.18, 0, 0, 0);
return pattern = generateRandomPattern(0.45, 0, 0, 0);
return generateRandomPattern(0.45, 0, 0, 0);
}
return pattern = generateRandomPattern(0, 0, 0, 0);
return generateRandomPattern(0, 0, 0, 0);
}
finally
var p = generateCore();
foreach (var obj in p.HitObjects)
{
foreach (var obj in pattern.HitObjects)
{
if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair;
if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair;
}
if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair;
if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair;
}
return p.Yield();
}
/// <summary>
@ -303,8 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
bool addToCentre;
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out var addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
int nextColumn = GetRandomColumn(upperBound: columnLimit);
@ -384,8 +381,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns>The amount of notes to be generated. The note to be added to the centre column will NOT be part of this count.</returns>
private int getRandomNoteCountMirrored(double centreProbability, double p2, double p3, out bool addToCentre)
{
addToCentre = false;
switch (TotalColumns)
{
case 2:

View File

@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -54,11 +53,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (allowSpecial && TotalColumns == 8)
{
const float local_x_divisor = 512f / 7;
return MathHelper.Clamp((int)Math.Floor(position / local_x_divisor), 0, 6) + 1;
return Math.Clamp((int)MathF.Floor(position / local_x_divisor), 0, 6) + 1;
}
float localXDivisor = 512f / TotalColumns;
return MathHelper.Clamp((int)Math.Floor(position / localXDivisor), 0, TotalColumns - 1);
return Math.Clamp((int)MathF.Floor(position / localXDivisor), 0, TotalColumns - 1);
}
/// <summary>
@ -113,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;
@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param>
/// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no
@ -148,9 +147,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null,
params Pattern[] patterns)
{
lowerBound = lowerBound ?? RandomStart;
upperBound = upperBound ?? TotalColumns;
nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound));
lowerBound ??= RandomStart;
upperBound ??= TotalColumns;
nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound));
// Check for the initial column
if (isValid(initialColumn))
@ -184,7 +183,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns"/> is used.</param>
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
/// <summary>

View File

@ -37,12 +37,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]);
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
countPerfect = Score.Statistics[HitResult.Perfect];
countGreat = Score.Statistics[HitResult.Great];
countGood = Score.Statistics[HitResult.Good];
countOk = Score.Statistics[HitResult.Ok];
countMeh = Score.Statistics[HitResult.Meh];
countMiss = Score.Statistics[HitResult.Miss];
if (mods.Any(m => !m.Ranked))
return 0;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Timing;
@ -9,7 +10,6 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit
// Flip the vertical coordinate space when scrolling downwards
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
targetPosition = targetPosition - referenceParent.DrawHeight;
targetPosition -= referenceParent.DrawHeight;
float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column;
}
columnDelta = MathHelper.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
obj.Column += columnDelta;

View File

@ -6,7 +6,6 @@ using System.Linq;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays
@ -84,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.Replays
var currentObject = Beatmap.HitObjects[i];
var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
double endTime = (currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime;
double endTime = currentObject.GetEndTime();
bool canDelayKeyUp = nextObjectInColumn == null ||
nextObjectInColumn.StartTime > endTime + RELEASE_DELAY;

View File

@ -3,13 +3,11 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor<ManiaHitObject>
internal class ManiaScoreProcessor : ScoreProcessor
{
/// <summary>
/// The hit HP multiplier at OD = 0.
@ -51,12 +49,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
/// </summary>
private double hpMultiplier = 1;
public ManiaScoreProcessor(DrawableRuleset<ManiaHitObject> drawableRuleset)
: base(drawableRuleset)
public ManiaScoreProcessor(IBeatmap beatmap)
: base(beatmap)
{
}
protected override void ApplyBeatmap(Beatmap<ManiaHitObject> beatmap)
protected override void ApplyBeatmap(IBeatmap beatmap)
{
base.ApplyBeatmap(beatmap);
@ -65,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max);
}
protected override void SimulateAutoplay(Beatmap<ManiaHitObject> beatmap)
protected override void SimulateAutoplay(IBeatmap beatmap)
{
while (true)
{

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(Beatmap);
public override int Variant => (int)(Beatmap.Stages.Count == 1 ? PlayfieldType.Single : PlayfieldType.Dual) + Beatmap.TotalColumns;

View File

@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.UI
foreach (var stage in stages)
{
sum = sum + stage.Columns.Count;
sum += stage.Columns.Count;
if (sum > column)
return stage;
}

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>smash the keys. to the beat.</Description>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" />
</manifest>

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
@ -41,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.Tests
break;
}
ConvertValue createConvertValue(OsuHitObject obj) => new ConvertValue
static ConvertValue createConvertValue(OsuHitObject obj) => new ConvertValue
{
StartTime = obj.StartTime,
EndTime = (obj as IHasEndTime)?.EndTime ?? obj.StartTime,
EndTime = obj.GetEndTime(),
X = obj.StackedPosition.X,
Y = obj.StackedPosition.Y
};

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.931145117263422, "diffcalc-test")]
[TestCase(6.9311451172608853d, "diffcalc-test")]
[TestCase(1.0736587013228804d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
positionOffset = positionOffset ?? Vector2.Zero;
positionOffset ??= Vector2.Zero;
var circle = new HitCircle
{

View File

@ -16,9 +16,11 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
@ -75,14 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) => new CustomSkinWorkingBeatmap(beatmap, Clock, audio, testBeatmapSkin);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin);
public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
{
private readonly ISkinSource skin;
public CustomSkinWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin)
: base(beatmap, frameBasedClock, audio)
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin)
: base(beatmap, storyboard, frameBasedClock, audio)
{
this.skin = skin;
}
@ -124,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
if (!enabled) return null;
return new SpriteText
return new OsuSpriteText
{
Text = identifier,
Font = OsuFont.Default.With(size: 30),

View File

@ -148,9 +148,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType<RepeatPoint>().All(assertSamples));
AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0);
bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
bool assertSamples(HitObject hitObject)
static bool assertSamples(HitObject hitObject)
{
return hitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)
&& hitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE);
@ -183,8 +183,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType<RepeatPoint>().All(assertSamples));
AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0);
bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick";
static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
@ -379,8 +380,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
var osuObject = judgedObject as DrawableOsuHitObject;
if (osuObject == null)
if (!(judgedObject is DrawableOsuHitObject osuObject))
return;
OsuSpriteText text;

View File

@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index];
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}

View File

@ -15,6 +15,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Storyboards;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
@ -28,9 +29,9 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool Autoplay => true;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap)
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}

View File

@ -9,6 +9,7 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Types;
using System;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@ -23,52 +24,48 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
{
var curveData = original as IHasCurve;
var endTimeData = original as IHasEndTime;
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;
var legacyOffset = original as IHasLegacyLastTickOffset;
if (curveData != null)
switch (original)
{
yield return new Slider
{
StartTime = original.StartTime,
Samples = original.Samples,
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
};
}
else if (endTimeData != null)
{
yield return new Spinner
{
StartTime = original.StartTime,
Samples = original.Samples,
EndTime = endTimeData.EndTime,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
};
}
else
{
yield return new HitCircle
{
StartTime = original.StartTime,
Samples = original.Samples,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
};
case IHasCurve curveData:
return new Slider
{
StartTime = original.StartTime,
Samples = original.Samples,
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
}.Yield();
case IHasEndTime endTimeData:
return new Spinner
{
StartTime = original.StartTime,
Samples = original.Samples,
EndTime = endTimeData.EndTime,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
}.Yield();
default:
return new HitCircle
{
StartTime = original.StartTime,
Samples = original.Samples,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
}.Yield();
}
}

View File

@ -4,7 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (objectN is Spinner)
continue;
double endTime = (stackBaseObject as IHasEndTime)?.EndTime ?? stackBaseObject.StartTime;
double endTime = stackBaseObject.GetEndTime();
double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency;
if (objectN.StartTime - endTime > stackThreshold)
@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
OsuHitObject objectN = beatmap.HitObjects[n];
if (objectN is Spinner) continue;
double endTime = (objectN as IHasEndTime)?.EndTime ?? objectN.StartTime;
double endTime = objectN.GetEndTime();
if (objectI.StartTime - endTime > stackThreshold)
//We are no longer within stacking range of the previous object.
@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
continue;
double startTime = (currHitObject as IHasEndTime)?.EndTime ?? currHitObject.StartTime;
double startTime = currHitObject.GetEndTime();
int sliderStack = 0;
for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
@ -217,14 +217,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
{
currHitObject.StackHeight++;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[j].StartTime;
startTime = beatmap.HitObjects[j].GetEndTime();
}
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
{
//Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[j].StartTime;
startTime = beatmap.HitObjects[j].GetEndTime();
}
}
}

View File

@ -45,32 +45,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
mods = Score.Mods;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
countGreat = Score.Statistics[HitResult.Great];
countGood = Score.Statistics[HitResult.Good];
countMeh = Score.Statistics[HitResult.Meh];
countMiss = Score.Statistics[HitResult.Miss];
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is OsuModNoFail))
multiplier *= 0.90f;
multiplier *= 0.90;
if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 0.95f;
multiplier *= 0.95;
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1f) +
Math.Pow(speedValue, 1.1f) +
Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f
Math.Pow(aimValue, 1.1) +
Math.Pow(speedValue, 1.1) +
Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryRatings != null)
@ -93,82 +93,82 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0f * Math.Max(1.0f, rawAim / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more
double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
aimValue *= Math.Pow(0.97f, countMiss);
aimValue *= Math.Pow(0.97, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0f;
double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33f)
approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f);
else if (Attributes.ApproachRate < 8.0f)
if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0)
{
approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate);
approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
}
aimValue *= approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0f + 0.04f * (12.0f - Attributes.ApproachRate);
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
if (mods.Any(h => h is OsuModFlashlight))
{
// Apply object-based bonus for flashlight.
aimValue *= 1.0f + 0.35f * Math.Min(1.0f, totalHits / 200.0f) +
aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200
? 0.3f * Math.Min(1.0f, (totalHits - 200) / 300.0f) +
(totalHits > 500 ? (totalHits - 500) / 1200.0f : 0.0f)
: 0.0f);
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
: 0.0);
}
// Scale the aim value with accuracy _slightly_
aimValue *= 0.5f + accuracy / 2.0f;
aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that
aimValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue;
}
private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.SpeedStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more
speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
speedValue *= 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
speedValue *= Math.Pow(0.97f, countMiss);
speedValue *= Math.Pow(0.97, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0f;
if (Attributes.ApproachRate > 10.33f)
approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f);
double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
speedValue *= approachRateFactor;
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0f + 0.04f * (12.0f - Attributes.ApproachRate);
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
// Scale the speed value with accuracy _slightly_
speedValue *= 0.02f + accuracy;
speedValue *= 0.02 + accuracy;
// It is important to also consider accuracy difficulty when doing that
speedValue *= 0.96f + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
return speedValue;
}
@ -190,15 +190,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Pow(1.52163f, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08f;
accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02f;
accuracyValue *= 1.02;
return accuracyValue;
}

View File

@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (progress % 2 >= 1)
progress = 1 - progress % 1;
else
progress = progress % 1;
progress %= 1;
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;

View File

@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
CornerRadius = Size.X / 2;
CornerExponent = 2;
InternalChild = new RingPiece();
}

View File

@ -11,19 +11,21 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointPiece : BlueprintPiece<Slider>
{
public Action<int> RequestSelection;
public Action<Vector2[]> ControlPointsChanged;
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public readonly BindableBool IsSelected = new BindableBool();
public readonly int Index;
public readonly PathControlPoint ControlPoint;
private readonly Slider slider;
private readonly Path path;
@ -36,10 +38,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved]
private OsuColour colours { get; set; }
public PathControlPointPiece(Slider slider, int index)
private IBindable<Vector2> sliderPosition;
private IBindable<int> pathVersion;
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
{
this.slider = slider;
Index = index;
ControlPoint = controlPoint;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@ -85,24 +91,100 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
};
}
protected override void Update()
protected override void LoadComplete()
{
base.Update();
base.LoadComplete();
Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateDisplay());
pathVersion = slider.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updateDisplay());
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
updateDisplay();
}
private void updateDisplay()
{
updateMarkerDisplay();
updateConnectingPath();
}
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnHover(HoverEvent e)
{
updateMarkerDisplay();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateMarkerDisplay();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection == null)
return false;
switch (e.Button)
{
case MouseButton.Left:
RequestSelection.Invoke(this, e);
return true;
case MouseButton.Right:
if (!IsSelected.Value)
RequestSelection.Invoke(this, e);
return false; // Allow context menu to show
}
return false;
}
protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left;
protected override bool OnDrag(DragEvent e)
{
if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
(Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
Vector2 movementDelta = snappedPosition - slider.Position;
slider.Position += movementDelta;
slider.StartTime = snappedTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position.Value -= movementDelta;
}
else
ControlPoint.Position.Value += e.Delta;
return true;
}
protected override bool OnDragEnd(DragEndEvent e) => true;
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
Position = slider.StackedPosition + ControlPoint.Position.Value;
markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
if (IsHovered || IsSelected.Value)
colour = Color4.White;
marker.Colour = colour;
@ -115,72 +197,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
path.ClearVertices();
if (Index != slider.Path.ControlPoints.Length - 1)
int index = slider.Path.ControlPoints.IndexOf(ControlPoint);
if (index == -1)
return;
if (++index != slider.Path.ControlPoints.Count)
{
path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection != null)
{
RequestSelection.Invoke(Index);
return true;
}
return false;
}
protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) => true;
protected override bool OnDrag(DragEvent e)
{
var newControlPoints = slider.Path.ControlPoints.ToArray();
if (Index == 0)
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
(Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
Vector2 movementDelta = snappedPosition - slider.Position;
slider.Position += movementDelta;
slider.StartTime = snappedTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < newControlPoints.Length; i++)
newControlPoints[i] -= movementDelta;
}
else
newControlPoints[Index] += e.Delta;
if (isSegmentSeparatorWithNext)
newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious)
newControlPoints[Index - 1] = newControlPoints[Index];
ControlPointsChanged?.Invoke(newControlPoints);
return true;
}
protected override bool OnDragEnd(DragEndEvent e) => true;
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
}
}

View File

@ -3,30 +3,37 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{
public Action<Vector2[]> ControlPointsChanged;
internal readonly Container<PathControlPointPiece> Pieces;
private readonly Slider slider;
private readonly bool allowSelection;
private InputManager inputManager;
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
private IBindableList<PathControlPoint> controlPoints;
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
@ -43,45 +50,41 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
base.LoadComplete();
inputManager = GetContainingInputManager();
controlPoints = slider.Path.ControlPoints.GetBoundCopy();
controlPoints.ItemsAdded += addControlPoints;
controlPoints.ItemsRemoved += removeControlPoints;
addControlPoints(controlPoints);
}
protected override void Update()
private void addControlPoints(IEnumerable<PathControlPoint> controlPoints)
{
base.Update();
while (slider.Path.ControlPoints.Length > Pieces.Count)
foreach (var point in controlPoints)
{
var piece = new PathControlPointPiece(slider, Pieces.Count)
{
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
};
var piece = new PathControlPointPiece(slider, point);
if (allowSelection)
piece.RequestSelection = selectPiece;
Pieces.Add(piece);
}
}
while (slider.Path.ControlPoints.Length < Pieces.Count)
Pieces.Remove(Pieces[Pieces.Count - 1]);
private void removeControlPoints(IEnumerable<PathControlPoint> controlPoints)
{
foreach (var point in controlPoints)
Pieces.RemoveAll(p => p.ControlPoint == point);
}
protected override bool OnClick(ClickEvent e)
{
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return false;
}
private void selectPiece(int index)
{
if (inputManager.CurrentState.Keyboard.ControlPressed)
Pieces[index].IsSelected.Toggle();
else
{
foreach (var piece in Pieces)
piece.IsSelected.Value = piece.Index == index;
piece.IsSelected.Value = false;
}
return false;
}
public bool OnPressed(PlatformAction action)
@ -89,45 +92,106 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (action.ActionMethod)
{
case PlatformActionMethod.Delete:
var newControlPoints = new List<Vector2>();
foreach (var piece in Pieces)
{
if (!piece.IsSelected.Value)
newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
}
// Ensure that there are any points to be deleted
if (newControlPoints.Count == slider.Path.ControlPoints.Length)
return false;
// If there are 0 remaining control points, treat the slider as being deleted
if (newControlPoints.Count == 0)
{
placementHandler?.Delete(slider);
return true;
}
// Make control points relative
Vector2 first = newControlPoints[0];
for (int i = 0; i < newControlPoints.Count; i++)
newControlPoints[i] = newControlPoints[i] - first;
// The slider's position defines the position of the first control point, and all further control points are relative to that point
slider.Position = slider.Position + first;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
ControlPointsChanged?.Invoke(newControlPoints.ToArray());
return true;
return deleteSelected();
}
return false;
}
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e)
{
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
piece.IsSelected.Toggle();
else
{
foreach (var p in Pieces)
p.IsSelected.Value = p == piece;
}
}
private bool deleteSelected()
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return false;
RemoveControlPointsRequested?.Invoke(toRemove);
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
public MenuItem[] ContextMenuItems
{
get
{
if (!Pieces.Any(p => p.IsHovered))
return null;
var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToList();
int count = selectedPieces.Count;
if (count == 0)
return null;
List<MenuItem> items = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0]))
items.Add(createMenuItemForPathType(null));
// todo: hide/disable items which aren't valid for selected points
items.Add(createMenuItemForPathType(PathType.Linear));
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
items.Add(createMenuItemForPathType(PathType.Bezier));
items.Add(createMenuItemForPathType(PathType.Catmull));
return new MenuItem[]
{
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected()),
new OsuMenuItem("Curve type")
{
Items = items
}
};
}
}
private MenuItem createMenuItemForPathType(PathType? type)
{
int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
var item = new PathTypeMenuItem(type, () =>
{
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
p.ControlPoint.Type.Value = type;
});
if (countOfState == totalCount)
item.State.Value = TernaryState.True;
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
return item;
}
private class PathTypeMenuItem : TernaryStateMenuItem
{
public PathTypeMenuItem(PathType? type, Action action)
: base(type == null ? "Inherit" : type.ToString().Humanize(), changeState, MenuItemType.Standard, _ => action?.Invoke())
{
}
private static TernaryState changeState(TernaryState state) => TernaryState.True;
}
}
}

View File

@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
@ -27,11 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece;
private readonly List<Segment> segments = new List<Segment>();
private Vector2 cursor;
private InputManager inputManager;
private PlacementState state;
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@ -40,7 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
: base(new Objects.Slider())
{
RelativeSizeAxes = Axes.Both;
segments.Add(new Segment(Vector2.Zero));
HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear));
currentSegmentLength = 1;
}
[BackgroundDependencyLoader]
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(),
new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
new PathControlPointVisualiser(HitObject, false)
};
setState(PlacementState.Initial);
@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break;
case PlacementState.Body:
ensureCursor();
// The given screen-space position may have been externally snapped, but the unsnapped position from the input manager
// is used instead since snapping control points doesn't make much sense
cursor = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
break;
}
}
@ -91,7 +93,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (e.Button)
{
case MouseButton.Left:
segments.Last().ControlPoints.Add(cursor);
ensureCursor();
// Detatch the cursor
cursor = null;
break;
}
@ -110,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override bool OnDoubleClick(DoubleClickEvent e)
{
segments.Add(new Segment(segments[segments.Count - 1].ControlPoints.Last()));
// Todo: This should all not occur on double click, but rather if the previous control point is hovered.
segmentStart = HitObject.Path.ControlPoints[HitObject.Path.ControlPoints.Count - 1];
segmentStart.Type.Value = PathType.Linear;
currentSegmentLength = 1;
return true;
}
@ -132,14 +141,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updateSlider();
}
private void updatePathType()
{
switch (currentSegmentLength)
{
case 1:
case 2:
segmentStart.Type.Value = PathType.Linear;
break;
case 3:
segmentStart.Type.Value = PathType.PerfectCurve;
break;
default:
segmentStart.Type.Value = PathType.Bezier;
break;
}
}
private void ensureCursor()
{
if (cursor == null)
{
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
currentSegmentLength++;
updatePathType();
}
}
private void updateSlider()
{
Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
var unsnappedPath = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
HitObject.Path = new SliderPath(unsnappedPath.Type, newControlPoints, snappedDistance);
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@ -156,15 +190,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Initial,
Body,
}
private class Segment
{
public readonly List<Vector2> ControlPoints = new List<Vector2>();
public Segment(Vector2 offset)
{
ControlPoints.Add(offset);
}
}
}
}

View File

@ -1,9 +1,10 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
@ -11,10 +12,10 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit.Compose;
using osuTK;
using osuTK.Input;
@ -30,6 +31,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@ -40,10 +44,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
{
RemoveControlPointsRequested = removeControlPoints
}
};
}
private IBindable<int> pathVersion;
protected override void LoadComplete()
{
base.LoadComplete();
pathVersion = HitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updatePath());
}
protected override void Update()
{
base.Update();
@ -77,12 +94,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
Debug.Assert(placementControlPointIndex != null);
Vector2 position = e.MousePosition - HitObject.Position;
var controlPoints = HitObject.Path.ControlPoints.ToArray();
controlPoints[placementControlPointIndex.Value] = position;
onNewControlPoints(controlPoints);
HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position.Value = e.MousePosition - HitObject.Position;
return true;
}
@ -93,19 +105,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
private BindableList<PathControlPoint> controlPoints => HitObject.Path.ControlPoints;
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;
var controlPoints = new Vector2[HitObject.Path.ControlPoints.Length + 1];
HitObject.Path.ControlPoints.CopyTo(controlPoints);
int insertionIndex = 0;
float minDistance = float.MaxValue;
for (int i = 0; i < controlPoints.Length - 2; i++)
for (int i = 0; i < controlPoints.Count - 1; i++)
{
float dist = new Line(controlPoints[i], controlPoints[i + 1]).DistanceToPoint(position);
float dist = new Line(controlPoints[i].Position.Value, controlPoints[i + 1].Position.Value).DistanceToPoint(position);
if (dist < minDistance)
{
@ -115,21 +126,45 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Move the control points from the insertion index onwards to make room for the insertion
Array.Copy(controlPoints, insertionIndex, controlPoints, insertionIndex + 1, controlPoints.Length - insertionIndex - 1);
controlPoints[insertionIndex] = position;
onNewControlPoints(controlPoints);
controlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } });
return insertionIndex;
}
private void onNewControlPoints(Vector2[] controlPoints)
private void removeControlPoints(List<PathControlPoint> toRemove)
{
var unsnappedPath = new SliderPath(controlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, controlPoints);
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return;
HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
foreach (var c in toRemove)
{
// The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type.Value == null)
controlPoints[1].Type.Value = controlPoints[0].Type.Value;
controlPoints.Remove(c);
}
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
if (controlPoints.Count <= 1)
{
placementHandler?.Delete(HitObject);
return;
}
// The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
Vector2 first = controlPoints[0].Position.Value;
foreach (var c in controlPoints)
c.Position.Value -= first;
HitObject.Position += first;
}
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
UpdateHitObject();
}

View File

@ -1,6 +1,7 @@
// 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 JetBrains.Annotations;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@ -8,8 +9,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
: base(hitObject, nextHitObject, hitObject.StackedEndPosition)
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
: base(hitObject.StackedPosition, hitObject.StartTime, nextHitObject?.StartTime)
{
Masking = true;
}

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -13,7 +14,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
}
private float calculateGap(float value) => MathHelper.Clamp(value, 0, target_clamp) * targetBreakMultiplier;
private float calculateGap(float value) => Math.Clamp(value, 0, target_clamp) * targetBreakMultiplier;
// lagrange polinominal for (0,0) (0.6,0.4) (1,1) should make a good curve
private static float applyAdjustmentCurve(float value) => 0.6f * value * value + 0.4f * value;

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Bindables;
@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods
var destination = e.MousePosition;
FlashlightPosition = Interpolation.ValueAt(
MathHelper.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out);
Math.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out);
return base.OnMouseMove(e);
}

View File

@ -22,18 +22,14 @@ namespace osu.Game.Rulesets.Osu.Mods
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
var slider = hitObject as Slider;
if (slider == null)
if (!(hitObject is Slider slider))
return;
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var newControlPoints = new Vector2[slider.Path.ControlPoints.Length];
for (int i = 0; i < slider.Path.ControlPoints.Length; i++)
newControlPoints[i] = new Vector2(slider.Path.ControlPoints[i].X, -slider.Path.ControlPoints[i].Y);
slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance);
foreach (var point in slider.Path.ControlPoints)
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
}
}
}

View File

@ -6,9 +6,9 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;
foreach (var d in drawables.OfType<DrawableOsuHitObject>())
{
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Mods
var fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier;
// new duration from completed fade in to end (before fading out)
var longFadeDuration = ((h as IHasEndTime)?.EndTime ?? h.StartTime) - fadeOutStartTime;
var longFadeDuration = h.GetEndTime() - fadeOutStartTime;
switch (drawable)
{

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
float appearDistance = (float)(hitObject.TimePreempt - hitObject.TimeFadeIn) / 2;
Vector2 originalPosition = drawable.Position;
Vector2 appearOffset = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * appearDistance;
Vector2 appearOffset = new Vector2(MathF.Cos(theta), MathF.Sin(theta)) * appearDistance;
//the - 1 and + 1 prevents the hit objects to appear in the wrong position.
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;

View File

@ -25,11 +25,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
Origin = Anchor.Centre;
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new Container
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
CornerRadius = width / 2,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,

View File

@ -6,7 +6,7 @@ using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Vector2 startPosition = osuStart.EndPosition;
Vector2 endPosition = osuEnd.Position;
double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime;
double startTime = osuStart.GetEndTime();
double endTime = osuEnd.StartTime;
Vector2 distanceVector = endPosition - startPosition;

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager);
internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager;
protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);

View File

@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
float aimRotation = MathHelper.RadiansToDegrees((float)Math.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
float aimRotation = MathHelper.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
while (Math.Abs(aimRotation - Rotation) > 180)
aimRotation += aimRotation < Rotation ? 360 : -360;
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
Rotation = Interpolation.ValueAt(MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}

View File

@ -1,6 +1,7 @@
// 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 osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> stackHeightBindable = new Bindable<int>();
private readonly IBindable<float> scaleBindable = new Bindable<float>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
@ -83,9 +84,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
stackHeightBindable.BindTo(HitObject.StackHeightBindable);
scaleBindable.BindTo(HitObject.ScaleBindable);
pathBindable.BindTo(slider.PathBindable);
pathVersion.BindTo(slider.Path.Version);
pathBindable.BindValueChanged(_ => Body.Refresh());
pathVersion.BindValueChanged(_ => Body.Refresh());
AccentColour.BindValueChanged(colour =>
{
@ -165,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = Ball.Tracking;
double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
Ball.UpdateProgress(completionProgress);
Body.UpdateProgress(completionProgress);

View File

@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public class DrawableSliderHead : DrawableHitCircle
{
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
private readonly Slider slider;
@ -27,17 +26,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void load()
{
positionBindable.BindTo(HitObject.PositionBindable);
pathBindable.BindTo(slider.PathBindable);
pathVersion.BindTo(slider.Path.Version);
positionBindable.BindValueChanged(_ => updatePosition());
pathBindable.BindValueChanged(_ => updatePosition(), true);
pathVersion.BindValueChanged(_ => updatePosition(), true);
}
protected override void Update()
{
base.Update();
double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
@ -21,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
: base(hitCircle)
@ -36,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true;
positionBindable.BindTo(hitCircle.PositionBindable);
pathBindable.BindTo(slider.PathBindable);
pathVersion.BindTo(slider.Path.Version);
positionBindable.BindValueChanged(_ => updatePosition());
pathBindable.BindValueChanged(_ => updatePosition(), true);
pathVersion.BindValueChanged(_ => updatePosition(), true);
// TODO: This has no drawable content. Support for skins should be added.
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -136,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
}
public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
public float Progress => Math.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{

View File

@ -16,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Masking = true;
CornerRadius = Size.X / 2;
CornerExponent = 2;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;

View File

@ -18,10 +18,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = new Container
InternalChild = new CircularContainer
{
Masking = true,
CornerRadius = Size.X / 2,
BorderThickness = 10,
BorderColour = Color4.White,
RelativeSizeAxes = Axes.Both,

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var spanProgress = slider.ProgressAt(completionProgress);
double start = 0;
double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
if (span >= slider.SpanCount() - 1)
{

View File

@ -20,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
const int count = 18;
const float count = 18;
for (int i = 0; i < count; i++)
for (float i = 0; i < count; i++)
{
Add(new Container
{
@ -40,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Size = new Vector2(60, 10),
Origin = Anchor.Centre,
Position = new Vector2(
0.5f + (float)Math.Sin((float)i / count * 2 * MathHelper.Pi) / 2 * 0.86f,
0.5f + (float)Math.Cos((float)i / count * 2 * MathHelper.Pi) / 2 * 0.86f
0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f,
0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.86f
),
Rotation = -(float)i / count * 360 + 90,
Rotation = -i / count * 360 + 90,
Children = new[]
{
new Box

View File

@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -28,17 +27,21 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
public readonly Bindable<SliderPath> PathBindable = new Bindable<SliderPath>();
private readonly SliderPath path = new SliderPath();
public SliderPath Path
{
get => PathBindable.Value;
get => path;
set
{
PathBindable.Value = value;
endPositionCache.Invalidate();
path.ControlPoints.Clear();
path.ExpectedDistance.Value = null;
updateNestedPositions();
if (value != null)
{
path.ControlPoints.AddRange(value.ControlPoints);
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
}
@ -50,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
base.Position = value;
endPositionCache.Invalidate();
updateNestedPositions();
}
}
@ -112,6 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
SamplesBindable.ItemsAdded += _ => updateNestedSamples();
SamplesBindable.ItemsRemoved += _ => updateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
@ -189,6 +191,8 @@ namespace osu.Game.Rulesets.Osu.Objects
private void updateNestedPositions()
{
endPositionCache.Invalidate();
if (HeadCircle != null)
HeadCircle.Position = Position;

View File

@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public class SliderTailCircle : SliderCircle
{
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
public SliderTailCircle(Slider slider)
{
pathBindable.BindTo(slider.PathBindable);
pathBindable.BindValueChanged(_ => Position = slider.EndPosition);
pathVersion.BindTo(slider.Path.Version);
pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
public override Judgement CreateJudgement() => new OsuSliderTailJudgement();

View File

@ -10,7 +10,7 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Replays
private void addDelayedMovements(OsuHitObject h, OsuHitObject prev)
{
double endTime = (prev as IHasEndTime)?.EndTime ?? prev.StartTime;
double endTime = prev.GetEndTime();
HitWindows hitWindows = null;
@ -185,14 +185,14 @@ namespace osu.Game.Rulesets.Osu.Replays
{
Vector2 spinCentreOffset = SPINNER_CENTRE - prevPos;
float distFromCentre = spinCentreOffset.Length;
float distToTangentPoint = (float)Math.Sqrt(distFromCentre * distFromCentre - SPIN_RADIUS * SPIN_RADIUS);
float distToTangentPoint = MathF.Sqrt(distFromCentre * distFromCentre - SPIN_RADIUS * SPIN_RADIUS);
if (distFromCentre > SPIN_RADIUS)
{
// Previous cursor position was outside spin circle, set startPosition to the tangent point.
// Angle between centre offset and tangent point offset.
float angle = (float)Math.Asin(SPIN_RADIUS / distFromCentre);
float angle = MathF.Asin(SPIN_RADIUS / distFromCentre);
if (angle > 0)
{
@ -204,8 +204,8 @@ namespace osu.Game.Rulesets.Osu.Replays
}
// Rotate by angle so it's parallel to tangent line
spinCentreOffset.X = spinCentreOffset.X * (float)Math.Cos(angle) - spinCentreOffset.Y * (float)Math.Sin(angle);
spinCentreOffset.Y = spinCentreOffset.X * (float)Math.Sin(angle) + spinCentreOffset.Y * (float)Math.Cos(angle);
spinCentreOffset.X = spinCentreOffset.X * MathF.Cos(angle) - spinCentreOffset.Y * MathF.Sin(angle);
spinCentreOffset.Y = spinCentreOffset.X * MathF.Sin(angle) + spinCentreOffset.Y * MathF.Cos(angle);
// Set length to distToTangentPoint
spinCentreOffset.Normalize();
@ -275,7 +275,7 @@ namespace osu.Game.Rulesets.Osu.Replays
var startFrame = new OsuReplayFrame(h.StartTime, new Vector2(startPosition.X, startPosition.Y), action);
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY;
double hEndTime = h.GetEndTime() + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0;
var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Vector2 difference = startPosition - SPINNER_CENTRE;
float radius = difference.Length;
float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X);
float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X);
double t;

View File

@ -5,22 +5,20 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Scoring
{
internal class OsuScoreProcessor : ScoreProcessor<OsuHitObject>
internal class OsuScoreProcessor : ScoreProcessor
{
public OsuScoreProcessor(DrawableRuleset<OsuHitObject> drawableRuleset)
: base(drawableRuleset)
public OsuScoreProcessor(IBeatmap beatmap)
: base(beatmap)
{
}
private float hpDrainRate;
protected override void ApplyBeatmap(Beatmap<OsuHitObject> beatmap)
protected override void ApplyBeatmap(IBeatmap beatmap)
{
base.ApplyBeatmap(beatmap);

View File

@ -3,14 +3,16 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning
{
public class LegacyCursor : CompositeDrawable
public class LegacyCursor : OsuCursorSprite
{
private bool spin;
public LegacyCursor()
{
Size = new Vector2(50);
@ -22,7 +24,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
InternalChildren = new Drawable[]
spin = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
InternalChildren = new[]
{
new NonPlayfieldSprite
{
@ -30,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new NonPlayfieldSprite
ExpandTarget = new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
@ -38,5 +42,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
};
}
protected override void LoadComplete()
{
if (spin)
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
}
}
}

View File

@ -49,7 +49,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
return this.GetAnimation(component.LookupName, true, false);
case OsuSkinComponents.SliderFollowCircle:
return this.GetAnimation("sliderfollowcircle", true, true);
var followCircle = this.GetAnimation("sliderfollowcircle", true, true);
if (followCircle != null)
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
followCircle.Scale *= 0.5f;
return followCircle;
case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, "");

View File

@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderPathRadius,
AllowSliderBallTint,
CursorExpand,
CursorRotate
}
}

View File

@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private bool cursorExpand;
private Container expandTarget;
private SkinnableDrawable cursorSprite;
private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite;
public OsuCursor()
{
@ -37,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[BackgroundDependencyLoader]
private void load()
{
InternalChild = expandTarget = new Container
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@ -62,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public void Contract() => expandTarget.ScaleTo(released_scale, 100, Easing.OutQuad);
private class DefaultCursor : CompositeDrawable
private class DefaultCursor : OsuCursorSprite
{
public DefaultCursor()
{
@ -71,10 +73,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
new CircularContainer
ExpandTarget = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = size / 6,

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public abstract class OsuCursorSprite : CompositeDrawable
{
/// <summary>
/// The an optional piece of the cursor to expand when in a clicked state.
/// If null, the whole cursor will be affected by expansion.
/// </summary>
public Drawable ExpandTarget { get; protected set; }
}
}

View File

@ -18,6 +18,7 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
@ -30,7 +31,9 @@ namespace osu.Game.Rulesets.Osu.UI
{
}
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(Beatmap);
protected override Playfield CreatePlayfield() => new OsuPlayfield();

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" />
</manifest>

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Tests.Beatmaps;
@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
yield return new ConvertValue
{
StartTime = hitObject.StartTime,
EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime,
EndTime = hitObject.GetEndTime(),
IsRim = hitObject is RimHit,
IsCentre = hitObject is CentreHit,
IsDrumRoll = hitObject is DrumRoll,

View File

@ -73,127 +73,133 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
{
var distanceData = obj as IHasDistance;
var repeatsData = obj as IHasRepeats;
var endTimeData = obj as IHasEndTime;
var curveData = obj as IHasCurve;
// Old osu! used hit sounding to determine various hit type information
IList<HitSampleInfo> samples = obj.Samples;
bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
if (distanceData != null)
switch (obj)
{
// Number of spans of the object - one for the initial length and for each repeat
int spans = repeatsData?.SpanCount() ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
double speedAdjustment = difficultyPoint.SpeedMultiplier;
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
double distance = distanceData.Distance * spans * legacy_velocity_multiplier;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the taiko hit object
double taikoDuration = distance / taikoVelocity;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
// osu-stable always uses the speed-adjusted beatlength to determine the velocities, but
// only uses it for tick rate if beatmap version < 8
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
speedAdjustedBeatLength *= speedAdjustment;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
case IHasDistance distanceData:
{
List<IList<HitSampleInfo>> allSamples = curveData != null ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
// Number of spans of the object - one for the initial length and for each repeat
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
int i = 0;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
double speedAdjustment = difficultyPoint.SpeedMultiplier;
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
double distance = distanceData.Distance * spans * legacy_velocity_multiplier;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the taiko hit object
double taikoDuration = distance / taikoVelocity;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
// osu-stable always uses the speed-adjusted beatlength to determine the velocities, but
// only uses it for tick rate if beatmap version < 8
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
speedAdjustedBeatLength *= speedAdjustment;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
IList<HitSampleInfo> currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
List<IList<HitSampleInfo>> allSamples = obj is IHasCurve curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
if (isRim)
{
yield return new RimHit
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong
};
}
else
{
yield return new CentreHit
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong
};
}
int i = 0;
i = (i + 1) % allSamples.Count;
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{
IList<HitSampleInfo> currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
if (isRim)
{
yield return new RimHit
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong
};
}
else
{
yield return new CentreHit
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong
};
}
i = (i + 1) % allSamples.Count;
}
}
}
else
{
yield return new DrumRoll
else
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
};
}
}
else if (endTimeData != null)
{
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
yield return new DrumRoll
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
};
}
yield return new Swell
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
};
}
else
{
bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
if (isRim)
{
yield return new RimHit
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong
};
break;
}
else
case IHasEndTime endTimeData:
{
yield return new CentreHit
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
yield return new Swell
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong
Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
};
break;
}
default:
{
bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
if (isRim)
{
yield return new RimHit
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong
};
}
else
{
yield return new CentreHit
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong
};
}
break;
}
}
}

View File

@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
mods = Score.Mods;
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
countGreat = Score.Statistics[HitResult.Great];
countGood = Score.Statistics[HitResult.Good];
countMeh = Score.Statistics[HitResult.Meh];
countMiss = Score.Statistics[HitResult.Miss];
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more
double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0);
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
strainValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available

View File

@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.MathUtils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Framework.Graphics;
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
else
rollingHits--;
rollingHits = MathHelper.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
MainPiece.FadeAccent(newColour, 100);

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
var completion = (float)numHits / HitObject.RequiredHits;
expandingRing
.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50)
.FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50)
.Then()
.FadeTo(completion / 8, 2000, Easing.OutQuint);

View File

@ -10,27 +10,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{
public class TaikoPiece : BeatSyncedContainer, IHasAccentColour
{
private Color4 accentColour;
/// <summary>
/// The colour of the inner circle and outer glows.
/// </summary>
public virtual Color4 AccentColour
{
get => accentColour;
set => accentColour = value;
}
private bool kiaiMode;
public virtual Color4 AccentColour { get; set; }
/// <summary>
/// Whether Kiai mode effects are enabled for this circle piece.
/// </summary>
public virtual bool KiaiMode
{
get => kiaiMode;
set => kiaiMode = value;
}
public virtual bool KiaiMode { get; set; }
public TaikoPiece()
{

View File

@ -43,76 +43,83 @@ namespace osu.Game.Rulesets.Taiko.Replays
IHasEndTime endTimeData = h as IHasEndTime;
double endTime = endTimeData?.EndTime ?? h.StartTime;
Swell swell = h as Swell;
DrumRoll drumRoll = h as DrumRoll;
Hit hit = h as Hit;
if (swell != null)
switch (h)
{
int d = 0;
int count = 0;
int req = swell.RequiredHits;
double hitRate = Math.Min(swell_hit_speed, swell.Duration / req);
for (double j = h.StartTime; j < endTime; j += hitRate)
case Swell swell:
{
TaikoAction action;
int d = 0;
int count = 0;
int req = swell.RequiredHits;
double hitRate = Math.Min(swell_hit_speed, swell.Duration / req);
switch (d)
for (double j = h.StartTime; j < endTime; j += hitRate)
{
default:
case 0:
action = TaikoAction.LeftCentre;
break;
TaikoAction action;
case 1:
action = TaikoAction.LeftRim;
break;
switch (d)
{
default:
case 0:
action = TaikoAction.LeftCentre;
break;
case 2:
action = TaikoAction.RightCentre;
break;
case 1:
action = TaikoAction.LeftRim;
break;
case 3:
action = TaikoAction.RightRim;
case 2:
action = TaikoAction.RightCentre;
break;
case 3:
action = TaikoAction.RightRim;
break;
}
Frames.Add(new TaikoReplayFrame(j, action));
d = (d + 1) % 4;
if (++count == req)
break;
}
Frames.Add(new TaikoReplayFrame(j, action));
d = (d + 1) % 4;
if (++count == req)
break;
}
}
else if (drumRoll != null)
{
foreach (var tick in drumRoll.NestedHitObjects.OfType<DrumRollTick>())
{
Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre));
hitButton = !hitButton;
}
}
else if (hit != null)
{
TaikoAction[] actions;
if (hit is CentreHit)
{
actions = h.IsStrong
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
: new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre };
}
else
{
actions = h.IsStrong
? new[] { TaikoAction.LeftRim, TaikoAction.RightRim }
: new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim };
break;
}
Frames.Add(new TaikoReplayFrame(h.StartTime, actions));
case DrumRoll drumRoll:
{
foreach (var tick in drumRoll.NestedHitObjects.OfType<DrumRollTick>())
{
Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre));
hitButton = !hitButton;
}
break;
}
case Hit hit:
{
TaikoAction[] actions;
if (hit is CentreHit)
{
actions = h.IsStrong
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
: new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre };
}
else
{
actions = h.IsStrong
? new[] { TaikoAction.LeftRim, TaikoAction.RightRim }
: new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim };
}
Frames.Add(new TaikoReplayFrame(h.StartTime, actions));
break;
}
default:
throw new InvalidOperationException("Unknown hit object type.");
}
else
throw new InvalidOperationException("Unknown hit object type.");
var nextHitObject = GetNextObject(i); // Get the next object that requires pressing the same button

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