1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-04 00:55:29 +08:00

Compare commits

..

9 Commits

2626 changed files with 39996 additions and 139081 deletions
+12 -9
View File
@@ -3,25 +3,28 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"jetbrains.resharper.globaltools": { "jetbrains.resharper.globaltools": {
"version": "2025.2.3", "version": "2023.3.3",
"commands": [ "commands": [
"jb" "jb"
], ]
"rollForward": false },
"nvika": {
"version": "3.0.0",
"commands": [
"nvika"
]
}, },
"codefilesanity": { "codefilesanity": {
"version": "0.0.41", "version": "0.0.37",
"commands": [ "commands": [
"CodeFileSanity" "CodeFileSanity"
], ]
"rollForward": false
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2025.1208.0", "version": "2024.802.0",
"commands": [ "commands": [
"localisation" "localisation"
], ]
"rollForward": false
} }
} }
} }
-5
View File
@@ -19,11 +19,6 @@ 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.
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
generator: generator:
name: Run name: Run
runs-on: self-hosted runs-on: self-hosted
timeout-minutes: 1440 timeout-minutes: 720
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@v6 uses: actions/checkout@v4
with: with:
path: ${{ inputs.id }} path: ${{ inputs.id }}
repository: 'smoogipoo/diffcalc-sheet-generator' repository: 'smoogipoo/diffcalc-sheet-generator'
+26 -71
View File
@@ -6,7 +6,6 @@ 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:
@@ -14,10 +13,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
@@ -28,7 +27,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@v5 uses: actions/cache@v4
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') }}
@@ -50,14 +49,10 @@ jobs:
exit $exit_code exit $exit_code
- name: InspectCode - name: InspectCode
uses: JetBrains/ReSharper-InspectCode@v0.12 run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
with:
# this is WTF tier but if you don't specify *both* of these the defaults assume `build: true` - name: NVika
build: false run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
no-build: true
solution: ./osu.Desktop.slnf
caches-home: inspectcode
verbosity: WARN
test: test:
name: Test name: Test
@@ -76,10 +71,10 @@ jobs:
timeout-minutes: 120 timeout-minutes: 120
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
@@ -87,101 +82,61 @@ jobs:
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test - name: Test
run: > run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
dotnet test shell: pwsh
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/workflows-and-actions/expressions#cancelled # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results - name: Upload Test Results
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ always() }}
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@v6 uses: actions/checkout@v4
- name: Setup JDK 11 - name: Setup JDK 11
uses: actions/setup-java@v5 uses: actions/setup-java@v4
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@v5 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Install .NET workloads - name: Install .NET workloads
run: dotnet workload install android # since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
- name: Compile - name: Compile
run: dotnet build -c Debug osu.Android.slnf run: dotnet build -c Debug osu.Android.slnf
build-only-ios: build-only-ios:
name: Build only (iOS) name: Build only (iOS)
runs-on: macos-15 runs-on: macos-latest
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v4
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 run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
# 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.slnf run: dotnet build -c Debug osu.iOS
-88
View File
@@ -1,88 +0,0 @@
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
+45
View File
@@ -0,0 +1,45 @@
# 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'
+2 -2
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Create Sentry release - name: Create Sentry release
uses: getsentry/action-release@v3 uses: getsentry/action-release@v1
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ppy SENTRY_ORG: ppy
@@ -38,12 +38,8 @@ 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 --no-build -- mods > ../osu-web/database/mods.json run: dotnet run --project PerformanceCalculator -- 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
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="osu.Android">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
-13
View File
@@ -13,19 +13,6 @@
"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",
+3 -8
View File
@@ -2,10 +2,6 @@
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)
@@ -59,7 +55,9 @@ 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. 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. 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.
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).
@@ -75,9 +73,6 @@ 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.
-7
View File
@@ -18,10 +18,3 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize(
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead. M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() 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.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.
+1 -4
View File
@@ -51,11 +51,8 @@ dotnet_diagnostic.IDE1006.severity = warning
# Too many noisy warnings for parsing/formatting numbers # Too many noisy warnings for parsing/formatting numbers
dotnet_diagnostic.CA1305.severity = none dotnet_diagnostic.CA1305.severity = none
# messagepack complains about "osu" not being title cased due to reserved words
dotnet_diagnostic.CS8981.severity = none
# CA1507: Use nameof to express symbol names # CA1507: Use nameof to express symbol names
# Flags serialization name attributes # Flaggs serialization name attributes
dotnet_diagnostic.CA1507.severity = suggestion dotnet_diagnostic.CA1507.severity = suggestion
# CA1806: Do not ignore method results # CA1806: Do not ignore method results
+1 -5
View File
@@ -3,10 +3,6 @@
<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>
@@ -50,7 +46,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) 2025 ppy Pty Ltd</Copyright> <Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags> <PackageTags>osu game</PackageTags>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>. Copyright (c) 2024 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
+2 -2
View File
@@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu!
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below. If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation. **For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
## Developing a custom ruleset ## Developing a custom ruleset
@@ -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).
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**. 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.
## Licence ## Licence
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</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.11.1" />
<PackageReference Include="NUnit" Version="4.5.1" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.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) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
return new DifficultyAttributes(mods, 0); return new DifficultyAttributes(mods, 0);
} }
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>(); protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }
@@ -14,16 +14,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
public Vector2 Position { get; set; } public Vector2 Position { get; set; }
public float X public float X => Position.X;
{ public float Y => Position.Y;
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
} }
} }
@@ -2,7 +2,6 @@
// 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;
@@ -18,8 +17,5 @@ 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);
} }
} }
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</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.11.1" />
<PackageReference Include="NUnit" Version="4.5.1" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.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" />
@@ -14,16 +14,7 @@ namespace osu.Game.Rulesets.Pippidon.Objects
public Vector2 Position { get; set; } public Vector2 Position { get; set; }
public float X public float X => Position.X;
{ public float Y => Position.Y;
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
} }
} }
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
{ {
} }
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
return new DifficultyAttributes(mods, 0); return new DifficultyAttributes(mods, 0);
} }
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>(); protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }
@@ -9,8 +9,5 @@ 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;
} }
} }
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</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.11.1" />
<PackageReference Include="NUnit" Version="4.5.1" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.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) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
return new DifficultyAttributes(mods, 0); return new DifficultyAttributes(mods, 0);
} }
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>(); protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }
@@ -2,7 +2,6 @@
// 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
@@ -16,8 +15,5 @@ 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);
} }
} }
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</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.11.1" />
<PackageReference Include="NUnit" Version="4.5.1" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.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" />
@@ -1,7 +1,6 @@
// 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 System.Threading; using System.Threading;
@@ -10,6 +9,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.UI; using osu.Game.Rulesets.Pippidon.UI;
using osuTK;
namespace osu.Game.Rulesets.Pippidon.Beatmaps namespace osu.Game.Rulesets.Pippidon.Beatmaps
{ {
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
}; };
} }
private int getLane(HitObject hitObject) => (int)Math.Clamp( private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
(getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1); (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X; private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
{ {
} }
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
return new DifficultyAttributes(mods, 0); return new DifficultyAttributes(mods, 0);
} }
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>(); protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }
@@ -2,7 +2,6 @@
// 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
@@ -16,8 +15,5 @@ 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);
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl> <PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2024 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>
+32
View File
@@ -0,0 +1,32 @@
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\**\*'
+86
View File
@@ -0,0 +1,86 @@
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
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.513.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.1206.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.
+12 -9
View File
@@ -5,13 +5,16 @@
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" />
<provider android:name="androidx.core.content.FileProvider" <!-- for editor usage -->
android:authorities="sh.ppy.osulazer.fileprovider" <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
android:grantUriPermissions="true" <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
android:exported="false"> <!--
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" READ_MEDIA_* permissions are available only on API 33 or greater. Devices with older android versions
android:resource="@xml/filepaths" /> don't understand the new permissions, so request the old READ_EXTERNAL_STORAGE permission to get storage access.
</provider> Since the old permission has no effect on >= API 33, don't request it.
</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>
@@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play;
namespace osu.Android
{
public partial class GameplayScreenRotationLocker : Component
{
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
[Resolved]
private OsuGameActivity gameActivity { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserPlayInfo)
{
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
});
}
}
}
+19 -58
View File
@@ -13,6 +13,7 @@ using Android.Graphics;
using Android.OS; using Android.OS;
using Android.Views; using Android.Views;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Database; using osu.Game.Database;
using Debug = System.Diagnostics.Debug; using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri; using Uri = Android.Net.Uri;
@@ -20,24 +21,13 @@ 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 }, Label = "Import beatmap", DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", 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", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-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-skin-archive")]
DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")] [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
[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",
@@ -60,25 +50,9 @@ namespace osu.Android
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks> /// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
public new bool IsTablet { get; private set; } private OsuGameAndroid game = null!;
private readonly OsuGameAndroid game; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
private bool gameCreated;
protected override Framework.Game CreateGame()
{
if (gameCreated)
throw new InvalidOperationException("Framework tried to create a game twice.");
gameCreated = true;
return game;
}
public OsuGameActivity()
{
game = new OsuGameAndroid(this);
}
protected override void OnCreate(Bundle? savedInstanceState) protected override void OnCreate(Bundle? savedInstanceState)
{ {
@@ -102,9 +76,9 @@ namespace osu.Android
WindowManager.DefaultDisplay.GetSize(displaySize); WindowManager.DefaultDisplay.GetSize(displaySize);
#pragma warning restore CA1422 #pragma warning restore CA1422
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
IsTablet = smallestWidthDp >= 600f; bool isTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android. // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
// The assembly files are not available as files either after native AOT. // The assembly files are not available as files either after native AOT.
@@ -121,38 +95,25 @@ namespace osu.Android
private void handleIntent(Intent? intent) private void handleIntent(Intent? intent)
{ {
if (intent == null) switch (intent?.Action)
return;
switch (intent.Action)
{ {
case Intent.ActionDefault: case Intent.ActionDefault:
if (intent.Scheme == ContentResolver.SchemeContent) if (intent.Scheme == ContentResolver.SchemeContent)
{ handleImportFromUris(intent.Data.AsNonNull());
if (intent.Data != null)
handleImportFromUris(intent.Data);
}
else if (osu_url_schemes.Contains(intent.Scheme)) else if (osu_url_schemes.Contains(intent.Scheme))
{ game.HandleLink(intent.DataString);
if (intent.DataString != null)
game.HandleLink(intent.DataString);
}
break; break;
case Intent.ActionSend: case Intent.ActionSend:
case Intent.ActionSendMultiple: case Intent.ActionSendMultiple:
{ {
if (intent.ClipData == null)
break;
var uris = new List<Uri>(); var uris = new List<Uri>();
for (int i = 0; i < intent.ClipData.ItemCount; i++) for (int i = 0; i < intent.ClipData?.ItemCount; i++)
{ {
var item = intent.ClipData.GetItemAt(i); var content = intent.ClipData?.GetItemAt(i);
if (item?.Uri != null) if (content != null)
uris.Add(item.Uri); uris.Add(content.Uri.AsNonNull());
} }
handleImportFromUris(uris.ToArray()); handleImportFromUris(uris.ToArray());
+38 -45
View File
@@ -2,19 +2,14 @@
// 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 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;
using osu.Game.Screens;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK;
namespace osu.Android namespace osu.Android
{ {
@@ -23,62 +18,60 @@ 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 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 string Version public override Version AssemblyVersion
{ {
get get
{ {
if (!IsDeployedBuild) var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
return packageInfo.VersionName.AsNonNull(); try
{
// 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();
UserPlayingState.BindValueChanged(_ => updateOrientation()); LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
}
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
{
base.ScreenChanged(current, newScreen);
if (newScreen != null)
updateOrientation();
}
private void updateOrientation()
{
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
switch (orientation)
{
case MobileUtils.Orientation.Locked:
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
break;
case MobileUtils.Orientation.Portrait:
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
break;
case MobileUtils.Orientation.Default:
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
break;
}
} }
public override void SetHost(GameHost host) public override void SetHost(GameHost host)
-6
View File
@@ -1,6 +0,0 @@
<?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>
+28 -20
View File
@@ -51,14 +51,16 @@ namespace osu.Desktop
[Resolved] [Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
private IBindable<DiscordRichPresenceMode> privacyMode = null!; [Resolved]
private IBindable<UserStatus> userStatus = null!; private OsuConfigManager config { get; set; } = null!;
private IBindable<UserActivity?> userActivity = null!;
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
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,
@@ -69,12 +71,8 @@ namespace osu.Desktop
private IBindable<APIUser>? user; private IBindable<APIUser>? user;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, SessionStatics session) private void load()
{ {
privacyMode = config.GetBindable<DiscordRichPresenceMode>(OsuSetting.DiscordRichPresence);
userStatus = config.GetBindable<UserStatus>(OsuSetting.UserOnlineStatus);
userActivity = session.GetBindable<UserActivity?>(Static.UserOnlineActivity);
client = new DiscordRpcClient(client_id) client = new DiscordRpcClient(client_id)
{ {
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
@@ -83,7 +81,7 @@ namespace osu.Desktop
}; };
client.OnReady += onReady; client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network); client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
try try
{ {
@@ -107,11 +105,21 @@ namespace osu.Desktop
{ {
base.LoadComplete(); base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
user = api.LocalUser.GetBoundCopy(); user = api.LocalUser.GetBoundCopy();
user.BindValueChanged(u =>
{
status.UnbindBindings();
status.BindTo(u.NewValue.Status);
activity.UnbindBindings();
activity.BindTo(u.NewValue.Activity);
}, true);
ruleset.BindValueChanged(_ => schedulePresenceUpdate()); ruleset.BindValueChanged(_ => schedulePresenceUpdate());
userStatus.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate());
userActivity.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated; multiplayerClient.RoomUpdated += onRoomUpdated;
@@ -143,13 +151,13 @@ namespace osu.Desktop
if (!client.IsInitialized) if (!client.IsInitialized)
return; return;
if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
{ {
client.ClearPresence(); client.ClearPresence();
return; return;
} }
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb; bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
updatePresence(hideIdentifiableInformation); updatePresence(hideIdentifiableInformation);
client.SetPresence(presence); client.SetPresence(presence);
@@ -162,19 +170,19 @@ namespace osu.Desktop
return; return;
// user activity // user activity
if (userActivity.Value != null) if (activity.Value != null)
{ {
presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation)); presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{ {
presence.Buttons = new[] presence.Buttons = new[]
{ {
new Button new Button
{ {
Label = "View beatmap", Label = "View beatmap",
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
} }
}; };
} }
@@ -190,7 +198,7 @@ namespace osu.Desktop
} }
// user party // user party
if (!hideIdentifiableInformation && multiplayerClient.Room != null && !multiplayerClient.Room.Settings.MatchType.IsMatchmakingType()) if (!hideIdentifiableInformation && multiplayerClient.Room != null)
{ {
MultiplayerRoom room = multiplayerClient.Room; MultiplayerRoom room = multiplayerClient.Room;
@@ -1,13 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
namespace osu.Desktop.IPC.Messages
{
public class HitCountMessage : OsuWebSocketMessage
{
[JsonProperty("new_hits")]
public long NewHits { get; init; }
}
}
@@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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();
}
}
}
-75
View File
@@ -1,75 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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;
}
}
}
}
@@ -1,55 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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;
}
}
}
}
+12 -45
View File
@@ -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,12 +15,11 @@ 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;
@@ -34,10 +33,6 @@ 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)
{ {
@@ -72,12 +67,7 @@ namespace osu.Desktop
{ {
try try
{ {
stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz"); stableInstallPath = getStableInstallPathFromRegistry();
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
stableInstallPath = getStableInstallPathFromRegistry("osu!");
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath)) if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath; return stableInstallPath;
@@ -99,9 +89,9 @@ namespace osu.Desktop
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private string? getStableInstallPathFromRegistry(string progId) private string? getStableInstallPathFromRegistry()
{ {
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId)) using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
} }
@@ -109,14 +99,6 @@ 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();
@@ -125,7 +107,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited() public override bool RestartAppWhenExited()
{ {
RestartOnExitAction = () => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId); Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
return true; return true;
} }
@@ -135,39 +117,24 @@ namespace osu.Desktop
LoadComponentAsync(new DiscordRichPresence(), Add); LoadComponentAsync(new DiscordRichPresence(), Add);
switch (RuntimeInfo.OS) if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{ 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);
// Apple operating systems use a better icon provided via external assets. var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (!RuntimeInfo.IsApple) if (iconStream != null)
{ host.Window.SetIconFromStream(iconStream);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
}
host.Window.CursorState |= CursorState.Hidden;
host.Window.Title = Name; host.Window.Title = Name;
} }
+6 -29
View File
@@ -28,15 +28,13 @@ 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(args); setupVelopack();
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
@@ -137,13 +135,7 @@ 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",
});
}
} }
} }
@@ -175,21 +167,8 @@ namespace osu.Desktop
return false; return false;
} }
private static void setupVelopack(string[] args) private static void setupVelopack()
{ {
// 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.");
@@ -198,8 +177,6 @@ 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);
@@ -209,9 +186,9 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private static void configureWindows(VelopackApp app) private static void configureWindows(VelopackApp app)
{ {
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations()); app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations()); app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations()); app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
} }
} }
} }
@@ -7,7 +7,6 @@ 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;
@@ -27,15 +26,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 override bool IsImportant => true;
public ElevatedPrivilegesNotification() public ElevatedPrivilegesNotification()
{ {
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator"); 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.";
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
+80 -89
View File
@@ -2,25 +2,22 @@
// 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 : UpdateManager public partial class VelopackUpdateManager : Game.Updater.UpdateManager
{ {
[Resolved] private readonly UpdateManager updateManager;
private INotificationOverlay notificationOverlay { get; set; } = null!; private INotificationOverlay notificationOverlay = null!;
[Resolved] [Resolved]
private OsuGameBase game { get; set; } = null!; private OsuGameBase game { get; set; } = null!;
@@ -30,128 +27,122 @@ namespace osu.Desktop.Updater
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying; private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
private ScheduledDelegate? scheduledBackgroundCheck; private UpdateInfo? pendingUpdate;
private void scheduleNextUpdateCheck() public VelopackUpdateManager()
{ {
scheduledBackgroundCheck?.Cancel(); updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
scheduledBackgroundCheck = Scheduler.AddDelayed(() =>
{ {
log("Running scheduled background update check..."); AllowVersionDowngrade = true,
CheckForUpdate(); });
}, 60000 * 30);
} }
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken) [BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
{ {
scheduledBackgroundCheck?.Cancel(); notificationOverlay = notifications;
}
if (isInGameplay) protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
{
log("Update check cancelled - user is in gameplay"); private async Task<bool> checkForUpdateAsync()
scheduleNextUpdateCheck(); {
return false; // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
} bool scheduleRecheck = false;
try try
{ {
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon); // Avoid any kind of update checking while gameplay is running.
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions if (isInGameplay)
{ {
AllowVersionDowngrade = true scheduleRecheck = true;
});
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
log("Update check cancelled");
scheduleNextUpdateCheck();
return true; return true;
} }
if (update == null) // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
if (pendingUpdate != null)
{ {
// No update is available. // If there is an update pending restart, show the notification to restart again.
log("No update found"); notificationOverlay.Post(new UpdateApplicationCompleteNotification
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;
} }
// Download update in the background while notifying awaiters of the update being available. // An update is found, let's notify the user and start downloading it.
log($"New update available: {update.TargetFullRelease.Version}"); UpdateProgressNotification notification = new UpdateProgressNotification
downloadUpdate(updateManager, update, cancellationToken); {
return true; CompletionClickAction = () =>
{
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)
{ {
log($"Update check failed with error ({e.Message})"); // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
// we shouldn't crash on a web failure. or any failure for the matter. Logger.Log($@"update check failed ({e.Message})");
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)
{ {
CompletionClickAction = () => if (scheduleRecheck)
{ {
restartToApplyUpdate(updateManager, update); Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
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, CancellationToken cancellationToken) private void runOutsideOfGameplay(Action action)
{ {
if (cancellationToken.IsCancellationRequested)
return;
if (isInGameplay) if (isInGameplay)
{ {
Scheduler.AddDelayed(() => runOutsideOfGameplay(action, cancellationToken), 1000); Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
return; return;
} }
action(); action();
} }
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) private async Task restartToApplyUpdate()
{ {
game.RestartOnExitAction = () => updateManager.WaitExitThenApplyUpdates(update.TargetFullRelease); await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
game.AttemptExit(); Schedule(() => game.AttemptExit());
} }
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
} }
} }
+46 -149
View File
@@ -17,7 +17,6 @@ namespace osu.Desktop.Windows
public static class WindowsAssociationManager public static class WindowsAssociationManager
{ {
private const string software_classes = @"Software\Classes"; private const string software_classes = @"Software\Classes";
private const string software_registered_applications = @"Software\RegisteredApplications";
/// <summary> /// <summary>
/// Sub key for setting the icon. /// Sub key for setting the icon.
@@ -37,11 +36,7 @@ namespace osu.Desktop.Windows
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit, /// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key. /// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
/// </summary> /// </summary>
private const string program_id_file_prefix = "osu.File"; private const string program_id_prefix = "osu.File";
private const string program_id_protocol_prefix = "osu.Uri";
private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)");
private static readonly FileAssociation[] file_associations = private static readonly FileAssociation[] file_associations =
{ {
@@ -61,13 +56,14 @@ namespace osu.Desktop.Windows
/// Installs file and URI associations. /// Installs file and URI associations.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised. /// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks> /// </remarks>
public static void InstallAssociations() public static void InstallAssociations()
{ {
try try
{ {
updateAssociations(); updateAssociations();
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@@ -80,13 +76,17 @@ namespace osu.Desktop.Windows
/// Updates associations with latest definitions. /// Updates associations with latest definitions.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised. /// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks> /// </remarks>
public static void UpdateAssociations() public static void UpdateAssociations()
{ {
try try
{ {
updateAssociations(); updateAssociations();
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@@ -95,19 +95,11 @@ namespace osu.Desktop.Windows
} }
} }
// TODO: call this sometime. public static void UpdateDescriptions(LocalisationManager localisationManager)
public static void LocaliseDescriptions(LocalisationManager localisationManager)
{ {
try try
{ {
application_capability.LocaliseDescription(localisationManager); updateDescriptions(localisationManager);
foreach (var association in file_associations)
association.LocaliseDescription(localisationManager);
foreach (var association in uri_associations)
association.LocaliseDescription(localisationManager);
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@@ -120,8 +112,6 @@ namespace osu.Desktop.Windows
{ {
try try
{ {
application_capability.Uninstall();
foreach (var association in file_associations) foreach (var association in file_associations)
association.Uninstall(); association.Uninstall();
@@ -143,16 +133,22 @@ namespace osu.Desktop.Windows
/// </summary> /// </summary>
private static void updateAssociations() private static void updateAssociations()
{ {
application_capability.Install();
foreach (var association in file_associations) foreach (var association in file_associations)
association.Install(); association.Install();
foreach (var association in uri_associations) foreach (var association in uri_associations)
association.Install(); association.Install();
}
application_capability.RegisterFileAssociations(file_associations); private static void updateDescriptions(LocalisationManager? localisation)
application_capability.RegisterUriAssociations(uri_associations); {
foreach (var association in file_associations)
association.UpdateDescription(getLocalisedString(association.Description));
foreach (var association in uri_associations)
association.UpdateDescription(getLocalisedString(association.Description));
string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
} }
#region Native interop #region Native interop
@@ -178,87 +174,9 @@ namespace osu.Desktop.Windows
#endregion #endregion
private class ApplicationCapability private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
{ {
private string uniqueName { get; } private string programId => $@"{program_id_prefix}{Extension}";
private string capabilityPath { get; }
private LocalisableString description { get; }
public ApplicationCapability(string uniqueName, string capabilityPath, LocalisableString description)
{
this.uniqueName = uniqueName;
this.capabilityPath = capabilityPath;
this.description = description;
}
/// <summary>
/// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs">
/// Registering an Application for Use with Default Programs</see>.
/// </summary>
public void Install()
{
using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
{
capability.SetValue(@"ApplicationDescription", description.ToString());
}
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
registeredApplications?.SetValue(uniqueName, capabilityPath);
}
public void RegisterFileAssociations(FileAssociation[] associations)
{
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return;
using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
foreach (var association in associations)
fileAssociations.SetValue(association.Extension, association.ProgramId);
}
public void RegisterUriAssociations(UriAssociation[] associations)
{
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return;
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
foreach (var association in associations)
urlAssociations.SetValue(association.Protocol, association.ProgramId);
}
public void LocaliseDescription(LocalisationManager localisationManager)
{
using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
{
capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
}
}
public void Uninstall()
{
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
}
}
private class FileAssociation
{
public string ProgramId => $@"{program_id_file_prefix}{Extension}";
public string Extension { get; }
private LocalisableString description { get; }
private string iconPath { get; }
public FileAssociation(string extension, LocalisableString description, string iconPath)
{
Extension = extension;
this.description = description;
this.iconPath = iconPath;
}
/// <summary> /// <summary>
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key /// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
@@ -269,12 +187,10 @@ namespace osu.Desktop.Windows
if (classes == null) return; if (classes == null) return;
// register a program id for the given extension // register a program id for the given extension
using (var programKey = classes.CreateSubKey(ProgramId)) using (var programKey = classes.CreateSubKey(programId))
{ {
programKey.SetValue(null, description.ToString());
using (var defaultIconKey = programKey.CreateSubKey(default_icon)) using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, iconPath); defaultIconKey.SetValue(null, IconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND)) using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
@@ -282,25 +198,23 @@ namespace osu.Desktop.Windows
using (var extensionKey = classes.CreateSubKey(Extension)) using (var extensionKey = classes.CreateSubKey(Extension))
{ {
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer, // set ourselves as the default program
// so having it here is just confusing and may override user preferences. extensionKey.SetValue(null, programId);
if (extensionKey.GetValue(null) is string s && s == ProgramId)
extensionKey.SetValue(null, string.Empty);
// add to the open with dialog // add to the open with dialog
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box // https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds")) using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
openWithKey.SetValue(ProgramId, string.Empty); openWithKey.SetValue(programId, string.Empty);
} }
} }
public void LocaliseDescription(LocalisationManager localisationManager) public void UpdateDescription(string description)
{ {
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; if (classes == null) return;
using (var programKey = classes.OpenSubKey(ProgramId, true)) using (var programKey = classes.OpenSubKey(programId, true))
programKey?.SetValue(null, localisationManager.GetLocalisedString(description)); programKey?.SetValue(null, description);
} }
/// <summary> /// <summary>
@@ -313,34 +227,26 @@ namespace osu.Desktop.Windows
using (var extensionKey = classes.OpenSubKey(Extension, true)) using (var extensionKey = classes.OpenSubKey(Extension, true))
{ {
// clear our default association so that Explorer doesn't show the raw programId to users
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
if (extensionKey?.GetValue(null) is string s && s == programId)
extensionKey.SetValue(null, string.Empty);
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds")) using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false); openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
} }
classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false); classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
} }
} }
private class UriAssociation private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
{ {
/// <summary> /// <summary>
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler." /// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85). /// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
/// </summary> /// </summary>
private const string url_protocol = @"URL Protocol"; public const string URL_PROTOCOL = @"URL Protocol";
public string Protocol { get; }
private LocalisableString description { get; }
private string iconPath { get; }
public UriAssociation(string protocol, LocalisableString description, string iconPath)
{
Protocol = protocol;
this.description = description;
this.iconPath = iconPath;
}
public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}";
/// <summary> /// <summary>
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85). /// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
@@ -352,38 +258,29 @@ namespace osu.Desktop.Windows
using (var protocolKey = classes.CreateSubKey(Protocol)) using (var protocolKey = classes.CreateSubKey(Protocol))
{ {
protocolKey.SetValue(null, $@"URL:{description}"); protocolKey.SetValue(URL_PROTOCOL, string.Empty);
protocolKey.SetValue(url_protocol, string.Empty);
// clear out old data using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false); defaultIconKey.SetValue(null, IconPath);
protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false);
}
// register a program id for the given protocol using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
using (var programKey = classes.CreateSubKey(ProgramId))
{
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
} }
} }
public void LocaliseDescription(LocalisationManager localisationManager) public void UpdateDescription(string description)
{ {
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; if (classes == null) return;
using (var protocolKey = classes.OpenSubKey(Protocol, true)) using (var protocolKey = classes.OpenSubKey(Protocol, true))
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}"); protocolKey?.SetValue(null, $@"URL:{description}");
} }
public void Uninstall() public void Uninstall()
{ {
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false); classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
} }
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.
+3 -4
View File
@@ -24,10 +24,9 @@
<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="10.0.5" /> <PackageReference Include="System.IO.Packaging" Version="8.0.1" />
<!-- Held back due to invite bug in newer versions. See https://github.com/Lachee/discord-rpc-csharp/issues/286--> <PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="DiscordRichPresence" Version="1.5.0.51" /> <PackageReference Include="Velopack" Version="0.0.915" />
<PackageReference Include="Velopack" Version="0.0.1298" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />
+1 -1
View File
@@ -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) 2025 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2024 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.Tests.NonVisual.Filtering; using osu.Game.Screens.Select.Carousel;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
{ {
@@ -42,7 +42,7 @@ namespace osu.Game.Benchmarks
Status = BeatmapOnlineStatus.Loved Status = BeatmapOnlineStatus.Loved
}; };
private FilterMatchingTest.CarouselBeatmap carouselBeatmap = null!; private 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 FilterMatchingTest.CarouselBeatmap(beatmap); carouselBeatmap = new CarouselBeatmap(beatmap);
criteria1 = new FilterCriteria(); criteria1 = new FilterCriteria();
criteria2 = new FilterCriteria criteria2 = new FilterCriteria
{ {
@@ -1,77 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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.15.8" /> <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="4.5.1" /> <PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,15 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,15 +1,16 @@
// 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 UIKit; using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS namespace osu.Game.Rulesets.Catch.Tests.iOS
{ {
public static class Program public static class Application
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
UIApplication.Main(args, null, typeof(AppDelegate)); GameApplication.Main(new OsuTestBrowser());
} }
} }
} }
+3 -1
View File
@@ -35,9 +35,11 @@
<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.039861734717169d, 127, "diffcalc-test")] [TestCase(4.0505463516206195d, 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.1527173897800873d, 127, "diffcalc-test")] [TestCase(5.1696411260785498d, 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,7 +4,6 @@
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
{ {
@@ -22,9 +21,8 @@ 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.GetAdjustedDisplayDifficulty(beatmapInfo, []); var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
} }
@@ -34,9 +32,8 @@ 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.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]); var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
} }
@@ -46,9 +43,8 @@ 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.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModDoubleTime()]); var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
} }
@@ -2,7 +2,6 @@
// 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;
@@ -22,9 +21,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);
ClassicAssert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value); Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
ClassicAssert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value); Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
ClassicAssert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value); Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
} }
private class TestLegacySkin : LegacySkin private class TestLegacySkin : LegacySkin
@@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@@ -22,8 +23,6 @@ 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;
@@ -72,11 +71,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject); contentContainer.Playfield.HitObjectContainer.Add(hitObject);
} }
protected override void UpdatePlacementTimeAndPosition() protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
{ {
var position = InputManager.CurrentState.Mouse.Position; var result = base.SnapForBlueprint(blueprint);
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP; result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
CurrentBlueprint.UpdateTimeAndPosition(position, time); return result;
} }
} }
} }
@@ -1,6 +1,8 @@
// 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.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{ {
private JuiceStream hitObject = null!; private JuiceStream hitObject;
private readonly ManualClock manualClock = new ManualClock(); private readonly ManualClock manualClock = new ManualClock();
@@ -191,17 +193,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addVertexCheckStep(1, 0, times[0], positions[0]); addVertexCheckStep(1, 0, times[0], positions[0]);
} }
[Test]
public void TestDeletingSecondVertexDeletesEntireJuiceStream()
{
double[] times = { 100, 400 };
float[] positions = { 100, 150 };
addBlueprintStep(times, positions);
addDeleteVertexSteps(times[1], positions[1]);
AddAssert("juice stream deleted", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test] [Test]
public void TestVertexResampling() public void TestVertexResampling()
{ {
@@ -1,21 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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
});
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,2 @@
[General]
// no version specified means v1
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
[Test] [Test]
public void TestTinyDropletMissChangesCatcherState() public void TestTinyDropletMissPreservesCatcherState()
{ {
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 is changed but hyper dash state is preserved // catcher state and hyper dash state is preserved
checkState(CatcherAnimationState.Fail); checkState(CatcherAnimationState.Kiai);
checkHyperDash(true); checkHyperDash(true);
} }
@@ -193,20 +193,20 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public Color4 HyperDashColour public Color4 HyperDashColour
{ {
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)]; get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)] = value; set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
} }
public Color4 HyperDashAfterImageColour public Color4 HyperDashAfterImageColour
{ {
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)]; get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)] = value; set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
} }
public Color4 HyperDashFruitColour public Color4 HyperDashFruitColour
{ {
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)]; get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)] = value; set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
} }
public TestSkin() public TestSkin()
@@ -1,75 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.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="18.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.5.1" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
@@ -1,11 +1,9 @@
// 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;
@@ -18,30 +16,26 @@ 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 = BeatmapStatisticStrings.Fruits, Name = @"Fruit Count",
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 = BeatmapStatisticStrings.JuiceStreams, Name = @"Juice Stream Count",
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 = BeatmapStatisticStrings.BananaShowers, Name = @"Banana Shower Count",
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.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1, TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1 SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield(); }.Yield();

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