1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-14 01:32:40 +08:00

Compare commits

...

4240 Commits

1728 changed files with 75095 additions and 20464 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
]
},
"nvika": {
"version": "3.0.0",
"version": "4.0.0",
"commands": [
"nvika"
]
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
generator:
name: Run
runs-on: self-hosted
timeout-minutes: 720
timeout-minutes: 1440
outputs:
target: ${{ steps.run.outputs.target }}
+21 -9
View File
@@ -82,8 +82,18 @@ jobs:
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
shell: pwsh
run: >
dotnet test
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
--
NUnit.ConsoleOut=0
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
@@ -114,17 +124,14 @@ jobs:
dotnet-version: "8.0.x"
- name: Install .NET workloads
# 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)
run: dotnet workload install android
- name: Compile
run: dotnet build -c Debug osu.Android.slnf
build-only-ios:
name: Build only (iOS)
runs-on: macos-latest
runs-on: macos-15
timeout-minutes: 60
steps:
- name: Checkout
@@ -136,7 +143,12 @@ jobs:
dotnet-version: "8.0.x"
- name: Install .NET Workloads
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
run: dotnet workload install ios
# https://github.com/dotnet/macios/issues/19157
# https://github.com/actions/runner-images/issues/12758
- name: Use Xcode 16.4
run: sudo xcode-select -switch /Applications/Xcode_16.4.app
- name: Build
run: dotnet build -c Debug osu.iOS
run: dotnet build -c Debug osu.iOS.slnf
+87
View File
@@ -0,0 +1,87 @@
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@v4
- 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@v4
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@v4
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
@@ -0,0 +1,10 @@
<?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>
+7
View File
@@ -18,3 +18,10 @@ 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.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: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.
+4 -1
View File
@@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
# Too many noisy warnings for parsing/formatting numbers
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
# Flaggs serialization name attributes
# Flags serialization name attributes
dotnet_diagnostic.CA1507.severity = suggestion
# CA1806: Do not ignore method results
+5 -1
View File
@@ -3,6 +3,10 @@
<PropertyGroup Label="C#">
<LangVersion>12.0</LangVersion>
<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>
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
@@ -46,7 +50,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -1
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.
**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.
**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.
## Developing a custom ruleset
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
using osuTK;
@@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
if (button.HasValue)
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>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}
@@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
public class PippidonReplayFrame : ReplayFrame
{
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>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyScrolling.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
if (button.HasValue)
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>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.UI;
using osuTK;
namespace osu.Game.Rulesets.Pippidon.Beatmaps
{
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
};
}
private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
private int getLane(HitObject hitObject) => (int)Math.Clamp(
(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;
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
if (button.HasValue)
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>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework>
-32
View File
@@ -1,32 +0,0 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2022
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
before_build:
- cmd: dotnet --info # Useful when version mismatch between CI and local
- cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects
- cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects
- cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
after_build:
- ps: .\InspectCode.ps1
test:
assemblies:
except:
- '**\*Android*'
- '**\*iOS*'
- 'build\**\*'
-86
View File
@@ -1,86 +0,0 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
environment:
matrix:
- job_name: osu-game
- job_name: osu-ruleset
job_depends_on: osu-game
- job_name: taiko-ruleset
job_depends_on: osu-game
- job_name: catch-ruleset
job_depends_on: osu-game
- job_name: mania-ruleset
job_depends_on: osu-game
- job_name: templates
job_depends_on: osu-game
nuget:
project_feed: true
for:
-
matrix:
only:
- job_name: osu-game
build_script:
- cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: osu-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: taiko-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: catch-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: mania-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: templates
build_script:
- cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
artifacts:
- path: '**\*.nupkg'
deploy:
- provider: Environment
name: nuget
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1224.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.908.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -1,34 +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 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;
});
}
}
}
+40 -12
View File
@@ -13,7 +13,6 @@ using Android.Graphics;
using Android.OS;
using Android.Views;
using osu.Framework.Android;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;
@@ -50,9 +49,25 @@ namespace osu.Android
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
private OsuGameAndroid game = null!;
public new bool IsTablet { get; private set; }
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
private readonly OsuGameAndroid game;
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)
{
@@ -76,9 +91,9 @@ namespace osu.Android
WindowManager.DefaultDisplay.GetSize(displaySize);
#pragma warning restore CA1422
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
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.
// The assembly files are not available as files either after native AOT.
@@ -95,25 +110,38 @@ namespace osu.Android
private void handleIntent(Intent? intent)
{
switch (intent?.Action)
if (intent == null)
return;
switch (intent.Action)
{
case Intent.ActionDefault:
if (intent.Scheme == ContentResolver.SchemeContent)
handleImportFromUris(intent.Data.AsNonNull());
{
if (intent.Data != null)
handleImportFromUris(intent.Data);
}
else if (osu_url_schemes.Contains(intent.Scheme))
game.HandleLink(intent.DataString);
{
if (intent.DataString != null)
game.HandleLink(intent.DataString);
}
break;
case Intent.ActionSend:
case Intent.ActionSendMultiple:
{
if (intent.ClipData == null)
break;
var uris = new List<Uri>();
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
for (int i = 0; i < intent.ClipData.ItemCount; i++)
{
var content = intent.ClipData?.GetItemAt(i);
if (content != null)
uris.Add(content.Uri.AsNonNull());
var item = intent.ClipData.GetItemAt(i);
if (item?.Uri != null)
uris.Add(item.Uri);
}
handleImportFromUris(uris.ToArray());
+45 -38
View File
@@ -2,14 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater;
using osu.Game.Utils;
using osuTK;
namespace osu.Android
{
@@ -18,60 +23,62 @@ namespace osu.Android
[Cached]
private readonly OsuGameActivity gameActivity;
private readonly PackageInfo packageInfo;
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
}
public override Version AssemblyVersion
public override string Version
{
get
{
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
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());
return packageInfo.VersionName.AsNonNull();
}
}
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
UserPlayingState.BindValueChanged(_ => updateOrientation());
}
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)
+18 -27
View File
@@ -51,12 +51,9 @@ namespace osu.Desktop
[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = 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 IBindable<DiscordRichPresenceMode> privacyMode = null!;
private IBindable<UserStatus> userStatus = null!;
private IBindable<UserActivity?> userActivity = null!;
private readonly RichPresence presence = new RichPresence
{
@@ -71,8 +68,12 @@ namespace osu.Desktop
private IBindable<APIUser>? user;
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config, SessionStatics session)
{
privacyMode = config.GetBindable<DiscordRichPresenceMode>(OsuSetting.DiscordRichPresence);
userStatus = config.GetBindable<UserStatus>(OsuSetting.UserOnlineStatus);
userActivity = session.GetBindable<UserActivity?>(Static.UserOnlineActivity);
client = new DiscordRpcClient(client_id)
{
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
@@ -81,7 +82,7 @@ namespace osu.Desktop
};
client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
try
{
@@ -105,21 +106,11 @@ namespace osu.Desktop
{
base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
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());
status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate());
userStatus.BindValueChanged(_ => schedulePresenceUpdate());
userActivity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated;
@@ -151,13 +142,13 @@ namespace osu.Desktop
if (!client.IsInitialized)
return;
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
{
client.ClearPresence();
return;
}
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb;
updatePresence(hideIdentifiableInformation);
client.SetPresence(presence);
@@ -170,19 +161,19 @@ namespace osu.Desktop
return;
// user activity
if (activity.Value != null)
if (userActivity.Value != null)
{
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{
presence.Buttons = new[]
{
new Button
{
Label = "View beatmap",
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
}
};
}
+20 -4
View File
@@ -17,6 +17,7 @@ using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.Windows;
using osu.Framework.Allocation;
using osu.Game.Configuration;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.Multiplayer;
@@ -33,6 +34,8 @@ namespace osu.Desktop
[Cached(typeof(IHighPerformanceSessionManager))]
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
public bool IsFirstRun { get; init; }
public OsuGameDesktop(string[]? args = null)
: base(args)
{
@@ -67,7 +70,12 @@ namespace osu.Desktop
{
try
{
stableInstallPath = getStableInstallPathFromRegistry();
stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz");
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
stableInstallPath = getStableInstallPathFromRegistry("osu!");
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
@@ -89,9 +97,9 @@ namespace osu.Desktop
}
[SupportedOSPlatform("windows")]
private string? getStableInstallPathFromRegistry()
private string? getStableInstallPathFromRegistry(string progId)
{
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId))
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
}
@@ -99,6 +107,14 @@ namespace osu.Desktop
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)
return new NoActionUpdateManager();
@@ -107,7 +123,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited()
{
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
return true;
}
+28 -6
View File
@@ -28,13 +28,15 @@ namespace osu.Desktop
private static LegacyTcpIpcProvider? legacyIpc;
private static bool isFirstRun;
[STAThread]
public static void Main(string[] args)
{
// 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
// last time has been fixed, let's not tempt fate.
setupVelopack();
setupVelopack(args);
if (OperatingSystem.IsWindows())
{
@@ -135,7 +137,12 @@ namespace osu.Desktop
if (tournamentClient)
host.Run(new TournamentGame());
else
host.Run(new OsuGameDesktop(args));
{
host.Run(new OsuGameDesktop(args)
{
IsFirstRun = isFirstRun
});
}
}
}
@@ -167,8 +174,21 @@ namespace osu.Desktop
return false;
}
private static void setupVelopack()
private static void setupVelopack(string[] args)
{
// Arguments being present indicate the user is either starting the game in a special (aka tournament) mode,
// or is running with pending imports via file association or otherwise.
//
// In both these scenarios, we'd hope the game does not attempt to update.
//
// Special consideration for velopack startup arguments, which must be handled during update.
// See https://docs.velopack.io/integrating/hooks#command-line-hooks.
if (args.Length > 0 && !args[0].StartsWith("--velo", StringComparison.Ordinal))
{
Logger.Log("Handling arguments, skipping velopack setup.");
return;
}
if (OsuGameDesktop.IsPackageManaged)
{
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
@@ -177,6 +197,8 @@ namespace osu.Desktop
var app = VelopackApp.Build();
app.OnFirstRun(_ => isFirstRun = true);
if (OperatingSystem.IsWindows())
configureWindows(app);
@@ -186,9 +208,9 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")]
private static void configureWindows(VelopackApp app)
{
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
}
}
}
@@ -30,8 +30,6 @@ namespace osu.Desktop.Security
private partial class ElevatedPrivilegesNotification : SimpleNotification
{
public override bool IsImportant => true;
public ElevatedPrivilegesNotification()
{
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
+89 -80
View File
@@ -2,22 +2,25 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Velopack;
using Velopack.Sources;
using UpdateManager = osu.Game.Updater.UpdateManager;
namespace osu.Desktop.Updater
{
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
public partial class VelopackUpdateManager : UpdateManager
{
private readonly UpdateManager updateManager;
private INotificationOverlay notificationOverlay = null!;
[Resolved]
private INotificationOverlay notificationOverlay { get; set; } = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
@@ -27,122 +30,128 @@ namespace osu.Desktop.Updater
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
private UpdateInfo? pendingUpdate;
private ScheduledDelegate? scheduledBackgroundCheck;
public VelopackUpdateManager()
private void scheduleNextUpdateCheck()
{
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
scheduledBackgroundCheck?.Cancel();
scheduledBackgroundCheck = Scheduler.AddDelayed(() =>
{
AllowVersionDowngrade = true,
});
log("Running scheduled background update check...");
CheckForUpdate();
}, 60000 * 30);
}
[BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
{
notificationOverlay = notifications;
}
scheduledBackgroundCheck?.Cancel();
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync()
{
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
bool scheduleRecheck = false;
if (isInGameplay)
{
log("Update check cancelled - user is in gameplay");
scheduleNextUpdateCheck();
return false;
}
try
{
// Avoid any kind of update checking while gameplay is running.
if (isInGameplay)
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
{
scheduleRecheck = true;
AllowVersionDowngrade = true
});
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
log("Update check cancelled");
scheduleNextUpdateCheck();
return true;
}
// 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)
if (update == null)
{
// If there is an update pending restart, show the notification to restart again.
notificationOverlay.Post(new UpdateApplicationCompleteNotification
{
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;
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
return false;
}
// An update is found, let's notify the user and start downloading it.
UpdateProgressNotification notification = new UpdateProgressNotification
{
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!");
}
// Download update in the background while notifying awaiters of the update being available.
log($"New update available: {update.TargetFullRelease.Version}");
downloadUpdate(updateManager, update, cancellationToken);
return true;
}
catch (Exception e)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
Logger.Log($@"update check failed ({e.Message})");
log($"Update check failed with error ({e.Message})");
// we shouldn't crash on a web failure. or any failure for the matter.
scheduleNextUpdateCheck();
return true;
}
finally
}
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>
{
log($"Beginning download of update {update.TargetFullRelease.Version}...");
UpdateDownloadProgressNotification progressNotification = new UpdateDownloadProgressNotification(cancellationToken)
{
if (scheduleRecheck)
CompletionClickAction = () =>
{
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
restartToApplyUpdate(updateManager, update);
return true;
}
};
try
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(progressNotification.CancellationToken, cancellationToken))
{
progressNotification.StartDownload();
runOutsideOfGameplay(() => notificationOverlay.Post(progressNotification), cts.Token);
await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, cts.Token).ConfigureAwait(false);
runOutsideOfGameplay(() => progressNotification.State = ProgressNotificationState.Completed, cts.Token);
}
}
catch (OperationCanceledException)
{
progressNotification.FailDownload();
log(@"Update cancelled");
}
catch (Exception e)
{
// In the case of an error, a separate notification will be displayed.
progressNotification.FailDownload();
Logger.Error(e, @"Update failed!");
}
return true;
}
}, cancellationToken);
private void runOutsideOfGameplay(Action action)
private void runOutsideOfGameplay(Action action, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
if (isInGameplay)
{
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
Scheduler.AddDelayed(() => runOutsideOfGameplay(action, cancellationToken), 1000);
return;
}
action();
}
private async Task restartToApplyUpdate()
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) => Task.Run(async () =>
{
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false);
Schedule(() => game.AttemptExit());
}
});
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
}
}
+149 -46
View File
@@ -17,6 +17,7 @@ namespace osu.Desktop.Windows
public static class WindowsAssociationManager
{
private const string software_classes = @"Software\Classes";
private const string software_registered_applications = @"Software\RegisteredApplications";
/// <summary>
/// Sub key for setting the icon.
@@ -36,7 +37,11 @@ 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,
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
/// </summary>
private const string program_id_prefix = "osu.File";
private const string program_id_file_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 =
{
@@ -56,14 +61,13 @@ namespace osu.Desktop.Windows
/// Installs file and URI associations.
/// </summary>
/// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks>
public static void InstallAssociations()
{
try
{
updateAssociations();
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate();
}
catch (Exception e)
@@ -76,17 +80,13 @@ namespace osu.Desktop.Windows
/// Updates associations with latest definitions.
/// </summary>
/// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks>
public static void UpdateAssociations()
{
try
{
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();
}
catch (Exception e)
@@ -95,11 +95,19 @@ namespace osu.Desktop.Windows
}
}
public static void UpdateDescriptions(LocalisationManager localisationManager)
// TODO: call this sometime.
public static void LocaliseDescriptions(LocalisationManager localisationManager)
{
try
{
updateDescriptions(localisationManager);
application_capability.LocaliseDescription(localisationManager);
foreach (var association in file_associations)
association.LocaliseDescription(localisationManager);
foreach (var association in uri_associations)
association.LocaliseDescription(localisationManager);
NotifyShellUpdate();
}
catch (Exception e)
@@ -112,6 +120,8 @@ namespace osu.Desktop.Windows
{
try
{
application_capability.Uninstall();
foreach (var association in file_associations)
association.Uninstall();
@@ -133,22 +143,16 @@ namespace osu.Desktop.Windows
/// </summary>
private static void updateAssociations()
{
application_capability.Install();
foreach (var association in file_associations)
association.Install();
foreach (var association in uri_associations)
association.Install();
}
private static void updateDescriptions(LocalisationManager? localisation)
{
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();
application_capability.RegisterFileAssociations(file_associations);
application_capability.RegisterUriAssociations(uri_associations);
}
#region Native interop
@@ -174,9 +178,87 @@ namespace osu.Desktop.Windows
#endregion
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
private class ApplicationCapability
{
private string programId => $@"{program_id_prefix}{Extension}";
private string uniqueName { get; }
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>
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
@@ -187,10 +269,12 @@ namespace osu.Desktop.Windows
if (classes == null) return;
// 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))
defaultIconKey.SetValue(null, IconPath);
defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
@@ -198,23 +282,25 @@ namespace osu.Desktop.Windows
using (var extensionKey = classes.CreateSubKey(Extension))
{
// set ourselves as the default program
extensionKey.SetValue(null, programId);
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer,
// so having it here is just confusing and may override user preferences.
if (extensionKey.GetValue(null) is string s && s == ProgramId)
extensionKey.SetValue(null, string.Empty);
// 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
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
openWithKey.SetValue(programId, string.Empty);
openWithKey.SetValue(ProgramId, string.Empty);
}
}
public void UpdateDescription(string description)
public void LocaliseDescription(LocalisationManager localisationManager)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var programKey = classes.OpenSubKey(programId, true))
programKey?.SetValue(null, description);
using (var programKey = classes.OpenSubKey(ProgramId, true))
programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
}
/// <summary>
@@ -227,26 +313,34 @@ namespace osu.Desktop.Windows
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"))
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false);
}
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
}
}
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
private class UriAssociation
{
/// <summary>
/// "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).
/// </summary>
public const string URL_PROTOCOL = @"URL Protocol";
private 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>
/// 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).
@@ -258,29 +352,38 @@ namespace osu.Desktop.Windows
using (var protocolKey = classes.CreateSubKey(Protocol))
{
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
protocolKey.SetValue(null, $@"URL:{description}");
protocolKey.SetValue(url_protocol, string.Empty);
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath);
// clear out old data
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false);
}
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
// register a program id for the given protocol
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""");
}
}
public void UpdateDescription(string description)
public void LocaliseDescription(LocalisationManager localisationManager)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var protocolKey = classes.OpenSubKey(Protocol, true))
protocolKey?.SetValue(null, $@"URL:{description}");
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
}
public void Uninstall()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.
+2 -2
View File
@@ -24,9 +24,9 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.915" />
<PackageReference Include="Velopack" Version="0.0.1298" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
+1 -1
View File
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<language>en-AU</language>
</metadata>
<files>
@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -21,8 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -32,8 +34,9 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
}
@@ -43,8 +46,9 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
}
@@ -12,7 +12,6 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
@@ -23,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
protected sealed override Ruleset CreateRuleset() => new CatchRuleset();
protected const double TIME_SNAP = 100;
protected DrawableCatchHitObject LastObject;
@@ -71,11 +72,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
protected override void UpdatePlacementTimeAndPosition()
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
return result;
var position = InputManager.CurrentState.Mouse.Position;
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
CurrentBlueprint.UpdateTimeAndPosition(position, time);
}
}
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
private JuiceStream hitObject;
private JuiceStream hitObject = null!;
private readonly ManualClock manualClock = new ManualClock();
@@ -193,6 +191,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
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]
public void TestVertexResampling()
{
@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public partial class TestSceneCatchModMovingFast : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestMovingFast() => CreateModTest(new ModTestData
{
Mod = new CatchModMovingFast(),
PassCondition = () => true
});
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -1,2 +0,0 @@
[General]
// no version specified means v1
@@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneReplayRecording : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new Fruit { StartTime = 0, },
new Fruit { StartTime = 5000, },
new Fruit { StartTime = 10000, },
new Fruit { StartTime = 15000, }
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
public void TestRecording()
{
seekTo(0);
AddStep("start moving left", () => InputManager.PressKey(Key.Left));
seekTo(5000);
AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left));
AddAssert("catcher max left", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(0));
AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft])));
AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, 0)));
AddStep("start dashing right", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.Right);
});
seekTo(10000);
AddStep("end dashing right", () =>
{
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.Right);
});
AddAssert("catcher max right", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH));
AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight])));
AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH)));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
}
}
}
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
@@ -16,26 +18,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
int fruits = HitObjects.Count(s => s is Fruit);
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
int bananaShowers = HitObjects.Count(s => s is BananaShower);
int sum = Math.Max(1, fruits + juiceStreams);
return new[]
{
new BeatmapStatistic
{
Name = @"Fruit Count",
Name = BeatmapStatisticStrings.Fruits,
Content = fruits.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = fruits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Name = BeatmapStatisticStrings.JuiceStreams,
Content = juiceStreams.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = juiceStreams / (float)sum,
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Name = BeatmapStatisticStrings.BananaShowers,
Content = bananaShowers.ToString(),
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,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield();
+37 -3
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -11,9 +12,11 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Edit.Setup;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -24,6 +27,7 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@@ -32,6 +36,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Catch
@@ -149,6 +154,7 @@ namespace osu.Game.Rulesets.Catch
new CatchModFloatingFruits(),
new CatchModMuted(),
new CatchModNoScope(),
new CatchModMovingFast(),
};
case ModType.System:
@@ -228,7 +234,7 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[
new MetadataSection(),
new DifficultySection(),
new CatchDifficultySection(),
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
@@ -264,9 +270,10 @@ namespace osu.Game.Rulesets.Catch
}
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;
@@ -275,6 +282,33 @@ namespace osu.Game.Rulesets.Catch
return adjustedDifficulty;
}
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
{
var originalDifficulty = beatmapInfo.Difficulty;
var effectiveDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
yield return new RulesetBeatmapAttribute(SongSelectStrings.CircleSize, @"CS", originalDifficulty.CircleSize, effectiveDifficulty.CircleSize, 10)
{
Description = "Affects the size of fruits.",
AdditionalMetrics =
[
new RulesetBeatmapAttribute.AdditionalMetric("Hit circle radius", (CatchHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(effectiveDifficulty.CircleSize)).ToLocalisableString("0.#"))
]
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.ApproachRate, @"AR", originalDifficulty.ApproachRate, effectiveDifficulty.ApproachRate, 10)
{
Description = "Affects how early fruits fade in on the screen.",
AdditionalMetrics =
[
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRange(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
]
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
{
Description = "Affects the harshness of health drain and the health penalties for missing."
};
}
public override bool EditorShowScrollSpeed => false;
}
}
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
@@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
/// <summary>
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
/// </summary>
/// <remarks>
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
/// </remarks>
[JsonProperty("approach_rate")]
public double ApproachRate { get; set; }
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
@@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Todo: osu!catch should not output star rating in the 'aim' attribute.
yield return (ATTRIB_ID_AIM, StarRating);
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
@@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_AIM];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
}
}
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth;
public override int Version => 20220701;
public override int Version => 20250306;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -35,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (beatmap.HitObjects.Count == 0)
return new CatchDifficultyAttributes { Mods = mods };
// this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
{
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
StarRating = Math.Sqrt(skills.OfType<Movement>().Single().DifficultyValue()) * difficulty_multiplier,
Mods = mods,
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = beatmap.GetMaxCombo(),
};
@@ -3,6 +3,9 @@
using System;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (catchAttributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
double approachRate = catchAttributes.ApproachRate;
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
var track = new TrackVirtual(10000);
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
double clockRate = track.Rate;
// this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0;
double approachRateFactor = 1.0;
if (approachRate > 9.0)
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
@@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float? lastPlayerPosition;
private float lastDistanceMoved;
private float lastExactDistanceMoved;
private double lastStrainTime;
private bool isInBuzzSection;
/// <summary>
/// The speed multiplier applied to the player's catcher.
@@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
// For the exact position we consider that the catcher is in the correct position for both objects
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
@@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
{
if (isInBuzzSection)
distanceAddition = 0;
else
isInBuzzSection = true;
}
else
{
isInBuzzSection = false;
}
lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved;
lastStrainTime = catchCurrent.StrainTime;
lastExactDistanceMoved = exactDistanceMoved;
return distanceAddition / weightedStrainTime;
}
@@ -7,6 +7,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
@@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
if (!(result.Time is double time)) return;
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
if (!(result.Time is double time)) return result;
switch (PlacementActive)
{
@@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
return result;
}
}
}
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
where THitObject : CatchHitObject, new()
{
protected new THitObject HitObject => (THitObject)base.HitObject;
@@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
[Resolved]
private Playfield playfield { get; set; } = null!;
public CatchPlacementBlueprint()
[Resolved]
protected CatchHitObjectComposer? Composer { get; private set; }
protected CatchPlacementBlueprint()
: base(new THitObject())
{
}
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
}));
}
public void UpdateHitObjectFromPath(JuiceStream hitObject)
public virtual void UpdateHitObjectFromPath(JuiceStream hitObject)
{
// The SV setting may need to be changed for the current path.
var svBindable = hitObject.SliderVelocityMultiplierBindable;
@@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
EditorBeatmap?.EndChange();
}
public override void UpdateHitObjectFromPath(JuiceStream hitObject)
{
base.UpdateHitObjectFromPath(hitObject);
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement)
EditorBeatmap?.Remove(hitObject);
}
}
}
@@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
@@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
return result;
}
}
}
@@ -83,15 +83,22 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
switch (PlacementActive)
{
case PlacementState.Waiting:
if (!(result.Time is double snappedTime)) return;
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
HitObject.StartTime = snappedTime;
if (result.Time is double snappedTime)
HitObject.StartTime = snappedTime;
break;
case PlacementState.Active:
@@ -100,28 +107,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
break;
default:
return;
return result;
}
// Make sure the up-to-date position is used for outlines.
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
updateHitObjectFromPath();
}
if (lastEditablePathId != editablePath.PathId)
editablePath.UpdateHitObjectFromPath(HitObject);
lastEditablePathId = editablePath.PathId;
private void updateHitObjectFromPath()
{
if (lastEditablePathId == editablePath.PathId)
return;
editablePath.UpdateHitObjectFromPath(HitObject);
ApplyDefaultsToHitObject();
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
lastEditablePathId = editablePath.PathId;
return result;
}
private double positionToTime(float relativeYPosition)
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Catch.Edit
@@ -13,7 +14,14 @@ namespace osu.Game.Rulesets.Catch.Edit
{
private readonly List<ICheck> checks = new List<ICheck>
{
// Compose
new CheckBananaShowerGap(),
new CheckConcurrentObjects(),
// Spread
new CheckCatchLowestDiffDrainTime(),
// Settings
new CheckCatchAbnormalDifficultySettings(),
};
@@ -1,16 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
{
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
public CatchBlueprintContainer(CatchHitObjectComposer composer)
: base(composer)
{
@@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
}
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
}
}
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit
//
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -18,15 +19,15 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
[Cached]
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
{
private const float distance_snap_radius = 50;
public const float DISTANCE_SNAP_RADIUS = 50;
private CatchDistanceSnapGrid distanceSnapGrid = null!;
@@ -72,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());
@@ -136,22 +137,12 @@ namespace osu.Game.Rulesets.Catch.Edit
DistanceSnapProvider.HandleToggleViaKey(key);
}
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
{
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
return snapResult;
result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{
result = snapResult;
}
}
return result;
return null;
}
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
@@ -229,5 +220,40 @@ namespace osu.Game.Rulesets.Catch.Edit
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
#region Clipboard handling
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<CatchHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<CatchHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',');
for (int i = 0; i < splitDescription.Length; i++)
{
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
continue;
CatchHitObject? current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < splitDescription.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
#endregion
}
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
using Direction = osu.Framework.Graphics.Direction;
namespace osu.Game.Rulesets.Catch.Edit
@@ -38,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Edit
return true;
}
moveSelection(deltaX);
return true;
}
private void moveSelection(float deltaX)
{
EditorBeatmap.PerformOnSelection(h =>
{
if (!(h is CatchHitObject catchObject)) return;
@@ -48,7 +57,60 @@ namespace osu.Game.Rulesets.Catch.Edit
foreach (var nested in catchObject.NestedHitObjects.OfType<CatchHitObject>())
nested.OriginalX += deltaX;
});
}
private bool nudgeMovementActive;
protected override bool OnKeyDown(KeyDownEvent e)
{
// Until the keys below are global actions, this will prevent conflicts with "seek between sample points"
// which has a default of ctrl+shift+arrows.
if (e.ShiftPressed)
return false;
if (e.ControlPressed)
{
switch (e.Key)
{
case Key.Left:
return nudgeSelection(-1);
case Key.Right:
return nudgeSelection(1);
}
}
return false;
}
protected override void OnKeyUp(KeyUpEvent e)
{
base.OnKeyUp(e);
if (nudgeMovementActive && !e.ControlPressed)
{
EditorBeatmap.EndChange();
nudgeMovementActive = false;
}
}
/// <summary>
/// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
/// </summary>
private bool nudgeSelection(float deltaX)
{
if (!nudgeMovementActive)
{
nudgeMovementActive = true;
EditorBeatmap.BeginChange();
}
var firstBlueprint = SelectedBlueprints.FirstOrDefault();
if (firstBlueprint == null)
return false;
moveSelection(deltaX);
return true;
}
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var hitObjects = context.Beatmap.HitObjects;
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
for (int i = 0; i < hitObjects.Count - 1; ++i)
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
var diff = context.CurrentDifficulty.Playable.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
namespace osu.Game.Rulesets.Catch.Edit.Checks
{
public class CheckCatchLowestDiffDrainTime : CheckLowestDiffDrainTime
{
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
{
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Platter");
yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Rain");
yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Overdose");
}
}
}
@@ -0,0 +1,127 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Catch.Edit.Setup
{
public partial class CatchDifficultySection : SetupSection
{
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
circleSizeSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsCs,
HintText = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
healthDrainSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsDrain,
HintText = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
approachRateSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsAr,
HintText = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
baseVelocitySlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
KeyboardStep = 0.1f,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
tickRateSlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
KeyboardStep = 1,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
};
foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
@@ -10,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -11,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema<CatchHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -35,21 +37,39 @@ namespace osu.Game.Rulesets.Catch.Mods
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
public override string SettingDescription
public override string ExtendedIconInformation
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
return string.Empty;
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate,
spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
if (!CircleSize.IsDefault) return format("CS", CircleSize);
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
if (!DrainRate.IsDefault) return format("HP", DrainRate);
return string.Empty;
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
}
}
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
if (!CircleSize.IsDefault)
yield return ("Circle size", $"{CircleSize.Value:N1}");
foreach (var setting in base.SettingDescription)
yield return setting;
if (!ApproachRate.IsDefault)
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
if (!HardRockOffsets.IsDefault)
yield return ("Spicy patterns", "On");
}
}
+9 -1
View File
@@ -2,12 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
}
}
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 325;
public override float DefaultFlashlightSize => 203.125f;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);

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