Compare commits
4883 Commits
2025.304.0
...
pp-dev
@@ -3,28 +3,25 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"jetbrains.resharper.globaltools": {
|
"jetbrains.resharper.globaltools": {
|
||||||
"version": "2023.3.3",
|
"version": "2025.2.3",
|
||||||
"commands": [
|
"commands": [
|
||||||
"jb"
|
"jb"
|
||||||
]
|
],
|
||||||
},
|
"rollForward": false
|
||||||
"nvika": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"commands": [
|
|
||||||
"nvika"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"codefilesanity": {
|
"codefilesanity": {
|
||||||
"version": "0.0.37",
|
"version": "0.0.41",
|
||||||
"commands": [
|
"commands": [
|
||||||
"CodeFileSanity"
|
"CodeFileSanity"
|
||||||
]
|
],
|
||||||
|
"rollForward": false
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2024.802.0",
|
"version": "2025.1208.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
],
|
||||||
|
"rollForward": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ indent_style = space
|
|||||||
indent_size = 4
|
indent_size = 4
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
|
||||||
|
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
|
||||||
|
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
|
||||||
|
dotnet_diagnostic.CS1591.severity = none
|
||||||
|
|
||||||
#license header
|
#license header
|
||||||
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
generator:
|
generator:
|
||||||
name: Run
|
name: Run
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
timeout-minutes: 720
|
timeout-minutes: 1440
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
target: ${{ steps.run.outputs.target }}
|
target: ${{ steps.run.outputs.target }}
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout diffcalc-sheet-generator
|
- name: Checkout diffcalc-sheet-generator
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.id }}
|
path: ${{ inputs.id }}
|
||||||
repository: 'smoogipoo/diffcalc-sheet-generator'
|
repository: 'smoogipoo/diffcalc-sheet-generator'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ concurrency:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
security-events: write # for reporting InspectCode issues
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
inspect-code:
|
inspect-code:
|
||||||
@@ -13,10 +14,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 8.0.x
|
- name: Install .NET 8.0.x
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ jobs:
|
|||||||
run: dotnet restore osu.Desktop.slnf
|
run: dotnet restore osu.Desktop.slnf
|
||||||
|
|
||||||
- name: Restore inspectcode cache
|
- name: Restore inspectcode cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/inspectcode
|
path: ${{ github.workspace }}/inspectcode
|
||||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
|
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
|
||||||
@@ -49,10 +50,14 @@ jobs:
|
|||||||
exit $exit_code
|
exit $exit_code
|
||||||
|
|
||||||
- name: InspectCode
|
- name: InspectCode
|
||||||
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
|
uses: JetBrains/ReSharper-InspectCode@v0.12
|
||||||
|
with:
|
||||||
- name: NVika
|
# this is WTF tier but if you don't specify *both* of these the defaults assume `build: true`
|
||||||
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
|
build: false
|
||||||
|
no-build: true
|
||||||
|
solution: ./osu.Desktop.slnf
|
||||||
|
caches-home: inspectcode
|
||||||
|
verbosity: WARN
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
@@ -71,10 +76,10 @@ jobs:
|
|||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 8.0.x
|
- name: Install .NET 8.0.x
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
@@ -82,34 +87,72 @@ jobs:
|
|||||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
|
run: >
|
||||||
shell: pwsh
|
dotnet test
|
||||||
|
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
|
||||||
|
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
|
||||||
|
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
|
||||||
|
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
|
||||||
|
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
|
||||||
|
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
|
||||||
|
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
|
||||||
|
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
|
||||||
|
--
|
||||||
|
NUnit.ConsoleOut=0
|
||||||
|
|
||||||
# Attempt to upload results even if test fails.
|
# Attempt to upload results even if test fails.
|
||||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
# https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#cancelled
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
if: ${{ always() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||||
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
|
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
|
||||||
|
|
||||||
|
test-results:
|
||||||
|
name: Test results
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# we want to wait for the `test` job to complete, but run regardless of whether it succeeds or fails
|
||||||
|
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-not-requiring-successful-dependent-jobs
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
needs: test
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Download results
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: osu-test-results-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Add test results summary to workflow run
|
||||||
|
uses: dorny/test-reporter@v3.0.0
|
||||||
|
with:
|
||||||
|
name: Results
|
||||||
|
path: "*.trx"
|
||||||
|
reporter: dotnet-trx
|
||||||
|
list-suites: 'failed'
|
||||||
|
list-tests: 'failed'
|
||||||
|
use-actions-summary: 'true'
|
||||||
|
|
||||||
build-only-android:
|
build-only-android:
|
||||||
name: Build only (Android)
|
name: Build only (Android)
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup JDK 11
|
- name: Setup JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: microsoft
|
distribution: microsoft
|
||||||
java-version: 11
|
java-version: 11
|
||||||
|
|
||||||
- name: Install .NET 8.0.x
|
- name: Install .NET 8.0.x
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
@@ -121,19 +164,24 @@ jobs:
|
|||||||
|
|
||||||
build-only-ios:
|
build-only-ios:
|
||||||
name: Build only (iOS)
|
name: Build only (iOS)
|
||||||
runs-on: macos-latest
|
runs-on: macos-15
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 8.0.x
|
- name: Install .NET 8.0.x
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
- name: Install .NET Workloads
|
- name: Install .NET Workloads
|
||||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
run: dotnet workload install ios
|
||||||
|
|
||||||
|
# https://github.com/dotnet/macios/issues/19157
|
||||||
|
# https://github.com/actions/runner-images/issues/12758
|
||||||
|
- name: Use Xcode 16.4
|
||||||
|
run: sudo xcode-select -switch /Applications/Xcode_16.4.app
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build -c Debug osu.iOS
|
run: dotnet build -c Debug osu.iOS.slnf
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: Pack and nuget
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
- '!*-*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify_pending_production_deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Submit pending deployment notification
|
||||||
|
run: |
|
||||||
|
export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME"
|
||||||
|
export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID"
|
||||||
|
export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME:
|
||||||
|
[View Workflow Run]($URL)"
|
||||||
|
export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
|
||||||
|
|
||||||
|
BODY="$(jq --null-input '{
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"title": env.TITLE,
|
||||||
|
"color": 15098112,
|
||||||
|
"description": env.DESCRIPTION,
|
||||||
|
"url": env.URL,
|
||||||
|
"author": {
|
||||||
|
"name": env.GITHUB_ACTOR,
|
||||||
|
"icon_url": env.ACTOR_ICON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}')"
|
||||||
|
|
||||||
|
curl \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$BODY" \
|
||||||
|
"${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}"
|
||||||
|
|
||||||
|
pack:
|
||||||
|
name: Pack
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set artifacts directory
|
||||||
|
id: artifactsPath
|
||||||
|
run: echo "::set-output name=nuget_artifacts::${{github.workspace}}/artifacts"
|
||||||
|
|
||||||
|
- name: Install .NET 8.0.x
|
||||||
|
uses: actions/setup-dotnet@v5
|
||||||
|
with:
|
||||||
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
|
- name: Pack
|
||||||
|
run: |
|
||||||
|
# Replace project references in templates with package reference, because they're included as source files.
|
||||||
|
dotnet remove Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game/osu.Game.csproj
|
||||||
|
dotnet remove Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj
|
||||||
|
dotnet remove Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game/osu.Game.csproj
|
||||||
|
dotnet remove Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj
|
||||||
|
|
||||||
|
dotnet add Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||||
|
dotnet add Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||||
|
dotnet add Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||||
|
dotnet add Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||||
|
|
||||||
|
# Pack
|
||||||
|
dotnet pack -c Release osu.Game /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||||
|
dotnet pack -c Release osu.Game.Rulesets.Osu /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||||
|
dotnet pack -c Release osu.Game.Rulesets.Taiko /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||||
|
dotnet pack -c Release osu.Game.Rulesets.Catch /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||||
|
dotnet pack -c Release osu.Game.Rulesets.Mania /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||||
|
dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: osu
|
||||||
|
path: |
|
||||||
|
${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg
|
||||||
|
${{steps.artifactsPath.outputs.nuget_artifacts}}/*.snupkg
|
||||||
|
|
||||||
|
- name: Publish packages to nuget.org
|
||||||
|
run: dotnet nuget push ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
checks: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
annotate:
|
|
||||||
name: Annotate CI run with test results
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: ${{ github.event.workflow_run.repository.full_name }}
|
|
||||||
ref: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
|
|
||||||
- name: Download results
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: osu-test-results-*
|
|
||||||
merge-multiple: true
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Annotate CI run with test results
|
|
||||||
uses: dorny/test-reporter@v1.8.0
|
|
||||||
with:
|
|
||||||
name: Results
|
|
||||||
path: "*.trx"
|
|
||||||
reporter: dotnet-trx
|
|
||||||
list-suites: 'failed'
|
|
||||||
list-tests: 'failed'
|
|
||||||
@@ -13,12 +13,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Create Sentry release
|
- name: Create Sentry release
|
||||||
uses: getsentry/action-release@v1
|
uses: getsentry/action-release@v3
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ppy
|
SENTRY_ORG: ppy
|
||||||
|
|||||||
@@ -38,8 +38,12 @@ jobs:
|
|||||||
run: ./UseLocalOsu.sh
|
run: ./UseLocalOsu.sh
|
||||||
working-directory: ./osu-tools
|
working-directory: ./osu-tools
|
||||||
|
|
||||||
|
- name: Build tools
|
||||||
|
run: dotnet build PerformanceCalculator --nologo --verbosity quiet
|
||||||
|
working-directory: ./osu-tools
|
||||||
|
|
||||||
- name: Regenerate mod definitions
|
- name: Regenerate mod definitions
|
||||||
run: dotnet run --project PerformanceCalculator -- mods > ../osu-web/database/mods.json
|
run: dotnet run --project PerformanceCalculator --no-build -- mods > ../osu-web/database/mods.json
|
||||||
working-directory: ./osu-tools
|
working-directory: ./osu-tools
|
||||||
|
|
||||||
- name: Create pull request with changes
|
- name: Create pull request with changes
|
||||||
|
|||||||
@@ -13,6 +13,19 @@
|
|||||||
"preLaunchTask": "Build osu! (Debug)",
|
"preLaunchTask": "Build osu! (Debug)",
|
||||||
"console": "internalConsole"
|
"console": "internalConsole"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "osu! (Debug, Second Client)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
|
||||||
|
"--debug-client-id=1"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"preLaunchTask": "Build osu! (Debug)",
|
||||||
|
"console": "internalConsole"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "osu! (Release)",
|
"name": "osu! (Release)",
|
||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
|
Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
|
||||||
|
|
||||||
|
## Foreword on AI usage
|
||||||
|
|
||||||
|
Our team believes in **human contributions**. Any contribution – be it an issue report or a pull request – which is created by, documented by, or aided by AI/LLM usage will typically be **closed and locked without further discussion**.
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
1. [Reporting bugs](#reporting-bugs)
|
1. [Reporting bugs](#reporting-bugs)
|
||||||
@@ -55,9 +59,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
|||||||
|
|
||||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||||
|
|
||||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||||
|
|
||||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
|
||||||
|
|
||||||
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
|
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
|
||||||
|
|
||||||
@@ -73,6 +75,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
|||||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||||
|
|
||||||
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
||||||
|
- Please pick the following target branch for your pull request:
|
||||||
|
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
|
||||||
|
- `master`, otherwise.
|
||||||
- Please avoid pushing untested or incomplete code.
|
- Please avoid pushing untested or incomplete code.
|
||||||
- Please do not force-push or rebase unless we ask you to.
|
- Please do not force-push or rebase unless we ask you to.
|
||||||
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberiz
|
|||||||
M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
|
M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
|
||||||
M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||||
M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||||
|
M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
|
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
|
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
|
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
<PropertyGroup Label="C#">
|
<PropertyGroup Label="C#">
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>12.0</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<!-- Stabilises hot reload, see: https://platform.uno/docs/articles/studio/Hot%20Reload/hot-reload-overview.html?tabs=vswin%2Cwindows%2Cskia-desktop%2Ccommon-issues -->
|
||||||
|
<GenerateAssemblyInfo Condition="'$(Configuration)'=='Debug'">false</GenerateAssemblyInfo>
|
||||||
|
<!-- Required due to the above -->
|
||||||
|
<NoWarn Condition="'$(Configuration)'=='Debug'">$(NoWarn);CA1416</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
|
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
|
||||||
@@ -46,7 +50,7 @@
|
|||||||
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
|
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
|
||||||
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
|
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
|
||||||
<Company>ppy Pty Ltd</Company>
|
<Company>ppy Pty Ltd</Company>
|
||||||
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
|
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
|
||||||
<PackageTags>osu game</PackageTags>
|
<PackageTags>osu game</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
|
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ When it comes to contributing to the project, the two main things you can do to
|
|||||||
|
|
||||||
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
|
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
|
||||||
|
|
||||||
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
|
Our team believes in **human contributions**. Any contribution – be it an issue report or a pull request – which is created by, documented by, or aided by AI/LLM usage will typically be **closed and locked without further discussion**.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
|
|||||||
if (button.HasValue)
|
if (button.HasValue)
|
||||||
Actions.Add(button.Value);
|
Actions.Add(button.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool IsEquivalentTo(ReplayFrame other)
|
||||||
|
=> other is EmptyFreeformReplayFrame freeformFrame && Time == freeformFrame.Time && Position == freeformFrame.Position && Actions.SequenceEqual(freeformFrame.Actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
|
|||||||
public class PippidonReplayFrame : ReplayFrame
|
public class PippidonReplayFrame : ReplayFrame
|
||||||
{
|
{
|
||||||
public Vector2 Position;
|
public Vector2 Position;
|
||||||
|
|
||||||
|
public override bool IsEquivalentTo(ReplayFrame other)
|
||||||
|
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Position == pippidonFrame.Position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.EmptyScrolling.Replays
|
namespace osu.Game.Rulesets.EmptyScrolling.Replays
|
||||||
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
|
|||||||
if (button.HasValue)
|
if (button.HasValue)
|
||||||
Actions.Add(button.Value);
|
Actions.Add(button.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool IsEquivalentTo(ReplayFrame other)
|
||||||
|
=> other is EmptyScrollingReplayFrame scrollingFrame && Time == scrollingFrame.Time && Actions.SequenceEqual(scrollingFrame.Actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Pippidon.Replays
|
namespace osu.Game.Rulesets.Pippidon.Replays
|
||||||
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
|
|||||||
if (button.HasValue)
|
if (button.HasValue)
|
||||||
Actions.Add(button.Value);
|
Actions.Add(button.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool IsEquivalentTo(ReplayFrame other)
|
||||||
|
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Actions.SequenceEqual(pippidonFrame.Actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
|
||||||
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
|
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
|
||||||
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
|
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
|
||||||
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
|
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
|
||||||
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
|
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
|
||||||
<PackageTags>dotnet-new;templates;osu</PackageTags>
|
<PackageTags>dotnet-new;templates;osu</PackageTags>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
clone_depth: 1
|
|
||||||
version: '{branch}-{build}'
|
|
||||||
image: Visual Studio 2022
|
|
||||||
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}'
|
|
||||||
|
|
||||||
before_build:
|
|
||||||
- cmd: dotnet --info # Useful when version mismatch between CI and local
|
|
||||||
- cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects
|
|
||||||
- cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects
|
|
||||||
- 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: .\InspectCode.ps1
|
|
||||||
|
|
||||||
test:
|
|
||||||
assemblies:
|
|
||||||
except:
|
|
||||||
- '**\*Android*'
|
|
||||||
- '**\*iOS*'
|
|
||||||
- 'build\**\*'
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
clone_depth: 1
|
|
||||||
version: '{build}'
|
|
||||||
image: Visual Studio 2022
|
|
||||||
test: off
|
|
||||||
skip_non_tags: true
|
|
||||||
configuration: Release
|
|
||||||
|
|
||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- job_name: osu-game
|
|
||||||
- job_name: osu-ruleset
|
|
||||||
job_depends_on: osu-game
|
|
||||||
- job_name: taiko-ruleset
|
|
||||||
job_depends_on: osu-game
|
|
||||||
- job_name: catch-ruleset
|
|
||||||
job_depends_on: osu-game
|
|
||||||
- job_name: mania-ruleset
|
|
||||||
job_depends_on: osu-game
|
|
||||||
- job_name: templates
|
|
||||||
job_depends_on: osu-game
|
|
||||||
|
|
||||||
nuget:
|
|
||||||
project_feed: true
|
|
||||||
|
|
||||||
for:
|
|
||||||
-
|
|
||||||
matrix:
|
|
||||||
only:
|
|
||||||
- job_name: osu-game
|
|
||||||
build_script:
|
|
||||||
- cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
|
||||||
-
|
|
||||||
matrix:
|
|
||||||
only:
|
|
||||||
- job_name: osu-ruleset
|
|
||||||
build_script:
|
|
||||||
- cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
|
||||||
-
|
|
||||||
matrix:
|
|
||||||
only:
|
|
||||||
- job_name: taiko-ruleset
|
|
||||||
build_script:
|
|
||||||
- cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
|
||||||
-
|
|
||||||
matrix:
|
|
||||||
only:
|
|
||||||
- job_name: catch-ruleset
|
|
||||||
build_script:
|
|
||||||
- cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
|
||||||
-
|
|
||||||
matrix:
|
|
||||||
only:
|
|
||||||
- job_name: mania-ruleset
|
|
||||||
build_script:
|
|
||||||
- cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
|
||||||
-
|
|
||||||
matrix:
|
|
||||||
only:
|
|
||||||
- job_name: templates
|
|
||||||
build_script:
|
|
||||||
- cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
|
|
||||||
|
|
||||||
- cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
|
||||||
|
|
||||||
- cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- path: '**\*.nupkg'
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
- provider: Environment
|
|
||||||
name: nuget
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.225.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.513.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
|||||||
@@ -5,16 +5,13 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:label="osu!"
|
android:label="osu!"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher" />
|
android:roundIcon="@mipmap/ic_launcher">
|
||||||
<!-- for editor usage -->
|
<provider android:name="androidx.core.content.FileProvider"
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
android:authorities="sh.ppy.osulazer.fileprovider"
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
android:grantUriPermissions="true"
|
||||||
<!--
|
android:exported="false">
|
||||||
READ_MEDIA_* permissions are available only on API 33 or greater. Devices with older android versions
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
don't understand the new permissions, so request the old READ_EXTERNAL_STORAGE permission to get storage access.
|
android:resource="@xml/filepaths" />
|
||||||
Since the old permission has no effect on >= API 33, don't request it.
|
</provider>
|
||||||
|
</application>
|
||||||
Care needs to be taken to ensure runtime permission checks target the correct permission for the API level.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -20,13 +20,24 @@ using Uri = Android.Net.Uri;
|
|||||||
namespace osu.Android
|
namespace osu.Android
|
||||||
{
|
{
|
||||||
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
|
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*",
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
DataMimeType = "*/*")]
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import skin", DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*",
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
DataMimeType = "*/*")]
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import replay", DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*",
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
DataMimeType = "*/*")]
|
||||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||||
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import skin", DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||||
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import replay", DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||||
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import file", DataScheme = "content", DataMimeTypes = new[]
|
||||||
|
{
|
||||||
|
"application/zip",
|
||||||
|
"application/octet-stream",
|
||||||
|
"application/download",
|
||||||
|
"application/x-zip",
|
||||||
|
"application/x-zip-compressed",
|
||||||
|
})]
|
||||||
|
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, Label = "Import", DataMimeTypes = new[]
|
||||||
{
|
{
|
||||||
"application/zip",
|
"application/zip",
|
||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Microsoft.Maui.Devices;
|
using Microsoft.Maui.Devices;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
@@ -21,58 +23,30 @@ namespace osu.Android
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OsuGameActivity gameActivity;
|
private readonly OsuGameActivity gameActivity;
|
||||||
|
|
||||||
|
private readonly PackageInfo packageInfo;
|
||||||
|
|
||||||
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||||
|
|
||||||
public OsuGameAndroid(OsuGameActivity activity)
|
public OsuGameAndroid(OsuGameActivity activity)
|
||||||
: base(null)
|
: base(null)
|
||||||
{
|
{
|
||||||
gameActivity = activity;
|
gameActivity = activity;
|
||||||
|
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Version AssemblyVersion
|
public override string Version
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
|
if (!IsDeployedBuild)
|
||||||
|
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
|
||||||
|
|
||||||
try
|
return packageInfo.VersionName.AsNonNull();
|
||||||
{
|
|
||||||
// We store the osu! build number in the "VersionCode" field to better support google play releases.
|
|
||||||
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
|
|
||||||
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
|
|
||||||
//
|
|
||||||
// We also need to be aware that older SDK versions store this as a 32bit int.
|
|
||||||
//
|
|
||||||
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
|
|
||||||
string versionName;
|
|
||||||
|
|
||||||
if (OperatingSystem.IsAndroidVersionAtLeast(28))
|
|
||||||
{
|
|
||||||
versionName = packageInfo.LongVersionCode.ToString();
|
|
||||||
// ensure we only read the trailing portion of long (the part we are interested in).
|
|
||||||
versionName = versionName.Substring(versionName.Length - 9);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
// this is required else older SDKs will report missing method exception.
|
|
||||||
versionName = packageInfo.VersionCode.ToString();
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
}
|
|
||||||
|
|
||||||
// undo play store version garbling (as mentioned above).
|
|
||||||
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Version(packageInfo.VersionName.AsNonNull());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<!-- https://developer.android.com/reference/androidx/core/content/FileProvider -->
|
||||||
|
<external-files-path path="logs" name="logs" />
|
||||||
|
<external-files-path path="exports" name="exports" />
|
||||||
|
</paths>
|
||||||
@@ -58,6 +58,7 @@ namespace osu.Desktop
|
|||||||
private readonly RichPresence presence = new RichPresence
|
private readonly RichPresence presence = new RichPresence
|
||||||
{
|
{
|
||||||
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
|
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
|
||||||
|
Timestamps = Timestamps.Now,
|
||||||
Secrets = new Secrets
|
Secrets = new Secrets
|
||||||
{
|
{
|
||||||
JoinSecret = null,
|
JoinSecret = null,
|
||||||
@@ -189,7 +190,7 @@ namespace osu.Desktop
|
|||||||
}
|
}
|
||||||
|
|
||||||
// user party
|
// user party
|
||||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
|
if (!hideIdentifiableInformation && multiplayerClient.Room != null && !multiplayerClient.Room.Settings.MatchType.IsMatchmakingType())
|
||||||
{
|
{
|
||||||
MultiplayerRoom room = multiplayerClient.Room;
|
MultiplayerRoom room = multiplayerClient.Room;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Desktop.IPC.Messages
|
||||||
|
{
|
||||||
|
public class HitCountMessage : OsuWebSocketMessage
|
||||||
|
{
|
||||||
|
[JsonProperty("new_hits")]
|
||||||
|
public long NewHits { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
|
|
||||||
|
namespace osu.Desktop.IPC.Messages
|
||||||
|
{
|
||||||
|
public abstract class OsuWebSocketMessage
|
||||||
|
{
|
||||||
|
[JsonProperty("type")]
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
protected OsuWebSocketMessage()
|
||||||
|
{
|
||||||
|
Type = GetType().ReadableName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using osu.Desktop.IPC.Messages;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.IPC;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using JsonConvert = Newtonsoft.Json.JsonConvert;
|
||||||
|
|
||||||
|
namespace osu.Desktop.IPC
|
||||||
|
{
|
||||||
|
public partial class OsuWebSocketProvider : Component
|
||||||
|
{
|
||||||
|
private WebSocketServer? server;
|
||||||
|
private readonly Bindable<ScoreInfo> lastLocalScore = new Bindable<ScoreInfo>();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(SessionStatics sessionStatics)
|
||||||
|
{
|
||||||
|
server = new WebSocketServer(49727);
|
||||||
|
server.StartAsync().FireAndForget(onError: ex => Logger.Error(ex, "Failed to start websocket"));
|
||||||
|
|
||||||
|
sessionStatics.BindWith(Static.LastLocalUserScore, lastLocalScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
lastLocalScore.BindValueChanged(val =>
|
||||||
|
{
|
||||||
|
if (val.NewValue == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (server?.IsRunning != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = new HitCountMessage { NewHits = val.NewValue.Statistics.Where(kv => kv.Key.IsBasic() && kv.Key.IsHit()).Sum(kv => kv.Value) };
|
||||||
|
broadcast(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcast(OsuWebSocketMessage message)
|
||||||
|
{
|
||||||
|
if (server?.IsRunning != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string messageString = JsonConvert.SerializeObject(message);
|
||||||
|
server.BroadcastAsync(messageString).FireAndForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (server?.IsRunning == true)
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
cts.CancelAfter(TimeSpan.FromSeconds(10));
|
||||||
|
server.StopAsync(cts.Token).WaitSafely();
|
||||||
|
server = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
using System.IO;
|
||||||
|
using osu.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Desktop.MacOS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the game is located at `Applications` folder and displays a warning notification if not so.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MacOSAppLocationChecker : Component
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private INotificationOverlay notification { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
string assemblyPath = RuntimeInfo.EntryAssembly.Location;
|
||||||
|
|
||||||
|
bool inRootApp = assemblyPath.StartsWith("/Applications/", StringComparison.Ordinal);
|
||||||
|
bool inUserApp = assemblyPath.StartsWith(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications/"), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (!inRootApp && !inUserApp)
|
||||||
|
notification.Post(new MacOSAppLocationNotification());
|
||||||
|
|
||||||
|
Expire();
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class MacOSAppLocationNotification : SimpleNotification
|
||||||
|
{
|
||||||
|
public MacOSAppLocationNotification()
|
||||||
|
{
|
||||||
|
Text = NotificationsStrings.MacOSAppLocation(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.ShieldAlt;
|
||||||
|
IconContent.Colour = colours.YellowDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
using osu.Desktop.IPC;
|
||||||
using osu.Desktop.Performance;
|
using osu.Desktop.Performance;
|
||||||
using osu.Desktop.Security;
|
using osu.Desktop.Security;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@@ -15,11 +15,12 @@ using osu.Desktop.Updater;
|
|||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
|
using osu.Desktop.MacOS;
|
||||||
using osu.Desktop.Windows;
|
using osu.Desktop.Windows;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.IPC;
|
using osu.Game.IPC;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Performance;
|
using osu.Game.Performance;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
|
||||||
@@ -33,6 +34,10 @@ namespace osu.Desktop
|
|||||||
[Cached(typeof(IHighPerformanceSessionManager))]
|
[Cached(typeof(IHighPerformanceSessionManager))]
|
||||||
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
|
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
|
||||||
|
|
||||||
|
public bool IsFirstRun { get; init; }
|
||||||
|
|
||||||
|
public bool EnableWebSocketServer { get; init; }
|
||||||
|
|
||||||
public OsuGameDesktop(string[]? args = null)
|
public OsuGameDesktop(string[]? args = null)
|
||||||
: base(args)
|
: base(args)
|
||||||
{
|
{
|
||||||
@@ -104,6 +109,14 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
protected override UpdateManager CreateUpdateManager()
|
protected override UpdateManager CreateUpdateManager()
|
||||||
{
|
{
|
||||||
|
// If this is the first time we've run the game, ie it is being installed,
|
||||||
|
// reset the user's release stream to "lazer".
|
||||||
|
//
|
||||||
|
// This ensures that if a user is trying to recover from a failed startup on an unstable release stream,
|
||||||
|
// the game doesn't immediately try and update them back to the release stream after starting up.
|
||||||
|
if (IsFirstRun)
|
||||||
|
LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
||||||
|
|
||||||
if (IsPackageManaged)
|
if (IsPackageManaged)
|
||||||
return new NoActionUpdateManager();
|
return new NoActionUpdateManager();
|
||||||
|
|
||||||
@@ -112,7 +125,7 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
public override bool RestartAppWhenExited()
|
public override bool RestartAppWhenExited()
|
||||||
{
|
{
|
||||||
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
RestartOnExitAction = () => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,22 +135,38 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
LoadComponentAsync(new DiscordRichPresence(), Add);
|
LoadComponentAsync(new DiscordRichPresence(), Add);
|
||||||
|
|
||||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
switch (RuntimeInfo.OS)
|
||||||
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
|
{
|
||||||
|
case RuntimeInfo.Platform.Windows:
|
||||||
|
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RuntimeInfo.Platform.macOS when !IsPackageManaged && IsDeployedBuild:
|
||||||
|
if (!IsPackageManaged && IsDeployedBuild)
|
||||||
|
LoadComponentAsync(new MacOSAppLocationChecker(), Add);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
|
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
|
||||||
|
|
||||||
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
|
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
|
||||||
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
|
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
|
||||||
|
|
||||||
|
if (EnableWebSocketServer)
|
||||||
|
Add(new OsuWebSocketProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetHost(GameHost host)
|
public override void SetHost(GameHost host)
|
||||||
{
|
{
|
||||||
base.SetHost(host);
|
base.SetHost(host);
|
||||||
|
|
||||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
// Apple operating systems use a better icon provided via external assets.
|
||||||
if (iconStream != null)
|
if (!RuntimeInfo.IsApple)
|
||||||
host.Window.SetIconFromStream(iconStream);
|
{
|
||||||
|
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||||
|
if (iconStream != null)
|
||||||
|
host.Window.SetIconFromStream(iconStream);
|
||||||
|
}
|
||||||
|
|
||||||
host.Window.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
private static LegacyTcpIpcProvider? legacyIpc;
|
private static LegacyTcpIpcProvider? legacyIpc;
|
||||||
|
|
||||||
|
private static bool isFirstRun;
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
||||||
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
|
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
|
||||||
// last time has been fixed, let's not tempt fate.
|
// last time has been fixed, let's not tempt fate.
|
||||||
setupVelopack();
|
setupVelopack(args);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
@@ -135,7 +137,13 @@ namespace osu.Desktop
|
|||||||
if (tournamentClient)
|
if (tournamentClient)
|
||||||
host.Run(new TournamentGame());
|
host.Run(new TournamentGame());
|
||||||
else
|
else
|
||||||
host.Run(new OsuGameDesktop(args));
|
{
|
||||||
|
host.Run(new OsuGameDesktop(args)
|
||||||
|
{
|
||||||
|
IsFirstRun = isFirstRun,
|
||||||
|
EnableWebSocketServer = Environment.GetEnvironmentVariable("OSU_WEBSOCKET_SERVER") == "1",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +175,21 @@ namespace osu.Desktop
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setupVelopack()
|
private static void setupVelopack(string[] args)
|
||||||
{
|
{
|
||||||
|
// Arguments being present indicate the user is either starting the game in a special (aka tournament) mode,
|
||||||
|
// or is running with pending imports via file association or otherwise.
|
||||||
|
//
|
||||||
|
// In both these scenarios, we'd hope the game does not attempt to update.
|
||||||
|
//
|
||||||
|
// Special consideration for velopack startup arguments, which must be handled during update.
|
||||||
|
// See https://docs.velopack.io/integrating/hooks#command-line-hooks.
|
||||||
|
if (args.Length > 0 && !args[0].StartsWith("--velo", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Logger.Log("Handling arguments, skipping velopack setup.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (OsuGameDesktop.IsPackageManaged)
|
if (OsuGameDesktop.IsPackageManaged)
|
||||||
{
|
{
|
||||||
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
||||||
@@ -177,6 +198,8 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
var app = VelopackApp.Build();
|
var app = VelopackApp.Build();
|
||||||
|
|
||||||
|
app.OnFirstRun(_ => isFirstRun = true);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
configureWindows(app);
|
configureWindows(app);
|
||||||
|
|
||||||
@@ -186,9 +209,9 @@ namespace osu.Desktop
|
|||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
private static void configureWindows(VelopackApp app)
|
private static void configureWindows(VelopackApp app)
|
||||||
{
|
{
|
||||||
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||||
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||||
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
@@ -26,13 +27,15 @@ namespace osu.Desktop.Security
|
|||||||
|
|
||||||
if (Environment.IsPrivilegedProcess)
|
if (Environment.IsPrivilegedProcess)
|
||||||
notifications.Post(new ElevatedPrivilegesNotification());
|
notifications.Post(new ElevatedPrivilegesNotification());
|
||||||
|
|
||||||
|
Expire();
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
||||||
{
|
{
|
||||||
public ElevatedPrivilegesNotification()
|
public ElevatedPrivilegesNotification()
|
||||||
{
|
{
|
||||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator");
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
|||||||
@@ -2,22 +2,25 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Threading;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using Velopack;
|
using Velopack;
|
||||||
using Velopack.Sources;
|
using Velopack.Sources;
|
||||||
|
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||||
|
|
||||||
namespace osu.Desktop.Updater
|
namespace osu.Desktop.Updater
|
||||||
{
|
{
|
||||||
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
|
public partial class VelopackUpdateManager : UpdateManager
|
||||||
{
|
{
|
||||||
private readonly UpdateManager updateManager;
|
[Resolved]
|
||||||
private INotificationOverlay notificationOverlay = null!;
|
private INotificationOverlay notificationOverlay { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuGameBase game { get; set; } = null!;
|
private OsuGameBase game { get; set; } = null!;
|
||||||
@@ -27,122 +30,128 @@ namespace osu.Desktop.Updater
|
|||||||
|
|
||||||
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
|
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
|
||||||
|
|
||||||
private UpdateInfo? pendingUpdate;
|
private ScheduledDelegate? scheduledBackgroundCheck;
|
||||||
|
|
||||||
public VelopackUpdateManager()
|
private void scheduleNextUpdateCheck()
|
||||||
{
|
{
|
||||||
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
|
scheduledBackgroundCheck?.Cancel();
|
||||||
|
scheduledBackgroundCheck = Scheduler.AddDelayed(() =>
|
||||||
{
|
{
|
||||||
AllowVersionDowngrade = true,
|
log("Running scheduled background update check...");
|
||||||
});
|
CheckForUpdate();
|
||||||
|
}, 60000 * 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
|
||||||
private void load(INotificationOverlay notifications)
|
|
||||||
{
|
{
|
||||||
notificationOverlay = notifications;
|
scheduledBackgroundCheck?.Cancel();
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
if (isInGameplay)
|
||||||
|
{
|
||||||
private async Task<bool> checkForUpdateAsync()
|
log("Update check cancelled - user is in gameplay");
|
||||||
{
|
scheduleNextUpdateCheck();
|
||||||
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
|
return false;
|
||||||
bool scheduleRecheck = false;
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Avoid any kind of update checking while gameplay is running.
|
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||||
if (isInGameplay)
|
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
||||||
{
|
{
|
||||||
scheduleRecheck = true;
|
AllowVersionDowngrade = true
|
||||||
|
});
|
||||||
|
|
||||||
|
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
log("Update check cancelled");
|
||||||
|
scheduleNextUpdateCheck();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
if (update == null)
|
||||||
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
|
|
||||||
if (pendingUpdate != null)
|
|
||||||
{
|
{
|
||||||
// If there is an update pending restart, show the notification to restart again.
|
// No update is available.
|
||||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
log("No update found");
|
||||||
{
|
scheduleNextUpdateCheck();
|
||||||
Activated = () =>
|
|
||||||
{
|
|
||||||
Task.Run(restartToApplyUpdate);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
// No update is available. We'll check again later.
|
|
||||||
if (pendingUpdate == null)
|
|
||||||
{
|
|
||||||
scheduleRecheck = true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// An update is found, let's notify the user and start downloading it.
|
// Download update in the background while notifying awaiters of the update being available.
|
||||||
UpdateProgressNotification notification = new UpdateProgressNotification
|
log($"New update available: {update.TargetFullRelease.Version}");
|
||||||
{
|
downloadUpdate(updateManager, update, cancellationToken);
|
||||||
CompletionClickAction = () =>
|
return true;
|
||||||
{
|
|
||||||
Task.Run(restartToApplyUpdate);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
runOutsideOfGameplay(() => notificationOverlay.Post(notification));
|
|
||||||
notification.StartDownload();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
|
||||||
runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
// In the case of an error, a separate notification will be displayed.
|
|
||||||
scheduleRecheck = true;
|
|
||||||
notification.FailDownload();
|
|
||||||
Logger.Error(e, @"update failed!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
|
log($"Update check failed with error ({e.Message})");
|
||||||
scheduleRecheck = true;
|
|
||||||
Logger.Log($@"update check failed ({e.Message})");
|
// we shouldn't crash on a web failure. or any failure for the matter.
|
||||||
|
scheduleNextUpdateCheck();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
|
|
||||||
|
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>
|
||||||
|
{
|
||||||
|
log($"Beginning download of update {update.TargetFullRelease.Version}...");
|
||||||
|
|
||||||
|
UpdateDownloadProgressNotification progressNotification = new UpdateDownloadProgressNotification(cancellationToken)
|
||||||
{
|
{
|
||||||
if (scheduleRecheck)
|
CompletionClickAction = () =>
|
||||||
{
|
{
|
||||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
restartToApplyUpdate(updateManager, update);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(progressNotification.CancellationToken, cancellationToken))
|
||||||
|
{
|
||||||
|
progressNotification.StartDownload();
|
||||||
|
runOutsideOfGameplay(() => notificationOverlay.Post(progressNotification), cts.Token);
|
||||||
|
|
||||||
|
await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, cts.Token).ConfigureAwait(false);
|
||||||
|
runOutsideOfGameplay(() => progressNotification.State = ProgressNotificationState.Completed, cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
progressNotification.FailDownload();
|
||||||
|
log(@"Update cancelled");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// In the case of an error, a separate notification will be displayed.
|
||||||
|
progressNotification.FailDownload();
|
||||||
|
Logger.Error(e, @"Update failed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}, cancellationToken);
|
||||||
|
|
||||||
private void runOutsideOfGameplay(Action action)
|
private void runOutsideOfGameplay(Action action, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
if (isInGameplay)
|
if (isInGameplay)
|
||||||
{
|
{
|
||||||
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
|
Scheduler.AddDelayed(() => runOutsideOfGameplay(action, cancellationToken), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task restartToApplyUpdate()
|
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update)
|
||||||
{
|
{
|
||||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
game.RestartOnExitAction = () => updateManager.WaitExitThenApplyUpdates(update.TargetFullRelease);
|
||||||
Schedule(() => game.AttemptExit());
|
game.AttemptExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -24,9 +24,10 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
|
<PackageReference Include="System.IO.Packaging" Version="10.0.5" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<!-- Held back due to invite bug in newer versions. See https://github.com/Lachee/discord-rpc-csharp/issues/286-->
|
||||||
<PackageReference Include="Velopack" Version="0.0.1053" />
|
<PackageReference Include="DiscordRichPresence" Version="1.5.0.51" />
|
||||||
|
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||||
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
|
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
|
||||||
<releaseNotes>testing</releaseNotes>
|
<releaseNotes>testing</releaseNotes>
|
||||||
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
|
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
|
||||||
<language>en-AU</language>
|
<language>en-AU</language>
|
||||||
</metadata>
|
</metadata>
|
||||||
<files>
|
<files>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using BenchmarkDotNet.Attributes;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Carousel;
|
using osu.Game.Tests.NonVisual.Filtering;
|
||||||
|
|
||||||
namespace osu.Game.Benchmarks
|
namespace osu.Game.Benchmarks
|
||||||
{
|
{
|
||||||
@@ -42,7 +42,7 @@ namespace osu.Game.Benchmarks
|
|||||||
Status = BeatmapOnlineStatus.Loved
|
Status = BeatmapOnlineStatus.Loved
|
||||||
};
|
};
|
||||||
|
|
||||||
private CarouselBeatmap carouselBeatmap = null!;
|
private FilterMatchingTest.CarouselBeatmap carouselBeatmap = null!;
|
||||||
private FilterCriteria criteria1 = null!;
|
private FilterCriteria criteria1 = null!;
|
||||||
private FilterCriteria criteria2 = null!;
|
private FilterCriteria criteria2 = null!;
|
||||||
private FilterCriteria criteria3 = null!;
|
private FilterCriteria criteria3 = null!;
|
||||||
@@ -55,7 +55,7 @@ namespace osu.Game.Benchmarks
|
|||||||
var beatmap = getExampleBeatmap();
|
var beatmap = getExampleBeatmap();
|
||||||
beatmap.OnlineID = 20201010;
|
beatmap.OnlineID = 20201010;
|
||||||
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
|
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
|
||||||
carouselBeatmap = new CarouselBeatmap(beatmap);
|
carouselBeatmap = new FilterMatchingTest.CarouselBeatmap(beatmap);
|
||||||
criteria1 = new FilterCriteria();
|
criteria1 = new FilterCriteria();
|
||||||
criteria2 = new FilterCriteria
|
criteria2 = new FilterCriteria
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.IO;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Rulesets.Catch;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Mania;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Taiko;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Benchmarks
|
||||||
|
{
|
||||||
|
public class BenchmarkDifficultyCalculation : BenchmarkTest
|
||||||
|
{
|
||||||
|
private DifficultyCalculator osuCalculator = null!;
|
||||||
|
private DifficultyCalculator taikoCalculator = null!;
|
||||||
|
private DifficultyCalculator catchCalculator = null!;
|
||||||
|
private DifficultyCalculator maniaCalculator = null!;
|
||||||
|
|
||||||
|
public override void SetUp()
|
||||||
|
{
|
||||||
|
using var resources = new DllResourceStore(typeof(TestResources).Assembly);
|
||||||
|
|
||||||
|
using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz");
|
||||||
|
using var archiveReader = new ZipArchiveReader(archive);
|
||||||
|
|
||||||
|
var osuBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Gamu) [Insane].osu");
|
||||||
|
var taikoBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (MMzz) [Oni].osu");
|
||||||
|
var catchBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Deif) [Salad].osu");
|
||||||
|
var maniaBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (ExPew) [Another].osu");
|
||||||
|
|
||||||
|
osuCalculator = new OsuRuleset().CreateDifficultyCalculator(osuBeatmap);
|
||||||
|
taikoCalculator = new TaikoRuleset().CreateDifficultyCalculator(taikoBeatmap);
|
||||||
|
catchCalculator = new CatchRuleset().CreateDifficultyCalculator(catchBeatmap);
|
||||||
|
maniaCalculator = new ManiaRuleset().CreateDifficultyCalculator(maniaBeatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatmapName)
|
||||||
|
{
|
||||||
|
using var beatmapStream = new MemoryStream();
|
||||||
|
archiveReader.GetStream(beatmapName).CopyTo(beatmapStream);
|
||||||
|
|
||||||
|
beatmapStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
using var reader = new LineBufferedReader(beatmapStream);
|
||||||
|
|
||||||
|
var decoder = Beatmaps.Formats.Decoder.GetDecoder<Beatmap>(reader);
|
||||||
|
return new FlatWorkingBeatmap(decoder.Decode(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyOsu() => osuCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyTaiko() => taikoCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyCatch() => catchCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyMania() => maniaCalculator.Calculate();
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void CalculateDifficultyOsuHundredTimes()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
osuCalculator.Calculate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="4.5.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -35,11 +35,9 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch.Tests";
|
||||||
|
|
||||||
[TestCase(4.0505463516206195d, 127, "diffcalc-test")]
|
[TestCase(4.039861734717169d, 127, "diffcalc-test")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(5.1696411260785498d, 127, "diffcalc-test")]
|
[TestCase(5.1527173897800873d, 127, "diffcalc-test")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
{
|
{
|
||||||
@@ -21,8 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
var ruleset = new CatchRuleset();
|
var ruleset = new CatchRuleset();
|
||||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||||
|
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||||
|
|
||||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
|
||||||
|
|
||||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||||
}
|
}
|
||||||
@@ -32,8 +34,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
var ruleset = new CatchRuleset();
|
var ruleset = new CatchRuleset();
|
||||||
var difficulty = new BeatmapDifficulty();
|
var difficulty = new BeatmapDifficulty();
|
||||||
|
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||||
|
|
||||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]);
|
||||||
|
|
||||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||||
}
|
}
|
||||||
@@ -43,8 +46,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
var ruleset = new CatchRuleset();
|
var ruleset = new CatchRuleset();
|
||||||
var difficulty = new BeatmapDifficulty();
|
var difficulty = new BeatmapDifficulty();
|
||||||
|
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||||
|
|
||||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModDoubleTime()]);
|
||||||
|
|
||||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Game.Rulesets.Catch.Skinning;
|
using osu.Game.Rulesets.Catch.Skinning;
|
||||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||||
@@ -21,9 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
var skinSource = new SkinProvidingContainer(rawSkin);
|
var skinSource = new SkinProvidingContainer(rawSkin);
|
||||||
var skin = new CatchLegacySkinTransformer(skinSource);
|
var skin = new CatchLegacySkinTransformer(skinSource);
|
||||||
|
|
||||||
Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
|
ClassicAssert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
|
||||||
Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
|
ClassicAssert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
|
||||||
Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
|
ClassicAssert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestLegacySkin : LegacySkin
|
private class TestLegacySkin : LegacySkin
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||||
{
|
{
|
||||||
|
protected sealed override Ruleset CreateRuleset() => new CatchRuleset();
|
||||||
|
|
||||||
protected const double TIME_SNAP = 100;
|
protected const double TIME_SNAP = 100;
|
||||||
|
|
||||||
protected DrawableCatchHitObject LastObject;
|
protected DrawableCatchHitObject LastObject;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneCatchModMovingFast : ModTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMovingFast() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new CatchModMovingFast(),
|
||||||
|
PassCondition = () => true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,2 +0,0 @@
|
|||||||
[General]
|
|
||||||
// no version specified means v1
|
|
||||||
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTinyDropletMissPreservesCatcherState()
|
public void TestTinyDropletMissChangesCatcherState()
|
||||||
{
|
{
|
||||||
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
||||||
{
|
{
|
||||||
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}));
|
}));
|
||||||
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
||||||
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
||||||
// catcher state and hyper dash state is preserved
|
// catcher state is changed but hyper dash state is preserved
|
||||||
checkState(CatcherAnimationState.Kiai);
|
checkState(CatcherAnimationState.Fail);
|
||||||
checkHyperDash(true);
|
checkHyperDash(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,20 +193,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
public Color4 HyperDashColour
|
public Color4 HyperDashColour
|
||||||
{
|
{
|
||||||
get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
|
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)];
|
||||||
set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
|
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color4 HyperDashAfterImageColour
|
public Color4 HyperDashAfterImageColour
|
||||||
{
|
{
|
||||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
|
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)];
|
||||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
|
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color4 HyperDashFruitColour
|
public Color4 HyperDashFruitColour
|
||||||
{
|
{
|
||||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
|
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)];
|
||||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
|
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSkin()
|
public TestSkin()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Fruit { StartTime = 0, },
|
||||||
|
new Fruit { StartTime = 5000, },
|
||||||
|
new Fruit { StartTime = 10000, },
|
||||||
|
new Fruit { StartTime = 15000, }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||||
|
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecording()
|
||||||
|
{
|
||||||
|
seekTo(0);
|
||||||
|
AddStep("start moving left", () => InputManager.PressKey(Key.Left));
|
||||||
|
seekTo(5000);
|
||||||
|
AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left));
|
||||||
|
AddAssert("catcher max left", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(0));
|
||||||
|
AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft])));
|
||||||
|
AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, 0)));
|
||||||
|
|
||||||
|
AddStep("start dashing right", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.LShift);
|
||||||
|
InputManager.PressKey(Key.Right);
|
||||||
|
});
|
||||||
|
seekTo(10000);
|
||||||
|
AddStep("end dashing right", () =>
|
||||||
|
{
|
||||||
|
InputManager.ReleaseKey(Key.LShift);
|
||||||
|
InputManager.ReleaseKey(Key.Right);
|
||||||
|
});
|
||||||
|
AddAssert("catcher max right", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH));
|
||||||
|
AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight])));
|
||||||
|
AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seekTo(double time)
|
||||||
|
{
|
||||||
|
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||||
|
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
@@ -16,26 +18,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
int fruits = HitObjects.Count(s => s is Fruit);
|
int fruits = HitObjects.Count(s => s is Fruit);
|
||||||
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
|
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
|
||||||
int bananaShowers = HitObjects.Count(s => s is BananaShower);
|
int bananaShowers = HitObjects.Count(s => s is BananaShower);
|
||||||
|
int sum = Math.Max(1, fruits + juiceStreams);
|
||||||
|
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
new BeatmapStatistic
|
new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = @"Fruit Count",
|
Name = BeatmapStatisticStrings.Fruits,
|
||||||
Content = fruits.ToString(),
|
Content = fruits.ToString(),
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||||
|
BarDisplayLength = fruits / (float)sum,
|
||||||
},
|
},
|
||||||
new BeatmapStatistic
|
new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = @"Juice Stream Count",
|
Name = BeatmapStatisticStrings.JuiceStreams,
|
||||||
Content = juiceStreams.ToString(),
|
Content = juiceStreams.ToString(),
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||||
|
BarDisplayLength = juiceStreams / (float)sum,
|
||||||
},
|
},
|
||||||
new BeatmapStatistic
|
new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = @"Banana Shower Count",
|
Name = BeatmapStatisticStrings.BananaShowers,
|
||||||
Content = bananaShowers.ToString(),
|
Content = bananaShowers.ToString(),
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
||||||
|
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
||||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@@ -11,6 +12,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Difficulty;
|
using osu.Game.Rulesets.Catch.Difficulty;
|
||||||
using osu.Game.Rulesets.Catch.Edit;
|
using osu.Game.Rulesets.Catch.Edit;
|
||||||
@@ -25,6 +27,7 @@ using osu.Game.Rulesets.Catch.UI;
|
|||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Scoring.Legacy;
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
@@ -33,6 +36,7 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch
|
namespace osu.Game.Rulesets.Catch
|
||||||
@@ -150,6 +154,8 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
new CatchModFloatingFruits(),
|
new CatchModFloatingFruits(),
|
||||||
new CatchModMuted(),
|
new CatchModMuted(),
|
||||||
new CatchModNoScope(),
|
new CatchModNoScope(),
|
||||||
|
new CatchModMovingFast(),
|
||||||
|
new CatchModSynesthesia(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.System:
|
case ModType.System:
|
||||||
@@ -171,15 +177,20 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
||||||
|
|
||||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
public override IEnumerable<HitResult> GetValidHitResults()
|
||||||
{
|
{
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
HitResult.Great,
|
HitResult.Great,
|
||||||
|
HitResult.Miss,
|
||||||
|
|
||||||
HitResult.LargeTickHit,
|
HitResult.LargeTickHit,
|
||||||
|
HitResult.LargeTickMiss,
|
||||||
HitResult.SmallTickHit,
|
HitResult.SmallTickHit,
|
||||||
|
HitResult.SmallTickMiss,
|
||||||
HitResult.LargeBonus,
|
HitResult.LargeBonus,
|
||||||
|
HitResult.IgnoreHit,
|
||||||
|
HitResult.IgnoreMiss,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,9 +276,10 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
||||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||||
{
|
{
|
||||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||||
|
double rate = ModUtils.CalculateRateWithMods(mods);
|
||||||
|
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||||
preempt /= rate;
|
preempt /= rate;
|
||||||
@@ -276,6 +288,33 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
return adjustedDifficulty;
|
return adjustedDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||||
|
{
|
||||||
|
var originalDifficulty = beatmapInfo.Difficulty;
|
||||||
|
var effectiveDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||||
|
|
||||||
|
yield return new RulesetBeatmapAttribute(SongSelectStrings.CircleSize, @"CS", originalDifficulty.CircleSize, effectiveDifficulty.CircleSize, 10)
|
||||||
|
{
|
||||||
|
Description = "Affects the size of fruits.",
|
||||||
|
AdditionalMetrics =
|
||||||
|
[
|
||||||
|
new RulesetBeatmapAttribute.AdditionalMetric("Hit circle radius", (CatchHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(effectiveDifficulty.CircleSize)).ToLocalisableString("0.#"))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
yield return new RulesetBeatmapAttribute(SongSelectStrings.ApproachRate, @"AR", originalDifficulty.ApproachRate, effectiveDifficulty.ApproachRate, 10)
|
||||||
|
{
|
||||||
|
Description = "Affects how early fruits fade in on the screen.",
|
||||||
|
AdditionalMetrics =
|
||||||
|
[
|
||||||
|
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRangeInt(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
|
||||||
|
{
|
||||||
|
Description = "Affects the harshness of health drain and the health penalties for missing."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public override bool EditorShowScrollSpeed => false;
|
public override bool EditorShowScrollSpeed => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty;
|
|||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
@@ -22,16 +23,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 4.59;
|
private const double difficulty_multiplier = 4.59;
|
||||||
|
|
||||||
private float halfCatcherWidth;
|
public override int Version => 20251020;
|
||||||
|
|
||||||
public override int Version => 20220701;
|
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new CatchDifficultyAttributes { Mods = mods };
|
return new CatchDifficultyAttributes { Mods = mods };
|
||||||
@@ -46,12 +45,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
||||||
{
|
{
|
||||||
CatchHitObject? lastObject = null;
|
CatchHitObject? lastObject = null;
|
||||||
|
|
||||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
|
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
||||||
|
|
||||||
|
float halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||||
|
|
||||||
|
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
||||||
|
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||||
|
|
||||||
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
||||||
foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
|
foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
|
||||||
{
|
{
|
||||||
@@ -68,16 +74,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
||||||
{
|
{
|
||||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
|
||||||
|
|
||||||
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
|
||||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
|
||||||
|
|
||||||
return new Skill[]
|
return new Skill[]
|
||||||
{
|
{
|
||||||
new Movement(mods, halfCatcherWidth, clockRate),
|
new Movement(mods),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Audio.Track;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
@@ -51,15 +51,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if (catchAttributes.MaxCombo > 0)
|
if (catchAttributes.MaxCombo > 0)
|
||||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
value *= Math.Min(Math.Pow(score.MaxCombo, 0.35) / Math.Pow(catchAttributes.MaxCombo, 0.35), 1.0);
|
||||||
|
|
||||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||||
|
|
||||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||||
|
|
||||||
var track = new TrackVirtual(10000);
|
double clockRate = ModUtils.CalculateRateWithMods(score.Mods);
|
||||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
|
||||||
double clockRate = track.Rate;
|
|
||||||
|
|
||||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// 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.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class MovementEvaluator
|
||||||
|
{
|
||||||
|
private const double direction_change_bonus = 21.0;
|
||||||
|
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||||
|
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||||
|
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
double catcherSpeedMultiplier = current.ClockRate;
|
||||||
|
|
||||||
|
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||||
|
|
||||||
|
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||||
|
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||||
|
|
||||||
|
double edgeDashBonus = 0;
|
||||||
|
|
||||||
|
// Direction change bonus.
|
||||||
|
if (Math.Abs(catchCurrent.DistanceMoved) > 0.1)
|
||||||
|
{
|
||||||
|
if (current.Index >= 1 && Math.Abs(catchLast.DistanceMoved) > 0.1 && Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchLast.DistanceMoved))
|
||||||
|
{
|
||||||
|
double bonusFactor = Math.Min(50, Math.Abs(catchCurrent.DistanceMoved)) / 50;
|
||||||
|
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(catchLast.DistanceMoved)) / 70, 0.38);
|
||||||
|
|
||||||
|
distanceAddition += direction_change_bonus / Math.Sqrt(catchLast.StrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base bonus for every movement, giving some weight to streams.
|
||||||
|
distanceAddition += 12.5 * Math.Min(Math.Abs(catchCurrent.DistanceMoved), CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2)
|
||||||
|
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear spacing nerf.
|
||||||
|
double linearSpacingCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < Math.Min(current.Index, 10); i++)
|
||||||
|
{
|
||||||
|
var catchPrevObj = (CatchDifficultyHitObject)catchCurrent.Previous(i);
|
||||||
|
|
||||||
|
// Only same direction movements matter as they do not take any additional inputs.
|
||||||
|
if (Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchPrevObj.DistanceMoved) || catchCurrent.DistanceMoved == 0 || catchPrevObj.DistanceMoved == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
double currentSpacing = Math.Abs(catchCurrent.DistanceMoved / catchCurrent.StrainTime);
|
||||||
|
double prevSpacing = Math.Abs(catchPrevObj.DistanceMoved / catchPrevObj.StrainTime);
|
||||||
|
|
||||||
|
double relativeDifference = Math.Abs(currentSpacing / prevSpacing - 1);
|
||||||
|
|
||||||
|
if (relativeDifference > 0.05)
|
||||||
|
break;
|
||||||
|
|
||||||
|
linearSpacingCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
distanceAddition *= Math.Pow(0.7, linearSpacingCount);
|
||||||
|
|
||||||
|
// Bonus for edge dashes.
|
||||||
|
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||||
|
{
|
||||||
|
if (!catchCurrent.LastObject.HyperDash)
|
||||||
|
edgeDashBonus += 5.7;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||||
|
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH offsets
|
||||||
|
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||||
|
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH)
|
||||||
|
if (current.Index >= 2 && Math.Abs(catchCurrent.ExactDistanceMoved) <= CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2
|
||||||
|
&& catchCurrent.ExactDistanceMoved == -catchLast.ExactDistanceMoved && catchLast.ExactDistanceMoved == -catchLastLast.ExactDistanceMoved
|
||||||
|
&& catchCurrent.StrainTime == catchLast.StrainTime && catchLast.StrainTime == catchLastLast.StrainTime)
|
||||||
|
distanceAddition = 0;
|
||||||
|
|
||||||
|
return distanceAddition / weightedStrainTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,15 +11,49 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
public class CatchDifficultyHitObject : DifficultyHitObject
|
public class CatchDifficultyHitObject : DifficultyHitObject
|
||||||
{
|
{
|
||||||
private const float normalized_hitobject_radius = 41.0f;
|
public const float NORMALIZED_HALF_CATCHER_WIDTH = 41.0f;
|
||||||
|
private const float absolute_player_positioning_error = 16.0f;
|
||||||
|
|
||||||
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
||||||
|
|
||||||
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of <see cref="BaseObject"/>.
|
||||||
|
/// </summary>
|
||||||
public readonly float NormalizedPosition;
|
public readonly float NormalizedPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of <see cref="LastObject"/>.
|
||||||
|
/// </summary>
|
||||||
public readonly float LastNormalizedPosition;
|
public readonly float LastNormalizedPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of the player required to catch <see cref="BaseObject"/>, assuming the player moves as little as possible.
|
||||||
|
/// </summary>
|
||||||
|
public float PlayerPosition { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized position of the player after catching <see cref="LastObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public float LastPlayerPosition { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized distance between <see cref="LastPlayerPosition"/> and <see cref="PlayerPosition"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||||
|
/// </remarks>
|
||||||
|
public float DistanceMoved { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized distance the player has to move from <see cref="LastPlayerPosition"/> in order to catch <see cref="BaseObject"/> at its <see cref="NormalizedPosition"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||||
|
/// </remarks>
|
||||||
|
public float ExactDistanceMoved { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,13 +63,35 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
|||||||
: base(hitObject, lastObject, clockRate, objects, index)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
float scalingFactor = NORMALIZED_HALF_CATCHER_WIDTH / halfCatcherWidth;
|
||||||
|
|
||||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||||
|
|
||||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||||
StrainTime = Math.Max(40, DeltaTime);
|
StrainTime = Math.Max(40, DeltaTime);
|
||||||
|
|
||||||
|
setMovementState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMovementState()
|
||||||
|
{
|
||||||
|
LastPlayerPosition = Index == 0 ? LastNormalizedPosition : ((CatchDifficultyHitObject)Previous(0)).PlayerPosition;
|
||||||
|
|
||||||
|
PlayerPosition = Math.Clamp(
|
||||||
|
LastPlayerPosition,
|
||||||
|
NormalizedPosition - (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error),
|
||||||
|
NormalizedPosition + (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error)
|
||||||
|
);
|
||||||
|
|
||||||
|
DistanceMoved = PlayerPosition - LastPlayerPosition;
|
||||||
|
|
||||||
|
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||||
|
ExactDistanceMoved = NormalizedPosition - LastPlayerPosition;
|
||||||
|
|
||||||
|
// After a hyperdash we ARE in the correct position. Always!
|
||||||
|
if (LastObject.HyperDash)
|
||||||
|
PlayerPosition = NormalizedPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using osu.Game.Rulesets.Catch.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@@ -11,10 +10,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
public class Movement : StrainDecaySkill
|
public class Movement : StrainDecaySkill
|
||||||
{
|
{
|
||||||
private const float absolute_player_positioning_error = 16f;
|
|
||||||
private const float normalized_hitobject_radius = 41.0f;
|
|
||||||
private const double direction_change_bonus = 21.0;
|
|
||||||
|
|
||||||
protected override double SkillMultiplier => 1;
|
protected override double SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 0.2;
|
protected override double StrainDecayBase => 0.2;
|
||||||
|
|
||||||
@@ -22,107 +17,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
|
|
||||||
protected override int SectionLength => 750;
|
protected override int SectionLength => 750;
|
||||||
|
|
||||||
protected readonly float HalfCatcherWidth;
|
public Movement(Mod[] mods)
|
||||||
|
|
||||||
private float? lastPlayerPosition;
|
|
||||||
private float lastDistanceMoved;
|
|
||||||
private float lastExactDistanceMoved;
|
|
||||||
private double lastStrainTime;
|
|
||||||
private bool isInBuzzSection;
|
|
||||||
|
|
||||||
/// <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)
|
: 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)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
return MovementEvaluator.EvaluateDifficultyOf(current);
|
||||||
|
|
||||||
lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
|
|
||||||
|
|
||||||
float playerPosition = Math.Clamp(
|
|
||||||
lastPlayerPosition.Value,
|
|
||||||
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
|
|
||||||
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
|
|
||||||
);
|
|
||||||
|
|
||||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
|
||||||
|
|
||||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
|
||||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
|
||||||
|
|
||||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
|
||||||
|
|
||||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
|
||||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
|
||||||
|
|
||||||
double edgeDashBonus = 0;
|
|
||||||
|
|
||||||
// Direction change bonus.
|
|
||||||
if (Math.Abs(distanceMoved) > 0.1)
|
|
||||||
{
|
|
||||||
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
|
|
||||||
{
|
|
||||||
double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
|
|
||||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
|
|
||||||
|
|
||||||
distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base bonus for every movement, giving some weight to streams.
|
|
||||||
distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bonus for edge dashes.
|
|
||||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
|
||||||
{
|
|
||||||
if (!catchCurrent.LastObject.HyperDash)
|
|
||||||
edgeDashBonus += 5.7;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// After a hyperdash we ARE in the correct position. Always!
|
|
||||||
playerPosition = catchCurrent.NormalizedPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
|
||||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
|
||||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
|
||||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
|
||||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
|
||||||
{
|
|
||||||
if (isInBuzzSection)
|
|
||||||
distanceAddition = 0;
|
|
||||||
else
|
|
||||||
isInBuzzSection = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isInBuzzSection = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastPlayerPosition = playerPosition;
|
|
||||||
lastDistanceMoved = distanceMoved;
|
|
||||||
lastStrainTime = catchCurrent.StrainTime;
|
|
||||||
lastExactDistanceMoved = exactDistanceMoved;
|
|
||||||
|
|
||||||
return distanceAddition / weightedStrainTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
private double placementStartTime;
|
private double placementStartTime;
|
||||||
private double placementEndTime;
|
private double placementEndTime;
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||||
|
|
||||||
public BananaShowerPlacementBlueprint()
|
public BananaShowerPlacementBlueprint()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
private readonly Path drawablePath;
|
private readonly Path drawablePath;
|
||||||
|
|
||||||
private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
|
private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
|
||||||
|
private readonly List<Vector2> sliderVertices = new List<Vector2>();
|
||||||
|
|
||||||
public ScrollingPath()
|
public ScrollingPath()
|
||||||
{
|
{
|
||||||
@@ -47,9 +48,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
private void computeTimeXs(JuiceStream hitObject)
|
private void computeTimeXs(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
|
sliderVertices.Clear();
|
||||||
var sliderVertices = new List<Vector2>();
|
sliderVertices.AddRange(hitObject.Path.CalculatedPath);
|
||||||
hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
|
|
||||||
|
|
||||||
if (sliderVertices.Count == 0)
|
if (sliderVertices.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
{
|
{
|
||||||
base.UpdateHitObjectFromPath(hitObject);
|
base.UpdateHitObjectFromPath(hitObject);
|
||||||
|
|
||||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement)
|
||||||
EditorBeatmap?.Remove(hitObject);
|
EditorBeatmap?.Remove(hitObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
private InputManager inputManager = null!;
|
private InputManager inputManager = null!;
|
||||||
|
|
||||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||||
|
|
||||||
public JuiceStreamPlacementBlueprint()
|
public JuiceStreamPlacementBlueprint()
|
||||||
{
|
{
|
||||||
|
|||||||