1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 06:21:22 +08:00

Compare commits

...

4258 Commits

1464 changed files with 43966 additions and 14573 deletions
+10 -10
View File
@@ -2,12 +2,6 @@
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "0.35.0",
"commands": [
"dotnet-cake"
]
},
"dotnet-format": {
"version": "3.1.37601",
"commands": [
@@ -20,17 +14,23 @@
"jb"
]
},
"nvika": {
"version": "2.0.0",
"smoogipoo.nvika": {
"version": "1.0.1",
"commands": [
"nvika"
]
},
"codefilesanity": {
"version": "15.0.0",
"version": "0.0.36",
"commands": [
"CodeFileSanity"
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.725.0",
"commands": [
"localisation"
]
}
}
}
}
+6 -12
View File
@@ -10,14 +10,6 @@ trim_trailing_whitespace = true
#Roslyn naming styles
#PascalCase for public and protected members
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
dotnet_naming_rule.public_members_pascalcase.severity = error
dotnet_naming_rule.public_members_pascalcase.symbols = public_members
dotnet_naming_rule.public_members_pascalcase.style = pascalcase
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
@@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = false:silent
#Style - other C# 7.x features
dotnet_style_prefer_inferred_tuple_names = true:warning
@@ -176,8 +168,8 @@ 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 = true:warning
csharp_style_prefer_range_operator = true:warning
csharp_style_prefer_index_operator = false:silent
csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers
@@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none
dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
dotnet_diagnostic.RS0030.severity = error
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.
@@ -1,7 +0,0 @@
---
name: Feature Request
about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
**Proposal designs of the feature:**
+8 -1
View File
@@ -1,5 +1,12 @@
blank_issues_enabled: false
contact_links:
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here.
about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.
+94
View File
@@ -0,0 +1,94 @@
on: [push, pull_request]
name: Continuous Integration
jobs:
test:
name: Test
runs-on: ${{matrix.os.fullname}}
env:
OSU_EXECUTION_MODE: ${{matrix.threadingMode}}
strategy:
fail-fast: false
matrix:
os:
- { prettyname: Windows, fullname: windows-latest }
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
# https://github.com/ppy/osu-framework/issues/4349
# Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
- name: Install libavformat-dev
if: ${{matrix.os.fullname == 'ubuntu-latest'}}
run: |
sudo apt-get update && \
sudo apt-get -y install libavformat-dev
- name: Compile
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
shell: pwsh
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
dotnet codefilesanity | while read -r line; do
echo "::warning::$line"
done
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+32
View File
@@ -0,0 +1,32 @@
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
# See:
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
name: Annotate CI run with test results
on:
workflow_run:
workflows: ["Continuous Integration"]
types:
- completed
jobs:
annotate:
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
strategy:
fail-fast: false
matrix:
os:
- { prettyname: Windows }
- { prettyname: macOS }
- { prettyname: Linux }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 5
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2
with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
+163
View File
@@ -0,0 +1,163 @@
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
# Usage:
# !pp check 0 | Runs only the osu! ruleset.
# !pp check 0 2 | Runs only the osu! and catch rulesets.
#
name: Diffcalc Consistency Checks
on:
issue_comment:
types: [ created ]
env:
DB_USER: root
DB_HOST: 127.0.0.1
CONCURRENCY: 4
ALLOW_DOWNLOAD: 1
SAVE_DOWNLOADED: 1
jobs:
diffcalc:
name: Diffcalc
runs-on: ubuntu-latest
continue-on-error: true
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '!pp check') &&
(github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
strategy:
fail-fast: false
matrix:
ruleset:
- { name: osu, id: 0 }
- { name: taiko, id: 1 }
- { name: catch, id: 2 }
- { name: mania, id: 3 }
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Verify ruleset
if: contains(github.event.comment.body, matrix.ruleset.id) == false
run: |
echo "${{ github.event.comment.body }} doesn't contain ${{ matrix.ruleset.id }}"
exit 1
- name: Verify MySQL connection from host
run: |
sudo apt-get install -y mysql-client
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "SHOW DATABASES"
- name: Create directory structure
run: |
mkdir -p $GITHUB_WORKSPACE/master/
mkdir -p $GITHUB_WORKSPACE/pr/
# Checkout osu
- name: Checkout osu (master)
uses: actions/checkout@v2
with:
repository: ppy/osu
path: 'master/osu'
- name: Checkout osu (pr)
uses: actions/checkout@v2
with:
path: 'pr/osu'
# Checkout osu-difficulty-calculator
- name: Checkout osu-difficulty-calculator (master)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# Sanity checks to make sure diffcalc is not run when incompatible.
- name: Build diffcalc (master)
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
- name: Build diffcalc (pr)
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
# Initial data imports
- name: Download + import data
run: |
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
# Set env variable for further steps.
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
cd $GITHUB_WORKSPACE
wget https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
wget https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
cd $GITHUB_WORKSPACE/$PERFORMANCE_DATA_NAME
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_master"
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_pr"
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_master
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_pr
# Run diffcalc
- name: Run diffcalc (master)
env:
DB_NAME: osu_master
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
- name: Run diffcalc (pr)
env:
DB_NAME: osu_pr
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
# Print diffs
- name: Print diffs
run: |
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "
SELECT
m.beatmap_id,
m.mods,
m.diff_unified as 'sr_master',
p.diff_unified as 'sr_pr',
(p.diff_unified - m.diff_unified) as 'diff'
FROM osu_master.osu_beatmap_difficulty m
JOIN osu_pr.osu_beatmap_difficulty p
ON m.beatmap_id = p.beatmap_id
AND m.mode = p.mode
AND m.mods = p.mods
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
ORDER BY abs(m.diff_unified - p.diff_unified)
DESC
LIMIT 10000;"
# Todo: Run ppcalc
+3
View File
@@ -336,3 +336,6 @@ inspectcode
/BenchmarkDotNet.Artifacts
*.GeneratedMSBuildEditorConfig.editorconfig
# Fody (pulled in by Realm) - schema file
FodyWeavers.xsd
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="osu-client-sqlite" uuid="1aa4b9be-cd8d-47ae-8186-30a13cd724a5">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Dual client test" type="CompoundRunConfigurationType">
<toRun name="osu!" type="DotNetProject" />
<toRun name="osu! (Second Client)" type="DotNetProject" />
<method v="2" />
</configuration>
</component>
+20
View File
@@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="osu! (Second Client)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0/osu!.dll" />
<option name="PROGRAM_PARAMETERS" value="--debug-client-id=1" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/osu.Desktop/osu.Desktop.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net5.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>
-14
View File
@@ -113,20 +113,6 @@
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build benchmarks",
"console": "internalConsole"
},
{
"name": "Cake: Debug Script",
"type": "coreclr",
"request": "launch",
"program": "${workspaceRoot}/build/tools/Cake.CoreCLR/0.30.0/Cake.dll",
"args": [
"${workspaceRoot}/build/build.cake",
"--debug",
"--verbosity=diagnostic"
],
"cwd": "${workspaceRoot}/build",
"stopAtEntry": true,
"externalConsole": false
}
]
}
+1
View File
@@ -3,6 +3,7 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us
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.
T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
+3
View File
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Realm DisableAnalytics="true" />
</Weavers>
+10 -26
View File
@@ -1,27 +1,11 @@
[CmdletBinding()]
Param(
[string]$Target,
[string]$Configuration,
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity,
[switch]$ShowDescription,
[Alias("WhatIf", "Noop")]
[switch]$DryRun,
[Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
[string[]]$ScriptArgs
)
# Build Cake arguments
$cakeArguments = "";
if ($Target) { $cakeArguments += "-target=$Target" }
if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
if ($ShowDescription) { $cakeArguments += "-showdescription" }
if ($DryRun) { $cakeArguments += "-dryrun" }
if ($Experimental) { $cakeArguments += "-experimental" }
$cakeArguments += $ScriptArgs
dotnet tool restore
dotnet cake ./build/InspectCode.cake --bootstrap
dotnet cake ./build/InspectCode.cake $cakeArguments
exit $LASTEXITCODE
# Temporarily disabled until the tool is upgraded to 5.0.
# The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7.
# - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
dotnet tool restore
dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+5 -5
View File
@@ -4,14 +4,14 @@
# osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu)
[![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
A free-to-win rhythm game. 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.
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
## Status
@@ -23,7 +23,7 @@ We are accepting bug reports (please report with as much detail as possible and
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where lazer is currently and the roadmap going forward.
- Read peppy's [blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
## Running osu!
@@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
@@ -97,7 +97,7 @@ Before committing your code, please run a code formatter. This can be achieved b
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.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
@@ -10,9 +10,9 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.EmptyFreeform.Mods
{
public class EmptyFreeformModAutoplay : ModAutoplay<EmptyFreeformHitObject>
public class EmptyFreeformModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
@@ -10,9 +10,9 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
@@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject>
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
@@ -10,9 +10,9 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
@@ -3,7 +3,6 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
@@ -11,7 +10,7 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.EmptyScrolling.Mods
{
public class EmptyScrollingModAutoplay : ModAutoplay<EmptyScrollingHitObject>
public class EmptyScrollingModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
@@ -10,9 +10,9 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
@@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject>
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
+9 -6
View File
@@ -1,24 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2019
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
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
- cmd: dotnet --info # Useful when version mismatch between CI and local
- cmd: 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:
-17
View File
@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="..\osu.Desktop\osu.Desktop.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch.Tests\osu.Game.Rulesets.Catch.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania.Tests\osu.Game.Rulesets.Mania.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Osu.Tests\osu.Game.Rulesets.Osu.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko.Tests\osu.Game.Rulesets.Taiko.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Tournament.Tests\osu.Game.Tournament.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Tournament\osu.Game.Tournament.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
</Project>
-41
View File
@@ -1,41 +0,0 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.36"
///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
var target = Argument("target", "CodeAnalysis");
var configuration = Argument("configuration", "Release");
var rootDirectory = new DirectoryPath("..");
var sln = rootDirectory.CombineWithFilePath("osu.sln");
var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("InspectCode")
.Does(() => {
var inspectcodereport = "inspectcodereport.xml";
var cacheDir = "inspectcode";
var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
DotNetCoreTool(rootDirectory.FullPath,
"jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
});
Task("CodeFileSanity")
.Does(() => {
ValidateCodeSanity(new ValidateCodeSanitySettings {
RootDirectory = rootDirectory.FullPath,
IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
});
});
Task("CodeAnalysis")
.IsDependentOn("CodeFileSanity")
.IsDependentOn("InspectCode");
RunTarget(target);
-5
View File
@@ -1,5 +0,0 @@
[Nuget]
Source=https://api.nuget.org/v3/index.json
UseInProcessClient=true
LoadDependencies=true
+6 -2
View File
@@ -51,7 +51,11 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.521.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.3.0" />
</ItemGroup>
</Project>
+17 -1
View File
@@ -20,7 +20,21 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
{
"application/zip",
"application/octet-stream",
"application/download",
"application/x-zip",
"application/x-zip-compressed",
// newer official mime types (see https://osu.ppy.sh/wiki/en/osu%21_File_Formats).
"application/x-osu-beatmap-archive",
"application/x-osu-skin-archive",
"application/x-osu-replay",
})]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
@@ -65,12 +79,14 @@ namespace osu.Android
case Intent.ActionSendMultiple:
{
var uris = new List<Uri>();
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
{
var content = intent.ClipData?.GetItemAt(i);
if (content != null)
uris.Add(content.Uri);
}
handleImportFromUris(uris.ToArray());
break;
}
+1 -1
View File
@@ -64,7 +64,7 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>
+2 -2
View File
@@ -139,8 +139,8 @@ namespace osu.Desktop
{
switch (activity)
{
case UserActivity.SoloGame solo:
return solo.Beatmap.ToString();
case UserActivity.InGame game:
return game.Beatmap.ToString();
case UserActivity.Editing edit:
return edit.Beatmap.ToString();
+1 -1
View File
@@ -57,7 +57,7 @@ namespace osu.Desktop
private string getStableInstallPath()
{
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
string stableInstallPath;
+35 -12
View File
@@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
@@ -17,13 +16,43 @@ namespace osu.Desktop
{
public static class Program
{
private const string base_game_name = @"osu";
[STAThread]
public static int Main(string[] args)
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
string gameName = base_game_name;
bool tournamentClient = false;
foreach (var arg in args)
{
var split = arg.Split('=');
var key = split[0];
var val = split.Length > 1 ? split[1] : string.Empty;
switch (key)
{
case "--tournament":
tournamentClient = true;
break;
case "--debug-client-id":
if (!DebugUtils.IsDebugBuild)
throw new InvalidOperationException("Cannot use this argument in a non-debug build.");
if (!int.TryParse(val, out int clientID))
throw new ArgumentException("Provided client ID must be an integer.");
gameName = $"{base_game_name}-{clientID}";
break;
}
}
using (DesktopGameHost host = Host.GetSuitableHost(gameName, true))
{
host.ExceptionThrown += handleException;
@@ -48,16 +77,10 @@ namespace osu.Desktop
return 0;
}
switch (args.FirstOrDefault() ?? string.Empty)
{
default:
host.Run(new OsuGameDesktop(args));
break;
case "--tournament":
host.Run(new TournamentGame());
break;
}
if (tournamentClient)
host.Run(new TournamentGame());
else
host.Run(new OsuGameDesktop(args));
return 0;
}
+5 -3
View File
@@ -68,6 +68,8 @@ namespace osu.Desktop.Updater
return false;
}
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
@@ -98,7 +100,6 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
{
@@ -110,13 +111,14 @@ namespace osu.Desktop.Updater
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@@ -141,7 +143,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
.ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}
+4 -4
View File
@@ -5,23 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable<bool> disableWinKey;
private Bindable<bool> localUserPlaying;
private IBindable<bool> localUserPlaying;
[Resolved]
private GameHost host { get; set; }
[BackgroundDependencyLoader]
private void load(OsuGame game, OsuConfigManager config)
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
{
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
+2 -2
View File
@@ -5,8 +5,8 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
<AssemblyName>osu!</AssemblyName>
<Title>osu!lazer</Title>
<Product>osu!lazer</Product>
<Title>osu!</Title>
<Product>osu!(lazer)</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.0.0</Version>
+1 -2
View File
@@ -3,7 +3,7 @@
<metadata>
<id>osulazer</id>
<version>0.0.0</version>
<title>osu!lazer</title>
<title>osu!</title>
<authors>ppy Pty Ltd</authors>
<owners>Dean Herbert</owners>
<projectUrl>https://osu.ppy.sh/</projectUrl>
@@ -20,4 +20,3 @@
<file src="**.config" target="lib\net45\"/>
</files>
</package>
+34
View File
@@ -0,0 +1,34 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
private OsuModDoubleTime mod;
[Params(1, 10, 100)]
public int Times { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
mod = new OsuModDoubleTime();
}
[Benchmark]
public int ModHashCode()
{
var hashCode = new HashCode();
for (int i = 0; i < Times; i++)
hashCode.Add(mod);
return hashCode.ToHashCode();
}
}
}
@@ -7,9 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class CatchEditorTestSceneContainer : Container
{
[Cached(typeof(Playfield))]
public readonly ScrollingPlayfield Playfield;
protected override Container<Drawable> Content { get; }
public CatchEditorTestSceneContainer()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Width = CatchPlayfield.WIDTH;
Height = 1000;
Padding = new MarginPadding
{
Bottom = 100
};
InternalChildren = new Drawable[]
{
new ScrollingTestContainer(ScrollingDirection.Down)
{
TimeRange = 1000,
RelativeSizeAxes = Axes.Both,
Child = Playfield = new TestCatchPlayfield
{
RelativeSizeAxes = Axes.Both
}
},
new PlayfieldBorder
{
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full },
Clock = new FramedClock(new StopwatchClock(true))
},
Content = new Container
{
RelativeSizeAxes = Axes.Both
}
};
}
private class TestCatchPlayfield : CatchEditorPlayfield
{
public TestCatchPlayfield()
: base(new BeatmapDifficulty { CircleSize = 0 })
{
}
}
}
}
@@ -0,0 +1,79 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
protected const double TIME_SNAP = 100;
protected DrawableCatchHitObject LastObject;
protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchPlacementBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
contentContainer.Playfield.Clock = new FramedClock(new ManualClock());
}
[SetUp]
public void Setup() => Schedule(() =>
{
HitObjectContainer.Clear();
ResetPlacement();
LastObject = null;
});
protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
{
float y = HitObjectContainer.PositionAtTime(time);
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
InputManager.MoveMouseTo(pos);
});
protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () =>
{
InputManager.Click(button);
});
protected IEnumerable<FruitOutline> FruitOutlines => Content.ChildrenOfType<FruitOutline>();
// Unused because AddHitObject is overriden
protected override Container CreateHitObjectContainer() => new Container();
protected override void AddHitObject(DrawableHitObject hitObject)
{
LastObject = (DrawableCatchHitObject)hitObject;
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
return result;
}
}
}
@@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
protected readonly EditorBeatmap EditorBeatmap;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchSelectionBlueprintTestScene()
{
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{
BeatLength = 100
});
base.Content.Add(new EditorBeatmapDependencyContainer(EditorBeatmap, new BindableBeatDivisor())
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
EditorBeatmap,
contentContainer = new CatchEditorTestSceneContainer()
},
});
}
protected void AddMouseMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
{
float y = HitObjectContainer.PositionAtTime(time);
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
InputManager.MoveMouseTo(pos);
});
private class EditorBeatmapDependencyContainer : Container
{
[Cached]
private readonly EditorClock editorClock;
[Cached]
private readonly BindableBeatDivisor beatDivisor;
public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
{
editorClock = new EditorClock(beatmap, beatDivisor);
this.beatDivisor = beatDivisor;
}
}
}
}
@@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override void AddHitObject(DrawableHitObject hitObject)
{
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
base.AddHitObject(hitObject);
}
[Test]
public void TestBasicPlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestReversePlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestFinishWithZeroDuration()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is not placed", () => LastObject == null);
AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting);
}
[Test]
public void TestOpacity()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
AddMoveStep(200, 0);
AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1));
AddMoveStep(100, 0);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
}
private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType<TimeSpanOutline>().Single();
}
}
@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
}
}
@@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test]
public void TestFruitPlacementPosition()
{
const double time = 300;
const float x = CatchPlayfield.CENTER_X;
AddMoveStep(time, x);
AddClickStep(MouseButton.Left);
AddAssert("outline position is correct", () =>
{
var outline = FruitOutlines.Single();
return Precision.AlmostEquals(outline.X, x) &&
Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time));
});
AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time));
AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x));
}
}
}
@@ -0,0 +1,155 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
{
private const double velocity = 0.5;
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
}
[Test]
public void TestBasicPlacement()
{
double[] times = { 300, 800 };
float[] positions = { 100, 200 };
addPlacementSteps(times, positions);
AddAssert("juice stream is placed", () => lastObject != null);
AddAssert("start time is correct", () => Precision.AlmostEquals(lastObject.StartTime, times[0]));
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
}
[Test]
public void TestEmptyNotCommitted()
{
addMoveAndClickSteps(100, 100);
addMoveAndClickSteps(100, 100);
addMoveAndClickSteps(100, 100, true);
AddAssert("juice stream not placed", () => lastObject == null);
}
[Test]
public void TestMultipleSegments()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 100, 150, 100, 100 };
addPlacementSteps(times, positions);
AddAssert("has 4 vertices", () => lastObject.Path.ControlPoints.Count == 4);
addPathCheckStep(times, positions);
}
[Test]
public void TestVelocityLimit()
{
double[] times = { 100, 300 };
float[] positions = { 200, 500 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300 });
}
[Test]
public void TestPreviousVerticesAreFixed()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 200, 400, 100, 500 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
}
[Test]
public void TestClampedPositionIsRestored()
{
double[] times = { 100, 300, 500 };
float[] positions = { 200, 200, 0, 250 };
addMoveAndClickSteps(times[0], positions[0]);
addMoveAndClickSteps(times[1], positions[1]);
AddMoveStep(times[2], positions[2]);
addMoveAndClickSteps(times[2], positions[3], true);
addPathCheckStep(times, new float[] { 200, 200, 250 });
}
[Test]
public void TestFirstVertexIsFixed()
{
double[] times = { 100, 200 };
float[] positions = { 100, 300 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 100, 150 });
}
[Test]
public void TestOutOfOrder()
{
double[] times = { 100, 700, 500, 300 };
float[] positions = { 100, 200, 150, 50 };
addPlacementSteps(times, positions);
addPathCheckStep(times, positions);
}
[Test]
public void TestMoveBeforeFirstVertex()
{
double[] times = { 300, 500, 100 };
float[] positions = { 100, 100, 100 };
addPlacementSteps(times, positions);
AddAssert("start time is correct", () => Precision.AlmostEquals(lastObject.StartTime, times[0]));
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1], 1e-3));
}
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
private void addMoveAndClickSteps(double time, float position, bool end = false)
{
AddMoveStep(time, position);
AddClickStep(end ? MouseButton.Right : MouseButton.Left);
}
private void addPlacementSteps(double[] times, float[] positions)
{
for (int i = 0; i < times.Length; i++)
addMoveAndClickSteps(times[i], positions[i], i == times.Length - 1);
}
private void addPathCheckStep(double[] times, float[] positions) => AddStep("assert path is correct", () =>
Assert.That(getPositions(times), Is.EqualTo(positions).Within(Precision.FLOAT_EPSILON)));
private float[] getPositions(IEnumerable<double> times)
{
JuiceStream hitObject = lastObject.AsNonNull();
return times
.Select(time => (time - hitObject.StartTime) * hitObject.Velocity)
.Select(distance => hitObject.EffectiveX + hitObject.Path.PositionAt(distance / hitObject.Distance).X)
.ToArray();
}
}
}
@@ -0,0 +1,286 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
private JuiceStream hitObject;
private readonly ManualClock manualClock = new ManualClock();
[SetUp]
public void SetUp() => Schedule(() =>
{
EditorBeatmap.Clear();
Content.Clear();
manualClock.CurrentTime = 0;
Content.Clock = new FramedClock(manualClock);
InputManager.ReleaseButton(MouseButton.Left);
InputManager.ReleaseKey(Key.ShiftLeft);
InputManager.ReleaseKey(Key.ControlLeft);
});
[Test]
public void TestBasicComponentLayout()
{
double[] times = { 100, 300, 500 };
float[] positions = { 100, 200, 100 };
addBlueprintStep(times, positions);
for (int i = 0; i < times.Length; i++)
addVertexCheckStep(times.Length, i, times[i], positions[i]);
AddAssert("correct outline count", () =>
{
var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
return this.ChildrenOfType<FruitOutline>().Count() == expected;
});
AddAssert("correct vertex piece count", () =>
this.ChildrenOfType<VertexPiece>().Count() == times.Length);
AddAssert("first vertex is semitransparent", () =>
Precision.DefinitelyBigger(1, this.ChildrenOfType<VertexPiece>().First().Alpha));
}
[Test]
public void TestVertexDrag()
{
double[] times = { 100, 400, 700 };
float[] positions = { 100, 100, 100 };
addBlueprintStep(times, positions);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(500, 150);
addVertexCheckStep(3, 1, 500, 150);
addDragEndStep();
addDragStartStep(times[2], positions[2]);
AddMouseMoveStep(300, 50);
addVertexCheckStep(3, 1, 300, 50);
addVertexCheckStep(3, 2, 500, 150);
AddMouseMoveStep(-100, 100);
addVertexCheckStep(3, 1, times[0], positions[0]);
}
[Test]
public void TestMultipleDrag()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 100, 100, 100, 100 };
addBlueprintStep(times, positions);
AddMouseMoveStep(times[1], positions[1]);
AddStep("press left", () => InputManager.PressButton(MouseButton.Left));
AddStep("release left", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
addDragStartStep(times[2], positions[2]);
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
}
[Test]
public void TestClampedPositionIsRestored()
{
const double velocity = 0.25;
double[] times = { 100, 500, 700 };
float[] positions = { 100, 100, 100 };
addBlueprintStep(times, positions, velocity);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 200);
addVertexCheckStep(3, 1, times[1], 200);
addVertexCheckStep(3, 2, times[2], 150);
AddMouseMoveStep(times[1], 100);
addVertexCheckStep(3, 1, times[1], 100);
// Stored position is restored.
addVertexCheckStep(3, 2, times[2], positions[2]);
AddMouseMoveStep(times[1], 300);
addDragEndStep();
addDragStartStep(times[1], 300);
AddMouseMoveStep(times[1], 100);
// Position is different because a changed position is committed when the previous drag is ended.
addVertexCheckStep(3, 2, times[2], 250);
}
[Test]
public void TestScrollWhileDrag()
{
double[] times = { 300, 500 };
float[] positions = { 100, 100 };
addBlueprintStep(times, positions);
addDragStartStep(times[1], positions[1]);
// This mouse move is necessary to start drag and capture the input.
AddMouseMoveStep(times[1], positions[1] + 50);
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
}
[Test]
public void TestUpdateFromHitObject()
{
double[] times = { 100, 300 };
float[] positions = { 200, 200 };
addBlueprintStep(times, positions);
AddStep("update hit object path", () =>
{
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(100, 100),
new Vector2(0, 200),
});
EditorBeatmap.Update(hitObject);
});
AddAssert("path is updated", () => getVertices().Count > 2);
}
[Test]
public void TestAddVertex()
{
double[] times = { 100, 700 };
float[] positions = { 200, 200 };
addBlueprintStep(times, positions, 0.2);
addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 220);
addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(750, 180);
addVertexCheckStep(5, 4, 750, 180);
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
}
[Test]
public void TestDeleteVertex()
{
double[] times = { 100, 300, 500 };
float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions);
addDeleteVertexSteps(times[1], positions[1]);
addVertexCheckStep(2, 1, times[2], positions[2]);
// The first vertex cannot be deleted.
addDeleteVertexSteps(times[0], positions[0]);
addVertexCheckStep(2, 0, times[0], positions[0]);
addDeleteVertexSteps(times[2], positions[2]);
addVertexCheckStep(1, 0, times[0], positions[0]);
}
[Test]
public void TestVertexResampling()
{
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(100, 100),
new Vector2(50, 200),
}), 0.5);
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
addAddVertexSteps(150, 150);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
}
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
{
hitObject = new JuiceStream
{
StartTime = time,
X = x,
Path = sliderPath,
};
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
EditorBeatmap.Add(hitObject);
EditorBeatmap.Update(hitObject);
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
});
private void addBlueprintStep(double[] times, float[] positions, double velocity = 0.5)
{
var path = new JuiceStreamPath();
for (int i = 1; i < times.Length; i++)
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
var sliderPath = new SliderPath();
path.ConvertToSliderPath(sliderPath, 0);
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
}
private IReadOnlyList<JuiceStreamPathVertex> getVertices() => this.ChildrenOfType<EditablePath>().Single().Vertices;
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
{
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
float expectedX = x - hitObject.OriginalX;
var vertices = getVertices();
return vertices.Count == count &&
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
Precision.AlmostEquals(vertices[index].X, expectedX);
});
private void addDragStartStep(double time, float x)
{
AddMouseMoveStep(time, x);
AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
}
private void addDragEndStep() => AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
private void addAddVertexSteps(double time, float x)
{
AddMouseMoveStep(time, x);
AddStep("add vertex", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
}
private void addDeleteVertexSteps(double time, float x)
{
AddMouseMoveStep(time, x);
AddStep("delete vertex", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ShiftLeft);
});
}
}
}
@@ -0,0 +1,288 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class JuiceStreamPathTest
{
[TestCase(1e3, true, false)]
// When the coordinates are large, the slope invariant fails within the specified absolute allowance due to the floating-number precision.
[TestCase(1e9, false, false)]
// Using discrete values sometimes discover more edge cases.
[TestCase(10, true, true)]
public void TestRandomInsertSetPosition(double scale, bool checkSlope, bool integralValues)
{
var rng = new Random(1);
var path = new JuiceStreamPath();
for (int iteration = 0; iteration < 100000; iteration++)
{
if (rng.Next(10) == 0)
path.Clear();
int vertexCount = path.Vertices.Count;
switch (rng.Next(2))
{
case 0:
{
double distance = rng.NextDouble() * scale * 2 - scale;
if (integralValues)
distance = Math.Round(distance);
float oldX = path.PositionAtDistance(distance);
int index = path.InsertVertex(distance);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
break;
}
case 1:
{
int index = rng.Next(path.Vertices.Count);
double distance = path.Vertices[index].Distance;
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
if (integralValues)
newX = MathF.Round(newX);
path.SetVertexPosition(index, newX);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
break;
}
}
assertInvariants(path.Vertices, checkSlope);
}
}
[Test]
public void TestRemoveVertices()
{
var path = new JuiceStreamPath();
path.Add(10, 5);
path.Add(20, -5);
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(0, 0),
new JuiceStreamPathVertex(20, -5)
}));
removeCount = path.RemoveVertices((_, i) => i == 0);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(20, -5)
}));
removeCount = path.RemoveVertices((_, i) => true);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex()
}));
}
[Test]
public void TestResampleVertices()
{
var path = new JuiceStreamPath();
path.Add(-100, -10);
path.Add(100, 50);
path.ResampleVertices(new double[]
{
-50,
0,
70,
120
});
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(-100, -10),
new JuiceStreamPathVertex(-50, -5),
new JuiceStreamPathVertex(0, 0),
new JuiceStreamPathVertex(70, 35),
new JuiceStreamPathVertex(100, 50),
new JuiceStreamPathVertex(100, 50),
}));
path.Clear();
path.SetVertexPosition(0, 10);
path.ResampleVertices(Array.Empty<double>());
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(0, 10)
}));
}
[Test]
public void TestRandomConvertFromSliderPath()
{
var rng = new Random(1);
var path = new JuiceStreamPath();
var sliderPath = new SliderPath();
for (int iteration = 0; iteration < 10000; iteration++)
{
sliderPath.ControlPoints.Clear();
do
{
int start = sliderPath.ControlPoints.Count;
do
{
float x = (float)(rng.NextDouble() * 1e3);
float y = (float)(rng.NextDouble() * 1e3);
sliderPath.ControlPoints.Add(new PathControlPoint(new Vector2(x, y)));
} while (rng.Next(2) != 0);
int length = sliderPath.ControlPoints.Count - start + 1;
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
} while (rng.Next(3) != 0);
if (rng.Next(5) == 0)
sliderPath.ExpectedDistance.Value = rng.NextDouble() * 3e3;
else
sliderPath.ExpectedDistance.Value = null;
path.ConvertFromSliderPath(sliderPath);
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
assertInvariants(path.Vertices, true);
double[] sampleDistances = Enumerable.Range(0, 10)
.Select(_ => rng.NextDouble() * sliderPath.Distance)
.ToArray();
foreach (double distance in sampleDistances)
{
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
}
path.ResampleVertices(sampleDistances);
assertInvariants(path.Vertices, true);
foreach (double distance in sampleDistances)
{
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
}
}
}
[Test]
public void TestRandomConvertToSliderPath()
{
var rng = new Random(1);
var path = new JuiceStreamPath();
var sliderPath = new SliderPath();
for (int iteration = 0; iteration < 10000; iteration++)
{
path.Clear();
do
{
double distance = rng.NextDouble() * 1e3;
float x = (float)(rng.NextDouble() * 1e3);
path.Add(distance, x);
} while (rng.Next(5) != 0);
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
path.ConvertToSliderPath(sliderPath, sliderStartY);
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true);
foreach (var point in sliderPath.ControlPoints)
{
Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
}
for (int i = 0; i < 10; i++)
{
double distance = rng.NextDouble() * path.Distance;
float expected = path.PositionAtDistance(distance);
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
}
}
}
[Test]
public void TestInvalidation()
{
var path = new JuiceStreamPath();
Assert.That(path.InvalidationID, Is.EqualTo(1));
int previousId = path.InvalidationID;
path.InsertVertex(10);
checkNewId();
path.SetVertexPosition(1, 5);
checkNewId();
path.Add(20, 0);
checkNewId();
path.RemoveVertices((v, _) => v.Distance == 20);
checkNewId();
path.ResampleVertices(new double[] { 5, 10, 15 });
checkNewId();
path.Clear();
checkNewId();
path.ConvertFromSliderPath(new SliderPath());
checkNewId();
void checkNewId()
{
Assert.That(path.InvalidationID, Is.Not.EqualTo(previousId));
previousId = path.InvalidationID;
}
}
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
{
Assert.That(vertices, Is.Not.Empty);
for (int i = 0; i < vertices.Count; i++)
{
Assert.That(double.IsFinite(vertices[i].Distance));
Assert.That(float.IsFinite(vertices[i].X));
}
for (int i = 1; i < vertices.Count; i++)
{
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
if (!checkSlope) continue;
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
}
}
}
}
@@ -0,0 +1,120 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
[TestFixture]
public class CatchModMirrorTest
{
[Test]
public void TestModMirror()
{
IBeatmap original = createBeatmap(false);
IBeatmap mirrored = createBeatmap(true);
assertEffectivePositionsMirrored(original, mirrored);
}
private static IBeatmap createBeatmap(bool withMirrorMod)
{
var beatmap = createRawBeatmap();
var mirrorMod = new CatchModMirror();
var beatmapProcessor = new CatchBeatmapProcessor(beatmap);
beatmapProcessor.PreProcess();
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
beatmapProcessor.PostProcess();
if (withMirrorMod)
mirrorMod.ApplyToBeatmap(beatmap);
return beatmap;
}
private static IBeatmap createRawBeatmap() => new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
OriginalX = 150,
StartTime = 0
},
new Fruit
{
OriginalX = 450,
StartTime = 500
},
new JuiceStream
{
OriginalX = 250,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(-100, 1)),
new PathControlPoint(new Vector2(0, 2)),
new PathControlPoint(new Vector2(100, 3)),
new PathControlPoint(new Vector2(0, 4))
}
},
StartTime = 1000,
},
new BananaShower
{
StartTime = 5000,
Duration = 5000
}
}
};
private static void assertEffectivePositionsMirrored(IBeatmap original, IBeatmap mirrored)
{
if (original.HitObjects.Count != mirrored.HitObjects.Count)
Assert.Fail($"Top-level object count mismatch (original: {original.HitObjects.Count}, mirrored: {mirrored.HitObjects.Count})");
for (int i = 0; i < original.HitObjects.Count; ++i)
{
var originalObject = (CatchHitObject)original.HitObjects[i];
var mirroredObject = (CatchHitObject)mirrored.HitObjects[i];
// banana showers themselves are exempt, as we only really care about their nested bananas' positions.
if (!effectivePositionMirrored(originalObject, mirroredObject) && !(originalObject is BananaShower))
Assert.Fail($"{originalObject.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalObject, mirroredObject)})");
if (originalObject.NestedHitObjects.Count != mirroredObject.NestedHitObjects.Count)
Assert.Fail($"{originalObject.GetType().Name} nested object count mismatch (original: {originalObject.NestedHitObjects.Count}, mirrored: {mirroredObject.NestedHitObjects.Count})");
for (int j = 0; j < originalObject.NestedHitObjects.Count; ++j)
{
var originalNested = (CatchHitObject)originalObject.NestedHitObjects[j];
var mirroredNested = (CatchHitObject)mirroredObject.NestedHitObjects[j];
if (!effectivePositionMirrored(originalNested, mirroredNested))
Assert.Fail($"{originalObject.GetType().Name}'s nested {originalNested.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalNested, mirroredNested)})");
}
}
}
private static string printEffectivePositions(CatchHitObject original, CatchHitObject mirrored)
=> $"original X: {original.EffectiveX}, mirrored X is: {mirrored.EffectiveX}, mirrored X should be: {CatchPlayfield.WIDTH - original.EffectiveX}";
private static bool effectivePositionMirrored(CatchHitObject original, CatchHitObject mirrored)
=> Precision.AlmostEquals(original.EffectiveX, CatchPlayfield.WIDTH - mirrored.EffectiveX);
}
}
@@ -0,0 +1,2 @@
[General]
// no version specified means v1
@@ -4,10 +4,8 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchModHidden : ModTestScene
{
[BackgroundDependencyLoader]
private void load()
{
LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
}
[Test]
public void TestJuiceStream()
{
@@ -1,8 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -10,5 +18,22 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
{
if (withModifiedSkin)
{
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit());
CreateTest(null);
}
AddAssert("legacy HUD combo counter hidden", () =>
{
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
});
}
}
}
@@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using Direction = osu.Game.Rulesets.Catch.UI.Direction;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchSkinConfiguration : OsuTestScene
{
private Catcher catcher;
private readonly Container container;
public TestSceneCatchSkinConfiguration()
{
Add(container = new Container { RelativeSizeAxes = Axes.Both });
}
[TestCase(false)]
[TestCase(true)]
public void TestCatcherPlateFlipping(bool flip)
{
AddStep("setup catcher", () =>
{
var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin)
{
Child = catcher = new Catcher(new DroppedObjectContainer())
{
Anchor = Anchor.Centre
}
};
});
Fruit fruit = new Fruit();
AddStep("catch fruit", () => catchFruit(fruit, 20));
float position = 0;
AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
AddStep("face left", () => catcher.VisualDirection = Direction.Left);
if (flip)
AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
else
AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
AddStep("face right", () => catcher.VisualDirection = Direction.Right);
AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
}
private float getCaughtObjectPosition(Fruit fruit)
{
var caughtObject = catcher.ChildrenOfType<CaughtObject>().Single(c => c.HitObject == fruit);
return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
}
private void catchFruit(Fruit fruit, float x)
{
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableFruit = new DrawableFruit(fruit) { X = x };
var judgement = fruit.CreateJudgement();
catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
{
Type = judgement.MaxResult
});
}
private class TestSkin : DefaultSkin
{
public bool FlipCatcherPlate { get; set; }
public TestSkin()
: base(null)
{
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is CatchSkinConfiguration config)
{
if (config == CatchSkinConfiguration.FlipCatcherPlate)
return SkinUtils.As<TValue>(new Bindable<bool>(FlipCatcherPlate));
}
return base.GetConfig<TLookup, TValue>(lookup);
}
}
}
}
@@ -6,8 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
private Container<CaughtObject> droppedObjectContainer;
private DroppedObjectContainer droppedObjectContainer;
private TestCatcher catcher;
@@ -43,18 +43,15 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
var trailContainer = new Container();
droppedObjectContainer = new Container<CaughtObject>();
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
droppedObjectContainer = new DroppedObjectContainer();
Child = new Container
{
Anchor = Anchor.Centre,
Children = new Drawable[]
{
trailContainer,
droppedObjectContainer,
catcher
catcher = new TestCatcher(droppedObjectContainer, difficulty),
}
};
});
@@ -188,9 +185,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y < -25));
catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
catcher.CaughtObjects.Any(obj => obj.Y < 0));
}
[Test]
@@ -216,7 +213,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour);
catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
}
[Test]
@@ -293,8 +290,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(Container trailsTarget, Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, droppedObjectTarget, difficulty)
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
: base(droppedObjectTarget, difficulty)
{
}
}
@@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
@@ -41,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting));
AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
AddStep("catch many random fruit", () =>
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
{
Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
Type = area.Catcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
});
drawable.Expire();
@@ -95,20 +95,14 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = circleSize
};
SetContents(() =>
SetContents(_ =>
{
var droppedObjectContainer = new Container<CaughtObject>
{
RelativeSizeAxes = Axes.Both
};
return new CatchInputManager(catchRuleset)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
droppedObjectContainer,
new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
new TestCatcherArea(beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
@@ -126,12 +120,18 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
public TestCatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
: base(droppedObjectContainer, beatmapDifficulty)
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
{
var droppedObjectContainer = new DroppedObjectContainer();
Add(droppedObjectContainer);
Catcher = new Catcher(droppedObjectContainer, beatmapDifficulty)
{
X = CatchPlayfield.CENTER_X
};
}
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
public void ToggleHyperDash(bool status) => Catcher.SetHyperDashState(status ? 2 : 1);
}
}
}
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
scoreProcessor = new ScoreProcessor();
SetContents(() => new CatchComboDisplay
SetContents(_ => new CatchComboDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);
private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState;
private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).Catcher.CurrentState;
private void spawnFruits(bool hit = false)
{
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Catch.Tests
float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
catchPlayfield.Catcher.X = xCoords - x_offset;
if (hit)
xCoords -= x_offset;
@@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable)
{
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawable);
drawableRuleset.Playfield.Add(drawable);
}
@@ -19,22 +19,22 @@ namespace osu.Game.Rulesets.Catch.Tests
{
base.LoadComplete();
AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
AddStep("show pear", () => SetContents(_ => createDrawableFruit(0)));
AddStep("show grape", () => SetContents(_ => createDrawableFruit(1)));
AddStep("show pineapple / apple", () => SetContents(_ => createDrawableFruit(2)));
AddStep("show raspberry / orange", () => SetContents(_ => createDrawableFruit(3)));
AddStep("show banana", () => SetContents(createDrawableBanana));
AddStep("show banana", () => SetContents(_ => createDrawableBanana()));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
AddStep("show droplet", () => SetContents(_ => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(_ => createDrawableTinyDroplet()));
AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
AddStep("show hyperdash pear", () => SetContents(_ => createDrawableFruit(0, true)));
AddStep("show hyperdash grape", () => SetContents(_ => createDrawableFruit(1, true)));
AddStep("show hyperdash pineapple / apple", () => SetContents(_ => createDrawableFruit(2, true)));
AddStep("show hyperdash raspberry / orange", () => SetContents(_ => createDrawableFruit(3, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true)));
}
private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override void LoadComplete()
{
AddStep("fruit changes visual and hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
AddStep("fruit changes visual and hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
{
IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash },
}))));
AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
AddStep("droplet changes hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
{
HyperDashBindable = { BindTarget = hyperDash },
}))));
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
{
var catcher = Player.ChildrenOfType<CatcherArea>().FirstOrDefault()?.MovableCatcher;
var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault();
if (catcher == null)
return;
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
public void TestCustomEndGlowColour()
public void TestCustomAfterImageColour()
{
var skin = new TestSkin
{
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
public void TestCustomEndGlowColourPriority()
public void TestCustomAfterImageColourPriority()
{
var skin = new TestSkin
{
@@ -111,39 +111,45 @@ namespace osu.Game.Rulesets.Catch.Tests
checkHyperDashFruitColour(skin, skin.HyperDashColour);
}
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedAfterImageColour = null)
{
CatcherArea catcherArea = null;
CatcherTrailDisplay trails = null;
Catcher catcher = null;
AddStep("create hyper-dashing catcher", () =>
{
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container<CaughtObject>())
CatcherArea catcherArea;
Child = setupSkinHierarchy(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(4f),
Child = catcherArea = new CatcherArea
{
Catcher = catcher = new Catcher(new DroppedObjectContainer())
{
Scale = new Vector2(4)
}
}
}, skin);
trails = catcherArea.ChildrenOfType<CatcherTrailDisplay>().Single();
});
AddStep("get trails container", () =>
AddStep("start hyper-dash", () =>
{
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
catcherArea.MovableCatcher.SetHyperDashState(2);
catcher.SetHyperDashState(2);
});
AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour);
AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
AddAssert("catcher after-image colours are correct", () => trails.HyperDashAfterImageColour == (expectedAfterImageColour ?? expectedCatcherColour));
AddStep("finish hyper-dashing", () =>
{
catcherArea.MovableCatcher.SetHyperDashState(1);
catcherArea.MovableCatcher.FinishTransforms();
catcher.SetHyperDashState();
catcher.FinishTransforms();
});
AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
AddAssert("catcher colour returned to white", () => catcher.Colour == Color4.White);
}
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
@@ -32,28 +32,28 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
public void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
base.TestBeatmapComboColoursOverride(useBeatmapSkin);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, false, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
ConfigureTest(useBeatmapSkin, useBeatmapColour, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -72,10 +72,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
ConfigureTest(useBeatmapSkin, useBeatmapColour, true);
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
@@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)]
public void TestBeatmapHyperDashColours(bool useBeatmapSkin)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, true, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)]
public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin)
{
TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
@@ -2,9 +2,9 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
@@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
var positionData = obj as IHasXPosition;
var xPositionData = obj as IHasXPosition;
var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo;
switch (obj)
@@ -36,10 +37,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
X = positionData?.X ?? 0,
X = xPositionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
}.Yield();
case IHasDuration endTime:
@@ -59,7 +61,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
X = positionData?.X ?? 0
X = xPositionData?.X ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
}.Yield();
}
}
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public const int RNG_SEED = 1337;
public bool HardRockOffsets { get; set; }
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
case Fruit fruit:
if (shouldApplyHardRockOffset)
if (HardRockOffsets)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X;
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime;
+11 -5
View File
@@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Rulesets.Edit;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@@ -115,6 +117,7 @@ namespace osu.Game.Rulesets.Catch
{
new CatchModDifficultyAdjust(),
new CatchModClassic(),
new CatchModMirror(),
};
case ModType.Automation:
@@ -128,7 +131,8 @@ namespace osu.Game.Rulesets.Catch
return new Mod[]
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new CatchModFloatingFruits()
new CatchModFloatingFruits(),
new CatchModMuted(),
};
default:
@@ -161,13 +165,13 @@ namespace osu.Game.Rulesets.Catch
switch (result)
{
case HitResult.LargeTickHit:
return "large droplet";
return "Large droplet";
case HitResult.SmallTickHit:
return "small droplet";
return "Small droplet";
case HitResult.LargeBonus:
return "banana";
return "Banana";
}
return base.GetDisplayNameForHitResult(result);
@@ -175,12 +179,14 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
}
}
@@ -8,9 +8,8 @@ namespace osu.Game.Rulesets.Catch
Fruit,
Banana,
Droplet,
CatcherIdle,
CatcherFail,
CatcherKiai,
CatchComboCounter
Catcher,
CatchComboCounter,
HitExplosion
}
}
@@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
public double ApproachRate;
public double ApproachRate { get; set; }
}
}
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[]
{
new Movement(mods, halfCatcherWidth),
new Movement(mods, halfCatcherWidth, clockRate),
};
}
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -33,15 +32,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
misses = Score.Statistics.GetOrDefault(HitResult.Miss);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
misses = Score.Statistics.GetValueOrDefault(HitResult.Miss);
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
@@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
/// </summary>
public readonly double StrainTime;
public readonly double ClockRate;
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate)
{
@@ -37,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime);
ClockRate = clockRate;
}
}
}
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
public class Movement : StrainSkill
public class Movement : StrainDecaySkill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
@@ -28,10 +28,21 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved;
private double lastStrainTime;
public Movement(Mod[] mods, float halfCatcherWidth)
/// <summary>
/// The speed multiplier applied to the player's catcher.
/// </summary>
private readonly double catcherSpeedMultiplier;
public Movement(Mod[] mods, float halfCatcherWidth, double clockRate)
: base(mods)
{
HalfCatcherWidth = halfCatcherWidth;
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
// but also the speed of the player's catcher, which has an impact on difficulty
// TODO: Support variable clockrates caused by mods such as ModTimeRamp
// (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing)
catcherSpeedMultiplier = clockRate;
}
protected override double StrainValueOf(DifficultyHitObject current)
@@ -48,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime);
@@ -81,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;
@@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class BananaShowerCompositionTool : HitObjectCompositionTool
{
public BananaShowerCompositionTool()
: base(nameof(BananaShower))
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
}
}
@@ -0,0 +1,73 @@
// 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.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint<BananaShower>
{
private readonly TimeSpanOutline outline;
public BananaShowerPlacementBlueprint()
{
InternalChild = outline = new TimeSpanOutline();
}
protected override void Update()
{
base.Update();
outline.UpdateFrom(HitObjectContainer, HitObject);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
if (e.Button != MouseButton.Left) break;
BeginPlacement(true);
return true;
case PlacementState.Active:
if (e.Button != MouseButton.Right) break;
// If the duration is negative, swap the start and the end time to make the duration positive.
if (HitObject.Duration < 0)
{
HitObject.StartTime = HitObject.EndTime;
HitObject.Duration = -HitObject.Duration;
}
EndPlacement(HitObject.Duration > 0);
return true;
}
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
if (!(result.Time is double time)) return;
switch (PlacementActive)
{
case PlacementState.Waiting:
HitObject.StartTime = time;
break;
case PlacementState.Active:
HitObject.EndTime = time;
break;
}
}
}
}
@@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint<BananaShower>
{
public BananaShowerSelectionBlueprint(BananaShower hitObject)
: base(hitObject)
{
}
}
}
@@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
where THitObject : CatchHitObject, new()
{
protected new THitObject HitObject => (THitObject)base.HitObject;
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
private Playfield playfield { get; set; }
public CatchPlacementBlueprint()
: base(new THitObject())
{
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
}
}
@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject>
where THitObject : CatchHitObject
{
protected override bool AlwaysShowWhenSelected => true;
public override Vector2 ScreenSpaceSelectionPoint
{
get
{
Vector2 position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
return HitObjectContainer.ToScreenSpace(position + new Vector2(0, HitObjectContainer.DrawHeight));
}
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
private Playfield playfield { get; set; }
protected CatchSelectionBlueprint(THitObject hitObject)
: base(hitObject)
{
}
}
}
@@ -0,0 +1,190 @@
// 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 System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public abstract class EditablePath : CompositeDrawable
{
public int PathId => path.InvalidationID;
public IReadOnlyList<JuiceStreamPathVertex> Vertices => path.Vertices;
public int VertexCount => path.Vertices.Count;
protected readonly Func<float, double> PositionToDistance;
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
private readonly JuiceStreamPath path = new JuiceStreamPath();
// Invariant: `path.Vertices.Count == vertexStates.Count`
private readonly List<VertexState> vertexStates = new List<VertexState>
{
new VertexState { IsFixed = true }
};
private readonly List<VertexState> previousVertexStates = new List<VertexState>();
[Resolved(CanBeNull = true)]
[CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; }
protected EditablePath(Func<float, double> positionToDistance)
{
PositionToDistance = positionToDistance;
Anchor = Anchor.BottomLeft;
}
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
while (path.Vertices.Count < InternalChildren.Count)
RemoveInternal(InternalChildren[^1]);
while (InternalChildren.Count < path.Vertices.Count)
AddInternal(new VertexPiece());
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
for (int i = 0; i < VertexCount; i++)
{
var piece = (VertexPiece)InternalChildren[i];
var vertex = path.Vertices[i];
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
piece.UpdateFrom(vertexStates[i]);
}
}
public void InitializeFromHitObject(JuiceStream hitObject)
{
var sliderPath = hitObject.Path;
path.ConvertFromSliderPath(sliderPath);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
{
path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
}
vertexStates.Clear();
vertexStates.AddRange(path.Vertices.Select((_, i) => new VertexState
{
IsFixed = i == 0
}));
}
public void UpdateHitObjectFromPath(JuiceStream hitObject)
{
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
}
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
{
return ToLocalSpace(screenSpacePosition) - new Vector2(0, DrawHeight);
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
protected int AddVertex(double distance, float x)
{
int index = path.InsertVertex(distance);
path.SetVertexPosition(index, x);
vertexStates.Insert(index, new VertexState());
correctFixedVertexPositions();
Debug.Assert(vertexStates.Count == VertexCount);
return index;
}
protected bool RemoveVertex(int index)
{
if (index < 0 || index >= path.Vertices.Count)
return false;
if (vertexStates[index].IsFixed)
return false;
path.RemoveVertices((_, i) => i == index);
vertexStates.RemoveAt(index);
if (vertexStates.Count == 0)
vertexStates.Add(new VertexState());
Debug.Assert(vertexStates.Count == VertexCount);
return true;
}
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
{
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
previousVertexStates.Clear();
previousVertexStates.AddRange(vertexStates);
// We will recreate the path from scratch. Note that `Clear` leaves the first vertex.
int vertexCount = VertexCount;
path.Clear();
vertexStates.RemoveRange(1, vertexCount - 1);
for (int i = 1; i < vertexCount; i++)
{
var state = previousVertexStates[i];
double distance = state.VertexBeforeChange.Distance;
if (state.IsSelected)
distance += distanceDelta;
int newIndex = path.InsertVertex(Math.Max(0, distance));
vertexStates.Insert(newIndex, state);
}
// First, restore positions of the non-selected vertices.
for (int i = 0; i < vertexCount; i++)
{
if (!vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
}
// Then, move the selected vertices.
for (int i = 0; i < vertexCount; i++)
{
if (vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X + xDelta);
}
// Finally, correct the position of fixed vertices.
correctFixedVertexPositions();
}
private void correctFixedVertexPositions()
{
for (int i = 0; i < VertexCount; i++)
{
if (vertexStates[i].IsFixed)
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
}
}
}
}
@@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class FruitOutline : CompositeDrawable
{
public FruitOutline()
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre;
InternalChild = new BorderPiece();
}
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
Colour = osuColour.Yellow;
}
public void UpdateFrom(CatchHitObject hitObject)
{
Scale = new Vector2(hitObject.Scale);
}
}
}
@@ -0,0 +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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class NestedOutlineContainer : CompositeDrawable
{
private readonly List<CatchHitObject> nestedHitObjects = new List<CatchHitObject>();
public NestedOutlineContainer()
{
Anchor = Anchor.BottomLeft;
}
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
nestedHitObjects.Clear();
nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
.OfType<CatchHitObject>()
.Where(h => !(h is TinyDroplet)));
while (nestedHitObjects.Count < InternalChildren.Count)
RemoveInternal(InternalChildren[^1]);
while (InternalChildren.Count < nestedHitObjects.Count)
AddInternal(new FruitOutline());
for (int i = 0; i < nestedHitObjects.Count; i++)
{
var hitObject = nestedHitObjects[i];
var outline = (FruitOutline)InternalChildren[i];
outline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, hitObject) - Position;
outline.UpdateFrom(hitObject);
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}
@@ -0,0 +1,49 @@
// 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.Game.Rulesets.Catch.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class PlacementEditablePath : EditablePath
{
/// <summary>
/// The original position of the last added vertex.
/// This is not same as the last vertex of the current path because the vertex ordering can change.
/// </summary>
private JuiceStreamPathVertex lastVertex;
public PlacementEditablePath(Func<float, double> positionToDistance)
: base(positionToDistance)
{
}
public void AddNewVertex()
{
var endVertex = Vertices[^1];
int index = AddVertex(endVertex.Distance, endVertex.X);
for (int i = 0; i < VertexCount; i++)
{
VertexStates[i].IsSelected = i == index;
VertexStates[i].IsFixed = i != index;
VertexStates[i].VertexBeforeChange = Vertices[i];
}
lastVertex = Vertices[index];
}
/// <summary>
/// Move the vertex added by <see cref="AddNewVertex"/> in the last time.
/// </summary>
public void MoveLastVertex(Vector2 screenSpacePosition)
{
Vector2 position = ToRelativePosition(screenSpacePosition);
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
float xDelta = position.X - lastVertex.X;
MoveSelectedVertices(distanceDelta, xDelta);
}
}
}
@@ -0,0 +1,77 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class ScrollingPath : CompositeDrawable
{
private readonly Path drawablePath;
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
public ScrollingPath()
{
Anchor = Anchor.BottomLeft;
InternalChildren = new Drawable[]
{
drawablePath = new SmoothPath
{
PathRadius = 2,
Alpha = 0.5f
},
};
}
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
computeDistanceXs(hitObject);
drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
.ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
}
private void computeDistanceXs(JuiceStream hitObject)
{
vertices.Clear();
var sliderVertices = new List<Vector2>();
hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
if (sliderVertices.Count == 0)
return;
double distance = 0;
Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{
foreach (var position in sliderVertices)
{
distance += Vector2.Distance(lastPosition, position);
lastPosition = position;
vertices.Add((distance, position.X));
}
sliderVertices.Reverse();
}
}
// Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}
@@ -0,0 +1,130 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class SelectionEditablePath : EditablePath, IHasContextMenu
{
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
// To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IEditorChangeHandler changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToDistance)
: base(positionToDistance)
{
}
public void AddVertex(Vector2 relativePosition)
{
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
int index = AddVertex(distance, relativePosition.X);
selectOnly(index);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
protected override bool OnMouseDown(MouseDownEvent e)
{
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
if (index == -1 || VertexStates[index].IsFixed)
return false;
if (e.Button == MouseButton.Left && e.ShiftPressed)
{
RemoveVertex(index);
return true;
}
if (e.ControlPressed)
VertexStates[index].IsSelected = !VertexStates[index].IsSelected;
else if (!VertexStates[index].IsSelected)
selectOnly(index);
// Don't inhibit right click, to show the context menu
return e.Button != MouseButton.Right;
}
protected override bool OnDragStart(DragStartEvent e)
{
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
if (index == -1 || VertexStates[index].IsFixed)
return false;
if (e.Button != MouseButton.Left)
return false;
dragStartPosition = ToRelativePosition(e.ScreenSpaceMouseDownPosition);
for (int i = 0; i < VertexCount; i++)
VertexStates[i].VertexBeforeChange = Vertices[i];
changeHandler?.BeginChange();
return true;
}
protected override void OnDrag(DragEvent e)
{
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
float xDelta = mousePosition.X - dragStartPosition.X;
MoveSelectedVertices(distanceDelta, xDelta);
}
protected override void OnDragEnd(DragEndEvent e)
{
changeHandler?.EndChange();
}
private int getMouseTargetVertex(Vector2 screenSpacePosition)
{
for (int i = InternalChildren.Count - 1; i >= 0; i--)
{
if (i < VertexCount && InternalChildren[i].ReceivePositionalInputAt(screenSpacePosition))
return i;
}
return -1;
}
private IEnumerable<MenuItem> getContextMenuItems()
{
int selectedCount = VertexStates.Count(state => state.IsSelected);
if (selectedCount != 0)
yield return new OsuMenuItem($"Delete selected {(selectedCount == 1 ? "vertex" : $"{selectedCount} vertices")}", MenuItemType.Destructive, deleteSelectedVertices);
}
private void selectOnly(int index)
{
for (int i = 0; i < VertexCount; i++)
VertexStates[i].IsSelected = i == index;
}
private void deleteSelectedVertices()
{
for (int i = VertexCount - 1; i >= 0; i--)
{
if (VertexStates[i].IsSelected)
RemoveVertex(i);
}
}
}
}
@@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class TimeSpanOutline : CompositeDrawable
{
private const float border_width = 4;
private const float opacity_when_empty = 0.5f;
private bool isEmpty = true;
public TimeSpanOutline()
{
Anchor = Origin = Anchor.BottomLeft;
RelativeSizeAxes = Axes.X;
Masking = true;
BorderThickness = border_width;
Alpha = opacity_when_empty;
// A box is needed to make the border visible.
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Transparent
};
}
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
BorderColour = osuColour.Yellow;
}
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
{
float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
Y = Math.Max(startY, endY);
float height = Math.Abs(startY - endY);
bool wasEmpty = isEmpty;
isEmpty = height == 0;
if (wasEmpty != isEmpty)
this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
Height = Math.Max(height, border_width);
}
}
}
@@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class VertexPiece : Circle
{
[Resolved]
private OsuColour osuColour { get; set; }
public VertexPiece()
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre;
Size = new Vector2(15);
}
public void UpdateFrom(VertexState state)
{
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
Alpha = state.IsFixed ? 0.5f : 1;
}
}
}
@@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Objects;
#nullable enable
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
/// <summary>
/// Holds the state of a vertex in the path of a <see cref="EditablePath"/>.
/// </summary>
public class VertexState
{
/// <summary>
/// Whether the vertex is selected.
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// Whether the vertex can be moved or deleted.
/// </summary>
public bool IsFixed { get; set; }
/// <summary>
/// The position of the vertex before a vertex moving operation starts.
/// This is used to implement "memory-less" moving operations (only the final position matters) to improve UX.
/// </summary>
public JuiceStreamPathVertex VertexBeforeChange { get; set; }
}
}
@@ -0,0 +1,51 @@
// 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.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class FruitPlacementBlueprint : CatchPlacementBlueprint<Fruit>
{
private readonly FruitOutline outline;
public FruitPlacementBlueprint()
{
InternalChild = outline = new FruitOutline();
}
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update()
{
base.Update();
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
outline.UpdateFrom(HitObject);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
EndPlacement(true);
return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
}
}
}
@@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class FruitSelectionBlueprint : CatchSelectionBlueprint<Fruit>
{
private readonly FruitOutline outline;
public FruitSelectionBlueprint(Fruit hitObject)
: base(hitObject)
{
InternalChild = outline = new FruitOutline();
}
protected override void Update()
{
base.Update();
if (!IsSelected) return;
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
outline.UpdateFrom(HitObject);
}
}
}
@@ -0,0 +1,128 @@
// 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.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class JuiceStreamPlacementBlueprint : CatchPlacementBlueprint<JuiceStream>
{
private readonly ScrollingPath scrollingPath;
private readonly NestedOutlineContainer nestedOutlineContainer;
private readonly PlacementEditablePath editablePath;
private int lastEditablePathId = -1;
private InputManager inputManager;
public JuiceStreamPlacementBlueprint()
{
InternalChildren = new Drawable[]
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new PlacementEditablePath(positionToDistance)
};
}
protected override void Update()
{
base.Update();
if (PlacementActive == PlacementState.Active)
editablePath.UpdateFrom(HitObjectContainer, HitObject);
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
if (e.Button != MouseButton.Left) break;
editablePath.AddNewVertex();
BeginPlacement(true);
return true;
case PlacementState.Active:
switch (e.Button)
{
case MouseButton.Left:
editablePath.AddNewVertex();
return true;
case MouseButton.Right:
EndPlacement(HitObject.Duration > 0);
return true;
}
break;
}
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
if (!(result.Time is double snappedTime)) return;
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
HitObject.StartTime = snappedTime;
break;
case PlacementState.Active:
Vector2 unsnappedPosition = inputManager.CurrentState.Mouse.Position;
editablePath.MoveLastVertex(unsnappedPosition);
break;
default:
return;
}
// Make sure the up-to-date position is used for outlines.
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
updateHitObjectFromPath();
}
private void updateHitObjectFromPath()
{
if (lastEditablePathId == editablePath.PathId)
return;
editablePath.UpdateHitObjectFromPath(HitObject);
ApplyDefaultsToHitObject();
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
lastEditablePathId = editablePath.PathId;
}
private double positionToDistance(float relativeYPosition)
{
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity;
}
}
}
@@ -0,0 +1,187 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint<JuiceStream>
{
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
public override MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
private float minNestedX;
private float maxNestedX;
private readonly ScrollingPath scrollingPath;
private readonly NestedOutlineContainer nestedOutlineContainer;
private readonly Cached pathCache = new Cached();
private readonly SelectionEditablePath editablePath;
/// <summary>
/// The <see cref="JuiceStreamPath.InvalidationID"/> of the <see cref="JuiceStreamPath"/> corresponding the current <see cref="SliderPath"/> of the hit object.
/// When the path is edited, the change is detected and the <see cref="SliderPath"/> of the hit object is updated.
/// </summary>
private int lastEditablePathId = -1;
/// <summary>
/// The <see cref="SliderPath.Version"/> of the current <see cref="SliderPath"/> of the hit object.
/// When the <see cref="SliderPath"/> of the hit object is changed by external means, the change is detected and the <see cref="JuiceStreamPath"/> is re-initialized.
/// </summary>
private int lastSliderPathVersion = -1;
private Vector2 rightMouseDownPosition;
[Resolved(CanBeNull = true)]
[CanBeNull]
private EditorBeatmap editorBeatmap { get; set; }
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject)
{
InternalChildren = new Drawable[]
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToDistance)
};
}
[BackgroundDependencyLoader]
private void load()
{
HitObject.DefaultsApplied += onDefaultsApplied;
computeObjectBounds();
}
protected override void Update()
{
base.Update();
if (!IsSelected) return;
if (editablePath.PathId != lastEditablePathId)
updateHitObjectFromPath();
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
editablePath.UpdateFrom(HitObjectContainer, HitObject);
if (pathCache.IsValid) return;
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
pathCache.Validate();
}
protected override void OnSelected()
{
initializeJuiceStreamPath();
base.OnSelected();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!IsSelected) return base.OnMouseDown(e);
switch (e.Button)
{
case MouseButton.Left when e.ControlPressed:
editablePath.AddVertex(editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition));
return true;
case MouseButton.Right:
// Record the mouse position to be used in the "add vertex" action.
rightMouseDownPosition = editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition);
break;
}
return base.OnMouseDown(e);
}
private void onDefaultsApplied(HitObject _)
{
computeObjectBounds();
pathCache.Invalidate();
if (lastSliderPathVersion != HitObject.Path.Version.Value)
initializeJuiceStreamPath();
}
private void computeObjectBounds()
{
minNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Min(nested => nested.OriginalX) - HitObject.OriginalX;
maxNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Max(nested => nested.OriginalX) - HitObject.OriginalX;
}
private RectangleF getBoundingBox()
{
float left = HitObject.OriginalX + minNestedX;
float right = HitObject.OriginalX + maxNestedX;
float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
}
private double positionToDistance(float relativeYPosition)
{
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity;
}
private void initializeJuiceStreamPath()
{
editablePath.InitializeFromHitObject(HitObject);
// Record the current ID to update the hit object only when a change is made to the path.
lastEditablePathId = editablePath.PathId;
lastSliderPathVersion = HitObject.Path.Version.Value;
}
private void updateHitObjectFromPath()
{
editablePath.UpdateHitObjectFromPath(HitObject);
editorBeatmap?.Update(HitObject);
lastEditablePathId = editablePath.PathId;
lastSliderPathVersion = HitObject.Path.Version.Value;
}
private IEnumerable<MenuItem> getContextMenuItems()
{
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
{
editablePath.AddVertex(rightMouseDownPosition);
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
HitObject.DefaultsApplied -= onDefaultsApplied;
}
}
}
@@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchBlueprintContainer : ComposeBlueprintContainer
{
public CatchBlueprintContainer(CatchHitObjectComposer composer)
: base(composer)
{
}
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
{
case Fruit fruit:
return new FruitSelectionBlueprint(fruit);
case JuiceStream juiceStream:
return new JuiceStreamSelectionBlueprint(juiceStream);
case BananaShower bananaShower:
return new BananaShowerSelectionBlueprint(bananaShower);
}
return base.CreateHitObjectBlueprintFor(hitObject);
}
}
}
@@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchEditorPlayfield : CatchPlayfield
{
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
: base(difficulty)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
// TODO: honor "hit animation" setting?
Catcher.CatchFruitOnPlate = false;
// TODO: disable hit lighting as well
}
}
}
@@ -0,0 +1,55 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
{
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
}
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{
new FruitCompositionTool(),
new JuiceStreamCompositionTool(),
new BananaShowerCompositionTool()
};
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
// TODO: implement position snap
result.ScreenSpacePosition.X = screenSpacePosition.X;
return result;
}
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
}
}
@@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
/// <summary>
/// Utility functions used by the editor.
/// </summary>
public static class CatchHitObjectUtils
{
/// <summary>
/// Get the position of the hit object in the playfield based on <see cref="CatchHitObject.OriginalX"/> and <see cref="HitObject.StartTime"/>.
/// </summary>
public static Vector2 GetStartPosition(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
{
return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime));
}
/// <summary>
/// Get the range of horizontal position occupied by the hit object.
/// </summary>
/// <remarks>
/// <see cref="TinyDroplet"/>s are excluded and returns <see cref="PositionRange.EMPTY"/>.
/// </remarks>
public static PositionRange GetPositionRange(HitObject hitObject)
{
switch (hitObject)
{
case Fruit fruit:
return new PositionRange(fruit.OriginalX);
case Droplet droplet:
return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX);
case JuiceStream _:
return GetPositionRange(hitObject.NestedHitObjects);
case BananaShower _:
// A banana shower occupies the whole screen width.
return new PositionRange(0, CatchPlayfield.WIDTH);
default:
return PositionRange.EMPTY;
}
}
/// <summary>
/// Get the range of horizontal position occupied by the hit objects.
/// </summary>
/// <remarks>
/// <see cref="TinyDroplet"/>s are excluded.
/// </remarks>
public static PositionRange GetPositionRange(IEnumerable<HitObject> hitObjects) => hitObjects.Select(GetPositionRange).Aggregate(PositionRange.EMPTY, PositionRange.Union);
}
}

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