A game framework written with osu! in mind.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'master' into bassmix

+1442 -406
+6
.config/dotnet-tools.json
··· 25 25 "commands": [ 26 26 "nvika" 27 27 ] 28 + }, 29 + "codefilesanity": { 30 + "version": "0.0.36", 31 + "commands": [ 32 + "CodeFileSanity" 33 + ] 28 34 } 29 35 } 30 36 }
+7 -1
.editorconfig
··· 211 211 dotnet_diagnostic.CA2225.severity = none 212 212 213 213 # Banned APIs 214 - dotnet_diagnostic.RS0030.severity = error 214 + dotnet_diagnostic.RS0030.severity = error 215 + 216 + [*.{yaml,yml}] 217 + insert_final_newline = true 218 + indent_style = space 219 + indent_size = 2 220 + trim_trailing_whitespace = true
+8
.github/dependabot.yml
··· 1 + version: 2 2 + updates: 3 + - package-ecosystem: nuget 4 + directory: "/" 5 + schedule: 6 + interval: monthly 7 + time: "17:00" 8 + open-pull-requests-limit: 99
+54
.github/workflows/ci-nativelibs.yml
··· 1 + on: workflow_dispatch 2 + name: Continuous Integration - NativeLibs 3 + 4 + jobs: 5 + check-if-tag: 6 + name: Set Package Version 7 + runs-on: ubuntu-latest 8 + outputs: 9 + version: ${{steps.deployment.outputs.version}} 10 + steps: 11 + - name: Set Variables 12 + id: deployment 13 + shell: bash 14 + run: | 15 + if [ $(echo ${{github.ref}} | grep -q "refs/tags/"; echo $?) == 0 ]; then 16 + echo "::set-output name=VERSION::${GITHUB_REF#/refs\/tags\//}" 17 + else 18 + echo "::set-output name=VERSION::0.0.0+${{github.run_id}}" 19 + fi 20 + deploy: 21 + name: Deploy 22 + runs-on: ubuntu-latest 23 + needs: check-if-tag 24 + steps: 25 + - name: Checkout 26 + uses: actions/checkout@v2 27 + 28 + - name: Set Artifacts Directory 29 + id: artifactsPath 30 + run: echo "::set-output name=NUGET_ARTIFACTS::${{github.workspace}}/artifacts" 31 + 32 + - name: Setup .NET 5.0.x 33 + uses: actions/setup-dotnet@v1 34 + with: 35 + dotnet-version: "5.0.x" 36 + 37 + - name: Build NativeLibs 38 + run: dotnet pack -c Release osu.Framework.NativeLibs /p:Configuration=Release /p:Version=${{needs.check-if-tag.outputs.version}} /p:GenerateDocumentationFile=true -o ${{steps.artifactsPath.outputs.nuget_artifacts}} 39 + 40 + - name: Upload Artifacts 41 + uses: actions/upload-artifact@v2 42 + with: 43 + name: osu-framework-nativelibs 44 + path: ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg 45 + 46 + - name: Deploy 47 + run: | 48 + if [ $(echo ${{needs.check-if-tag.outputs.version}} | grep -q "0.0.0+${{github.run_id}}"; echo $?) == 0 ]; then 49 + echo "Skipping publish, no tag detected." 50 + exit 0; 51 + else 52 + dotnet nuget add source https://api.nuget.org/v3/index.json -n authed-nuget -u ${{secrets.NUGET_USER_NAME}} -p ${{secrets.NUGET_AUTH_TOKEN}} 53 + dotnet nuget push ${{github.workspace}}/artifacts/*.nupkg --skip-duplicate --source authed-nuget 54 + fi
+240
.github/workflows/ci.yml
··· 1 + on: [push, pull_request] 2 + name: Continuous Integration 3 + 4 + jobs: 5 + check-if-tag: 6 + name: Set Package Version 7 + runs-on: ubuntu-latest 8 + outputs: 9 + version: ${{steps.deployment.outputs.version}} 10 + steps: 11 + - name: Set Variables 12 + id: deployment 13 + shell: bash 14 + run: | 15 + if [ $(echo ${{github.ref}} | grep -q "refs/tags/"; echo $?) == 0 ]; then 16 + echo "::set-output name=VERSION::${GITHUB_REF#/refs\/tags\//}" 17 + else 18 + echo "::set-output name=VERSION::0.0.0+${{github.run_id}}" 19 + fi 20 + test: 21 + name: Test 22 + runs-on: windows-latest 23 + steps: 24 + - name: Checkout 25 + uses: actions/checkout@v2 26 + 27 + - name: Install .NET 5.0.x 28 + uses: actions/setup-dotnet@v1 29 + with: 30 + dotnet-version: "5.0.x" 31 + 32 + - name: Compile 33 + run: dotnet build -c Debug build/Desktop.proj 34 + 35 + - name: Test 36 + run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --settings $pwd/build/vstestconfig.runsettings --logger "trx;LogFileName=TestResults.trx" 37 + shell: powershell 38 + 39 + # Attempt to upload results even if test fails. 40 + # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always 41 + - name: Upload Test Results 42 + uses: actions/upload-artifact@v2 43 + if: ${{ always() }} 44 + with: 45 + name: osu-framework-test-results 46 + path: ${{github.workspace}}\TestResults\TestResults.trx 47 + 48 + inspect-code: 49 + name: Code Quality 50 + runs-on: ubuntu-latest 51 + steps: 52 + - name: Checkout 53 + uses: actions/checkout@v2 54 + 55 + # FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side. 56 + # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e 57 + - name: Install .NET 3.1.x LTS 58 + uses: actions/setup-dotnet@v1 59 + with: 60 + dotnet-version: "3.1.x" 61 + 62 + - name: Install .NET 5.0.x 63 + uses: actions/setup-dotnet@v1 64 + with: 65 + dotnet-version: "5.0.x" 66 + 67 + - name: Restore Tools 68 + run: dotnet tool restore 69 + 70 + - name: Restore Packages 71 + run: dotnet restore 72 + 73 + - name: CodeFileSanity 74 + run: | 75 + # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. 76 + # FIXME: Suppress warnings from templates project 77 + dotnet codefilesanity | while read -r line; do 78 + if [[ "$line" != *"/osu.Framework.Templates/"* ]]; then 79 + echo "::warning::$line" 80 + fi 81 + done 82 + 83 + - name: .NET Format (Dry Run) 84 + run: dotnet format --dry-run --check 85 + 86 + - name: InspectCode 87 + run: dotnet jb inspectcode $(pwd)/osu-framework.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN 88 + 89 + - name: ReSharper 90 + uses: glassechidna/resharper-action@master 91 + with: 92 + report: ${{github.workspace}}/inspectcodereport.xml 93 + 94 + pack-framework: 95 + name: Pack (Framework) 96 + runs-on: windows-latest 97 + needs: [test, inspect-code, check-if-tag] 98 + defaults: 99 + run: 100 + shell: powershell 101 + steps: 102 + - name: Checkout 103 + uses: actions/checkout@v2 104 + 105 + - name: Set Artifacts Directory 106 + id: artifactsPath 107 + run: echo "::set-output name=NUGET_ARTIFACTS::${{github.workspace}}\artifacts" 108 + 109 + # FIXME: 3.1 LTS is required here because iOS builds refuse to build without it. 110 + # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e 111 + - name: Install .NET 3.1.x LTS 112 + uses: actions/setup-dotnet@v1 113 + with: 114 + dotnet-version: "3.1.x" 115 + 116 + - name: Install .NET 5.0.x 117 + uses: actions/setup-dotnet@v1 118 + with: 119 + dotnet-version: "5.0.x" 120 + 121 + - name: Pack (Framework) 122 + run: dotnet pack -c Release osu.Framework /p:Version=${{needs.check-if-tag.outputs.version}} /p:GenerateDocumentationFile=true -o ${{steps.artifactsPath.outputs.nuget_artifacts}} 123 + 124 + - name: Upload Artifacts 125 + uses: actions/upload-artifact@v2 126 + with: 127 + name: osu-framework 128 + path: ${{steps.artifactsPath.outputs.nuget_artifacts}}\*.nupkg 129 + 130 + pack-template: 131 + name: Pack (Templates) 132 + runs-on: windows-latest 133 + needs: [test, inspect-code, check-if-tag] 134 + defaults: 135 + run: 136 + shell: powershell 137 + steps: 138 + - name: Checkout 139 + uses: actions/checkout@v2 140 + 141 + - name: Set Artifacts Directory 142 + id: artifactsPath 143 + run: echo "::set-output name=NUGET_ARTIFACTS::${{github.workspace}}\artifacts" 144 + 145 + - name: Install .NET 5.0.x 146 + uses: actions/setup-dotnet@v1 147 + with: 148 + dotnet-version: "5.0.x" 149 + 150 + - name: Pack (Template) 151 + run: dotnet pack -c Release osu.Framework.Templates /p:Configuration=Release /p:Version=${{needs.check-if-tag.outputs.version}} /p:GenerateDocumentationFile=true /p:NoDefaultExcludes=true -o ${{steps.artifactsPath.outputs.nuget_artifacts}} 152 + 153 + - name: Upload Artifacts 154 + uses: actions/upload-artifact@v2 155 + with: 156 + name: osu-framework-templates 157 + path: ${{steps.artifactsPath.outputs.nuget_artifacts}}\*.nupkg 158 + 159 + pack-android: 160 + name: Pack (Android) 161 + runs-on: windows-latest 162 + needs: [test, inspect-code, check-if-tag] 163 + defaults: 164 + run: 165 + shell: powershell 166 + steps: 167 + - name: Checkout 168 + uses: actions/checkout@v2 169 + 170 + - name: Set Artifacts Directory 171 + id: artifactsPath 172 + run: echo "::set-output name=NUGET_ARTIFACTS::${{github.workspace}}\artifacts" 173 + 174 + - name: Add msbuild to PATH 175 + uses: microsoft/setup-msbuild@v1.0.2 176 + 177 + - name: Pack (Android Framework) 178 + run: msbuild -bl:msbuildlog.binlog -v:m -target:Pack -r osu.Framework.Android/osu.Framework.Android.csproj -p:Configuration=Release -p:Version=${{needs.check-if-tag.outputs.version}} -p:PackageOutputPath=${{steps.artifactsPath.outputs.nuget_artifacts}} 179 + 180 + - name: Upload Artifacts 181 + uses: actions/upload-artifact@v2 182 + with: 183 + name: osu-framework-android 184 + path: ${{steps.artifactsPath.outputs.nuget_artifacts}}\*.nupkg 185 + 186 + pack-ios: 187 + name: Pack (iOS) 188 + runs-on: macos-latest 189 + needs: [test, inspect-code, check-if-tag] 190 + defaults: 191 + run: 192 + shell: bash 193 + steps: 194 + - name: Checkout 195 + uses: actions/checkout@v2 196 + 197 + - name: Set Artifacts Directory 198 + id: artifactsPath 199 + run: echo "::set-output name=NUGET_ARTIFACTS::${{github.workspace}}/artifacts" 200 + 201 + - name: Pack (iOS Framework) 202 + run: msbuild -bl:msbuildlog.binlog -v:m -target:Pack -r osu.Framework.iOS/osu.Framework.iOS.csproj -p:Configuration=Release -p:Version=${{needs.check-if-tag.outputs.version}} -p:PackageOutputPath=${{steps.artifactsPath.outputs.nuget_artifacts}} 203 + 204 + - name: Upload Artifacts 205 + uses: actions/upload-artifact@v2 206 + with: 207 + name: osu-framework-ios 208 + path: ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg 209 + 210 + release: 211 + name: Release 212 + runs-on: ubuntu-latest 213 + needs: [check-if-tag, pack-android, pack-framework, pack-template, pack-ios] 214 + if: ${{ github.event != 'pull_request' }} 215 + steps: 216 + - name: Create Artifact Directory 217 + run: mkdir ${{github.workspace}}/artifacts/ 218 + 219 + - name: Download Artifacts 220 + uses: actions/download-artifact@v2 221 + with: 222 + path: ${{github.workspace}}/artifacts/ 223 + 224 + # Artifacts create their own directories. Let's fix that! 225 + # https://github.com/actions/download-artifact#download-all-artifacts 226 + - name: Move Artifacts to root of subdirectory 227 + working-directory: ${{github.workspace}}/artifacts/ 228 + run: | 229 + mv -v **/*.nupkg $(pwd) 230 + rm -rfv */ 231 + 232 + - name: Deploy 233 + run: | 234 + if [ $(echo ${{needs.check-if-tag.outputs.version}} | grep -q "0.0.0+${{github.run_id}}"; echo $?) == 0 ]; then 235 + echo "Skipping publish, no tag detected." 236 + exit 0; 237 + else 238 + dotnet nuget add source https://api.nuget.org/v3/index.json -n authed-nuget -u ${{secrets.NUGET_USER_NAME}} -p ${{secrets.NUGET_AUTH_TOKEN}} 239 + dotnet nuget push ${{github.workspace}}/artifacts/*.nupkg --skip-duplicate --source authed-nuget 240 + fi
+22
.github/workflows/report-nunit.yml
··· 1 + # This is a workaround to allow PRs to report their coverage. This will run inside the base repository. 2 + # See: 3 + # * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories 4 + # * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token 5 + name: "Test Report" 6 + on: 7 + workflow_run: 8 + workflows: ["Continuous Integration"] 9 + types: 10 + - completed 11 + jobs: 12 + report: 13 + runs-on: ubuntu-latest 14 + if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} 15 + steps: 16 + - name: Continuous Integration Test Report 17 + uses: dorny/test-reporter@v1.4.2 18 + with: 19 + artifact: osu-framework-test-results 20 + name: Test Results 21 + path: "*.trx" 22 + reporter: dotnet-trx
+1 -1
osu.Framework.Benchmarks/osu.Framework.Benchmarks.csproj
··· 8 8 9 9 <ItemGroup> 10 10 <PackageReference Include="BenchmarkDotNet" Version="0.12.1" /> 11 - <PackageReference Include="nunit" Version="3.13.1" /> 11 + <PackageReference Include="nunit" Version="3.13.2" /> 12 12 <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> 13 13 <!-- The following two are unused, but resolves warning MSB3277. --> 14 14 <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
+22
osu.Framework.Templates/templates/template-empty/TemplateGame.Game.Tests/Visual/TemplateGameTestScene.cs
··· 1 + using osu.Framework.Testing; 2 + 3 + namespace TemplateGame.Game.Tests.Visual 4 + { 5 + public class TemplateGameTestScene : TestScene 6 + { 7 + protected override ITestSceneTestRunner CreateRunner() => new TemplateGameTestSceneTestRunner(); 8 + 9 + private class TemplateGameTestSceneTestRunner : TemplateGameGameBase, ITestSceneTestRunner 10 + { 11 + private TestSceneTestRunner.TestRunner runner; 12 + 13 + protected override void LoadAsyncComplete() 14 + { 15 + base.LoadAsyncComplete(); 16 + Add(runner = new TestSceneTestRunner.TestRunner()); 17 + } 18 + 19 + public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); 20 + } 21 + } 22 + }
+1 -2
osu.Framework.Templates/templates/template-empty/TemplateGame.Game.Tests/Visual/TestSceneMainScreen.cs
··· 1 1 using osu.Framework.Graphics; 2 2 using osu.Framework.Screens; 3 - using osu.Framework.Testing; 4 3 5 4 namespace TemplateGame.Game.Tests.Visual 6 5 { 7 - public class TestSceneMainScreen : TestScene 6 + public class TestSceneMainScreen : TemplateGameTestScene 8 7 { 9 8 // Add visual tests to ensure correct behaviour of your game: https://github.com/ppy/osu-framework/wiki/Development-and-Testing 10 9 // You can make changes to classes associated with the tests and they will recompile and update immediately.
+1 -2
osu.Framework.Templates/templates/template-empty/TemplateGame.Game.Tests/Visual/TestSceneSpinningBox.cs
··· 1 1 using osu.Framework.Graphics; 2 - using osu.Framework.Testing; 3 2 4 3 namespace TemplateGame.Game.Tests.Visual 5 4 { 6 - public class TestSceneSpinningBox : TestScene 5 + public class TestSceneSpinningBox : TemplateGameTestScene 7 6 { 8 7 // Add visual tests to ensure correct behaviour of your game: https://github.com/ppy/osu-framework/wiki/Development-and-Testing 9 8 // You can make changes to classes associated with the tests and they will recompile and update immediately.
+1 -2
osu.Framework.Templates/templates/template-empty/TemplateGame.Game.Tests/Visual/TestSceneTemplateGameGame.cs
··· 1 1 using osu.Framework.Allocation; 2 2 using osu.Framework.Platform; 3 - using osu.Framework.Testing; 4 3 5 4 namespace TemplateGame.Game.Tests.Visual 6 5 { 7 - public class TestSceneTemplateGameGame : TestScene 6 + public class TestSceneTemplateGameGame : TemplateGameTestScene 8 7 { 9 8 // Add visual tests to ensure correct behaviour of your game: https://github.com/ppy/osu-framework/wiki/Development-and-Testing 10 9 // You can make changes to classes associated with the tests and they will recompile and update immediately.
+1 -1
osu.Framework.Templates/templates/template-empty/TemplateGame.Game/TemplateGame.Game.csproj
··· 6 6 <ProjectReference Include="..\TemplateGame.Resources\TemplateGame.Resources.csproj" /> 7 7 </ItemGroup> 8 8 <ItemGroup> 9 - <PackageReference Include="ppy.osu.Framework" Version="2021.330.0" /> 9 + <PackageReference Include="ppy.osu.Framework" Version="2021.427.0" /> 10 10 </ItemGroup> 11 11 </Project>
+2 -2
osu.Framework.Templates/templates/template-empty/TemplateGame.iOS/TemplateGame.iOS.csproj
··· 150 150 <Reference Include="System.Net.Http" /> 151 151 </ItemGroup> 152 152 <ItemGroup Label="Package References"> 153 - <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.330.0" /> 153 + <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.427.0" /> 154 154 </ItemGroup> 155 155 <ItemGroup Label="Transitive Dependencies"> 156 - <PackageReference Include="ppy.osu.Framework" Version="2021.330.0" /> 156 + <PackageReference Include="ppy.osu.Framework" Version="2021.427.0" /> 157 157 <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.1104.0" ExcludeAssets="all" /> 158 158 </ItemGroup> 159 159 <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
+22
osu.Framework.Templates/templates/template-flappy/FlappyDon.Game.Tests/Visual/FlappyDonTestScene.cs
··· 1 + using osu.Framework.Testing; 2 + 3 + namespace FlappyDon.Game.Tests.Visual 4 + { 5 + public class FlappyDonTestScene : TestScene 6 + { 7 + protected override ITestSceneTestRunner CreateRunner() => new FlappyDonTestSceneTestRunner(); 8 + 9 + private class FlappyDonTestSceneTestRunner : FlappyDonGameBase, ITestSceneTestRunner 10 + { 11 + private TestSceneTestRunner.TestRunner runner; 12 + 13 + protected override void LoadAsyncComplete() 14 + { 15 + base.LoadAsyncComplete(); 16 + Add(runner = new TestSceneTestRunner.TestRunner()); 17 + } 18 + 19 + public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); 20 + } 21 + } 22 + }
+1 -2
osu.Framework.Templates/templates/template-flappy/FlappyDon.Game.Tests/Visual/TestSceneBackdrop.cs
··· 1 1 using FlappyDon.Game.Elements; 2 2 using osu.Framework.Allocation; 3 - using osu.Framework.Testing; 4 3 5 4 namespace FlappyDon.Game.Tests.Visual 6 5 { ··· 8 7 /// A test scene for testing the alignment 9 8 /// and placement of the sprites that make up the backdrop 10 9 /// </summary> 11 - public class TestSceneBackdrop : TestScene 10 + public class TestSceneBackdrop : FlappyDonTestScene 12 11 { 13 12 [BackgroundDependencyLoader] 14 13 private void load()
+1 -2
osu.Framework.Templates/templates/template-flappy/FlappyDon.Game.Tests/Visual/TestSceneFlappyDonGame.cs
··· 1 1 using osu.Framework.Allocation; 2 2 using osu.Framework.Platform; 3 - using osu.Framework.Testing; 4 3 5 4 namespace FlappyDon.Game.Tests.Visual 6 5 { ··· 8 7 /// A test scene wrapping the entire game, 9 8 /// including audio. 10 9 /// </summary> 11 - public class TestSceneFlappyDonGame : TestScene 10 + public class TestSceneFlappyDonGame : FlappyDonTestScene 12 11 { 13 12 private FlappyDonGame game; 14 13
+1 -2
osu.Framework.Templates/templates/template-flappy/FlappyDon.Game.Tests/Visual/TestSceneObstacles.cs
··· 1 1 using FlappyDon.Game.Elements; 2 2 using osu.Framework.Allocation; 3 3 using osu.Framework.Graphics; 4 - using osu.Framework.Testing; 5 4 6 5 namespace FlappyDon.Game.Tests.Visual 7 6 { 8 7 /// <summary> 9 8 /// A scene to test the layout and positioning and rotation of two pipe sprites. 10 9 /// </summary> 11 - public class TestSceneObstacles : TestScene 10 + public class TestSceneObstacles : FlappyDonTestScene 12 11 { 13 12 [BackgroundDependencyLoader] 14 13 private void load()
+1 -2
osu.Framework.Templates/templates/template-flappy/FlappyDon.Game.Tests/Visual/TestScenePipeObstacle.cs
··· 1 1 using FlappyDon.Game.Elements; 2 2 using osu.Framework.Allocation; 3 3 using osu.Framework.Graphics; 4 - using osu.Framework.Testing; 5 4 6 5 namespace FlappyDon.Game.Tests.Visual 7 6 { 8 7 /// <summary> 9 8 /// A scene to test the layout and positioning and rotation of two pipe sprites. 10 9 /// </summary> 11 - public class TestScenePipeObstacle : TestScene 10 + public class TestScenePipeObstacle : FlappyDonTestScene 12 11 { 13 12 [BackgroundDependencyLoader] 14 13 private void load()
+1 -1
osu.Framework.Templates/templates/template-flappy/FlappyDon.Game/FlappyDon.Game.csproj
··· 3 3 <TargetFramework>netstandard2.1</TargetFramework> 4 4 </PropertyGroup> 5 5 <ItemGroup> 6 - <PackageReference Include="ppy.osu.Framework" Version="2021.330.0" /> 6 + <PackageReference Include="ppy.osu.Framework" Version="2021.427.0" /> 7 7 </ItemGroup> 8 8 <ItemGroup> 9 9 <ProjectReference Include="..\FlappyDon.Resources\FlappyDon.Resources.csproj" />
+2 -2
osu.Framework.Templates/templates/template-flappy/FlappyDon.iOS/FlappyDon.iOS.csproj
··· 162 162 <Reference Include="System.Net.Http" /> 163 163 </ItemGroup> 164 164 <ItemGroup Label="Package References"> 165 - <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.330.0" /> 165 + <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.427.0" /> 166 166 </ItemGroup> 167 167 <ItemGroup Label="Transitive Dependencies"> 168 - <PackageReference Include="ppy.osu.Framework" Version="2021.330.0" /> 168 + <PackageReference Include="ppy.osu.Framework" Version="2021.427.0" /> 169 169 <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.1104.0" ExcludeAssets="all" /> 170 170 </ItemGroup> 171 171 <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
+1 -4
osu.Framework.Tests/Audio/AudioComponentTest.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using System.Reflection; 5 - using System.Threading; 6 5 using System.Threading.Tasks; 7 6 using NUnit.Framework; 8 7 using osu.Framework.Audio; ··· 40 39 41 40 manager?.Dispose(); 42 41 43 - Thread.Sleep(500); 44 - 45 - Assert.IsTrue(thread.Exited); 42 + AudioThreadTest.WaitForOrAssert(() => thread.Exited, "Audio thread did not exit in time"); 46 43 } 47 44 48 45 [Test]
+3 -1
osu.Framework.Tests/Audio/SampleBassTest.cs
··· 39 39 [TearDown] 40 40 public void Teardown() 41 41 { 42 - Bass.Free(); 42 + // See AudioThread.freeDevice(). 43 + if (RuntimeInfo.OS != RuntimeInfo.Platform.Linux) 44 + Bass.Free(); 43 45 } 44 46 45 47 [Test]
+19 -1
osu.Framework.Tests/Audio/TrackBassTest.cs
··· 45 45 [TearDown] 46 46 public void Teardown() 47 47 { 48 - Bass.Free(); 48 + // See AudioThread.freeDevice(). 49 + if (RuntimeInfo.OS != RuntimeInfo.Platform.Linux) 50 + Bass.Free(); 49 51 } 50 52 51 53 [Test] ··· 380 382 // assert track channel still paused regardless of frequency because it's stopped via Stop() above. 381 383 Assert.IsFalse(track.IsRunning); 382 384 Assert.AreEqual(0, track.CurrentTime); 385 + } 386 + 387 + [Test] 388 + public void TestBitrate() 389 + { 390 + Assert.Greater(track.Bitrate, 0); 391 + } 392 + 393 + [Test] 394 + public void TestCurrentTimeUpdatedAfterInlineSeek() 395 + { 396 + track.StartAsync(); 397 + updateTrack(); 398 + 399 + runOnAudioThread(() => track.Seek(20000)); 400 + Assert.That(track.CurrentTime, Is.EqualTo(20000).Within(100)); 383 401 } 384 402 385 403 private void takeEffectsAndUpdateAfter(int after)
+11 -1
osu.Framework.Tests/Audio/TrackVirtualTest.cs
··· 21 21 [SetUp] 22 22 public void Setup() 23 23 { 24 - track = new TrackVirtual(10000); 24 + track = new TrackVirtual(60000); 25 25 updateTrack(); 26 26 } 27 27 ··· 251 251 Assert.AreEqual(1.5, track.Rate); 252 252 253 253 testPlaybackRate(1.5); 254 + } 255 + 256 + [Test] 257 + public void TestCurrentTimeUpdatedAfterInlineSeek() 258 + { 259 + track.Start(); 260 + updateTrack(); 261 + 262 + RunOnAudioThread(() => track.Seek(20000)); 263 + Assert.That(track.CurrentTime, Is.EqualTo(20000).Within(100)); 254 264 } 255 265 256 266 private void testPlaybackRate(double expectedRate)
+22 -37
osu.Framework.Tests/Bindables/BindableEnumTest.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using System; 5 + using System.Linq; 4 6 using NUnit.Framework; 5 7 using osu.Framework.Bindables; 6 8 ··· 19 21 Assert.AreEqual(value, bindable.Value); 20 22 } 21 23 22 - [TestCase("Value1", TestEnum.Value1)] 23 - [TestCase("Value2", TestEnum.Value2)] 24 - [TestCase("-1", TestEnum.Value1 - 1)] 25 - [TestCase("2", TestEnum.Value2 + 1)] 26 - public void TestParsingString(string value, TestEnum expected) 24 + [TestCase(TestEnum.Value1, "Value1", 0, 0f, 0d, 0L, (short)0, (sbyte)0)] 25 + [TestCase(TestEnum.Value2, "Value2", 1, 1f, 1d, 1L, (short)1, (sbyte)1)] 26 + [TestCase(TestEnum.Value1 - 1, "-1", -1, -1f, -1d, -1L, (short)-1, (sbyte)-1)] 27 + [TestCase(TestEnum.Value2 + 1, "2", 2, 2f, 2d, 2L, (short)2, (sbyte)2)] 28 + public void TestParsing(TestEnum expected, params object[] values) 27 29 { 28 30 var bindable = new Bindable<TestEnum>(); 29 - bindable.Parse(value); 31 + var nullable = new Bindable<TestEnum?>(); 30 32 31 - Assert.AreEqual(expected, bindable.Value); 32 - } 33 + foreach (var value in values.Append(expected)) 34 + { 35 + bindable.Parse(value); 36 + nullable.Parse(value); 33 37 34 - [TestCase("Value1", TestEnum.Value1)] 35 - [TestCase("Value2", TestEnum.Value2)] 36 - [TestCase("-1", TestEnum.Value1 - 1)] 37 - [TestCase("2", TestEnum.Value2 + 1)] 38 - public void TestParsingStringToNullableType(string value, TestEnum? expected) 39 - { 40 - var bindable = new Bindable<TestEnum?>(); 41 - bindable.Parse(value); 42 - 43 - Assert.AreEqual(expected, bindable.Value); 38 + Assert.AreEqual(expected, bindable.Value); 39 + Assert.AreEqual(expected, nullable.Value); 40 + } 44 41 } 45 42 46 - [TestCase(TestEnum.Value1)] 47 - [TestCase(TestEnum.Value2)] 48 - [TestCase(TestEnum.Value1 - 1)] 49 - [TestCase(TestEnum.Value2 + 1)] 50 - public void TestParsingEnum(TestEnum value) 43 + [TestCase(1.1f)] 44 + [TestCase("Not a value")] 45 + [TestCase("")] 46 + public void TestUnparsaebles(object value) 51 47 { 52 48 var bindable = new Bindable<TestEnum>(); 53 - bindable.Parse(value); 49 + var nullable = new Bindable<TestEnum?>(); 54 50 55 - Assert.AreEqual(value, bindable.Value); 56 - } 57 - 58 - [TestCase(TestEnum.Value1)] 59 - [TestCase(TestEnum.Value2)] 60 - [TestCase(TestEnum.Value1 - 1)] 61 - [TestCase(TestEnum.Value2 + 1)] 62 - public void TestParsingEnumToNullableType(TestEnum value) 63 - { 64 - var bindable = new Bindable<TestEnum?>(); 65 - bindable.Parse(value); 66 - 67 - Assert.AreEqual(value, bindable.Value); 51 + Assert.Throws<ArgumentException>(() => bindable.Parse(value)); 52 + Assert.Throws<ArgumentException>(() => nullable.Parse(value)); 68 53 } 69 54 70 55 public enum TestEnum
+2 -2
osu.Framework.Tests/Bindables/BindableTest.cs
··· 96 96 [TestCaseSource(nameof(getParsingConversionTests))] 97 97 public void TestParse(Type type, object input, object output) 98 98 { 99 - IBindable bindable = (IBindable)Activator.CreateInstance(typeof(Bindable<>).MakeGenericType(type), type == typeof(string) ? "" : Activator.CreateInstance(type)); 99 + object bindable = Activator.CreateInstance(typeof(Bindable<>).MakeGenericType(type), type == typeof(string) ? "" : Activator.CreateInstance(type)); 100 100 Debug.Assert(bindable != null); 101 101 102 - bindable.Parse(input); 102 + ((IParseable)bindable).Parse(input); 103 103 object value = bindable.GetType().GetProperty(nameof(Bindable<object>.Value), BindingFlags.Public | BindingFlags.Instance)?.GetValue(bindable); 104 104 105 105 Assert.That(value, Is.EqualTo(output));
+22
osu.Framework.Tests/Containers/TestSceneLoadComponentAsync.cs
··· 107 107 && composite.LoadedChildren.First() == composite.AsyncChild1); 108 108 } 109 109 110 + [Test] 111 + public void TestScheduleDuringAsyncLoad() 112 + { 113 + TestLoadBlockingDrawable composite = null; 114 + 115 + bool scheduleRun = false; 116 + 117 + AddStep("Async load drawable", () => 118 + { 119 + LoadComponentAsync(composite = new TestLoadBlockingDrawable(), d => Child = d); 120 + }); 121 + 122 + AddStep("Attempt to schedule on child 1", () => 123 + { 124 + composite.Schedule(() => scheduleRun = true); 125 + }); 126 + 127 + AddStep("Allow child 1 load", () => composite.AllowLoad.Set()); 128 + 129 + AddUntilStep("Scheduled content run", () => scheduleRun); 130 + } 131 + 110 132 private class AsyncChildrenLoadingComposite : CompositeDrawable 111 133 { 112 134 public IEnumerable<TestLoadBlockingDrawable> LoadedChildren;
+28
osu.Framework.Tests/Extensions/TestExtensions.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using NUnit.Framework; 5 + using osu.Framework.Extensions; 6 + 7 + namespace osu.Framework.Tests.Extensions 8 + { 9 + [TestFixture] 10 + public class TestExtensions 11 + { 12 + [TestCase(TestEnum.Value1, "Value1")] 13 + [TestCase(TestEnum.Value2, "V2")] 14 + [TestCase((TestEnum)3, "3")] 15 + public void TestGetDescription(TestEnum enumValue, string expected) 16 + { 17 + Assert.That(enumValue.GetDescription(), Is.EqualTo(expected)); 18 + } 19 + 20 + public enum TestEnum 21 + { 22 + Value1, 23 + 24 + [System.ComponentModel.Description("V2")] 25 + Value2, 26 + } 27 + } 28 + }
+54
osu.Framework.Tests/Input/KeyCombinationModifierTest.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using NUnit.Framework; 5 + using osu.Framework.Input.Bindings; 6 + 7 + namespace osu.Framework.Tests.Input 8 + { 9 + [TestFixture] 10 + public class KeyCombinationModifierTest 11 + { 12 + private static readonly object[][] key_combination_display_test_cases = 13 + { 14 + // test single combination matches. 15 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Any, true }, 16 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true }, 17 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Any, true }, 18 + new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, false }, 19 + new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true }, 20 + 21 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Exact, true }, 22 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, 23 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Exact, true }, 24 + new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, false }, 25 + new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, 26 + 27 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true }, 28 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, 29 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Modifiers, true }, 30 + new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false }, 31 + new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, 32 + 33 + // test multiple combination matches. 34 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Any, true }, 35 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true }, 36 + new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, false }, 37 + new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true }, 38 + 39 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Exact, true }, 40 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, 41 + new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, false }, 42 + new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, 43 + 44 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true }, 45 + new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, 46 + new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false }, 47 + new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, 48 + }; 49 + 50 + [TestCaseSource(nameof(key_combination_display_test_cases))] 51 + public void TestLeftRightModifierHandling(KeyCombination candidate, KeyCombination pressed, KeyCombinationMatchingMode matchingMode, bool shouldContain) 52 + => Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, matchingMode)); 53 + } 54 + }
+8 -1
osu.Framework.Tests/Input/KeyCombinationTest.cs
··· 14 14 new object[] { new KeyCombination(InputKey.Alt, InputKey.F4), "Alt-F4" }, 15 15 new object[] { new KeyCombination(InputKey.D, InputKey.Control), "Ctrl-D" }, 16 16 new object[] { new KeyCombination(InputKey.Shift, InputKey.F, InputKey.Control), "Ctrl-Shift-F" }, 17 - new object[] { new KeyCombination(InputKey.Alt, InputKey.Control, InputKey.Super, InputKey.Shift), "Ctrl-Alt-Shift-Win" } 17 + new object[] { new KeyCombination(InputKey.Alt, InputKey.Control, InputKey.Super, InputKey.Shift), "Ctrl-Alt-Shift-Win" }, 18 + new object[] { new KeyCombination(InputKey.LAlt, InputKey.F4), "LAlt-F4" }, 19 + new object[] { new KeyCombination(InputKey.D, InputKey.LControl), "LCtrl-D" }, 20 + new object[] { new KeyCombination(InputKey.LShift, InputKey.F, InputKey.LControl), "LCtrl-LShift-F" }, 21 + new object[] { new KeyCombination(InputKey.LAlt, InputKey.LControl, InputKey.LSuper, InputKey.LShift), "LCtrl-LAlt-LShift-LWin" }, 22 + new object[] { new KeyCombination(InputKey.Alt, InputKey.LAlt, InputKey.RControl, InputKey.A), "RCtrl-LAlt-A" }, 23 + new object[] { new KeyCombination(InputKey.Shift, InputKey.LControl, InputKey.X), "LCtrl-Shift-X" }, 24 + new object[] { new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Alt, InputKey.Super, InputKey.LAlt, InputKey.RShift, InputKey.LSuper), "Ctrl-LAlt-RShift-LWin" }, 18 25 }; 19 26 20 27 [TestCaseSource(nameof(key_combination_display_test_cases))]
+13 -5
osu.Framework.Tests/Visual/Containers/TestSceneCursorContainer.cs
··· 49 49 cursorContainer.ActiveCursor.ScreenSpaceDrawQuad.Centre, 50 50 container.ScreenSpaceDrawQuad.Centre); 51 51 52 + bool cursorAtMouseScreenSpace() => 53 + Precision.AlmostEquals( 54 + cursorContainer.ActiveCursor.ScreenSpaceDrawQuad.Centre, 55 + InputManager.CurrentState.Mouse.Position); 56 + 52 57 createContent(); 53 58 AddStep("Move cursor to centre", () => InputManager.MoveMouseTo(container.ScreenSpaceDrawQuad.Centre)); 54 - AddAssert("cursor is centered", cursorCenteredInContainer); 59 + AddAssert("cursor is centered", () => cursorCenteredInContainer()); 55 60 AddStep("Move container", () => container.Y += 50); 56 - AddAssert("cursor is still centered", cursorCenteredInContainer); 61 + AddAssert("cursor no longer centered", () => !cursorCenteredInContainer()); 62 + AddAssert("cursor at mouse position", () => cursorAtMouseScreenSpace()); 57 63 AddStep("Resize container", () => container.Size *= new Vector2(1.4f, 1)); 58 - AddAssert("cursor is still centered", cursorCenteredInContainer); 64 + AddAssert("cursor at mouse position", () => cursorAtMouseScreenSpace()); 59 65 60 66 // ensure positional updates work 61 67 AddStep("Move cursor to centre", () => InputManager.MoveMouseTo(container.ScreenSpaceDrawQuad.Centre)); 62 - AddAssert("cursor is still centered", cursorCenteredInContainer); 68 + AddAssert("cursor is not centered", () => cursorCenteredInContainer()); 69 + AddAssert("cursor at mouse position", () => cursorAtMouseScreenSpace()); 63 70 64 71 // ensure we received the mouse position update from IRequireHighFrequencyMousePosition 65 72 AddStep("Move cursor to test centre", () => InputManager.MoveMouseTo(Content.ScreenSpaceDrawQuad.Centre)); 66 73 AddStep("Recreate container with mouse already in place", createContent); 67 - AddAssert("cursor is centered", cursorCenteredInContainer); 74 + AddAssert("cursor is centered", () => cursorCenteredInContainer()); 75 + AddAssert("cursor at mouse position", () => cursorAtMouseScreenSpace()); 68 76 } 69 77 70 78 private class TestCursorContainer : CursorContainer
+3 -3
osu.Framework.Tests/Visual/Drawables/TestSceneTransformSequence.cs
··· 121 121 private void animate() 122 122 { 123 123 boxes[0].Delay(500).Then(500).Then(500).Then( 124 - b => b.Delay(500).Spin(1000, RotationDirection.CounterClockwise) 124 + b => b.Delay(500).Spin(1000, RotationDirection.Counterclockwise) 125 125 ); 126 126 127 - boxes[1].Spin(1000, RotationDirection.CounterClockwise); 127 + boxes[1].Spin(1000, RotationDirection.Counterclockwise); 128 128 129 - boxes[2].Delay(-2000).Spin(1000, RotationDirection.CounterClockwise); 129 + boxes[2].Delay(-2000).Spin(1000, RotationDirection.Counterclockwise); 130 130 131 131 boxes[3].RotateTo(90) 132 132 .Then().Delay(1000).RotateTo(0)
+75 -29
osu.Framework.Tests/Visual/Input/TestSceneKeyBindingsGrid.cs
··· 197 197 198 198 toggleKey(Key.ShiftLeft); 199 199 toggleKey(Key.AltLeft); 200 - checkPressed(TestAction.Alt_and_Shift, 1, 1, 1, 1, 1); 200 + checkPressed(TestAction.Alt_and_LShift, 1, 1, 1, 1, 1); 201 201 toggleKey(Key.A); 202 - checkPressed(TestAction.Shift_A, 1, 0, 0, 1, 1); 202 + checkPressed(TestAction.LShift_A, 1, 0, 0, 1, 1); 203 203 toggleKey(Key.AltLeft); 204 204 toggleKey(Key.ShiftLeft); 205 205 }); 206 206 } 207 207 208 208 [Test] 209 - public void ModifierKeys() 209 + public void PerSideModifierKeys() 210 210 { 211 211 wrapTest(() => 212 212 { 213 213 toggleKey(Key.ShiftLeft); 214 - checkPressed(TestAction.Shift, 1, 1, 1, 1, 1); 214 + checkPressed(TestAction.AnyShift, 1, 1, 1, 1, 1); 215 + checkPressed(TestAction.LShift, 0, 0, 0, 1, 1); 216 + checkPressed(TestAction.RShift, 0, 0, 0, 0, 0); 217 + 215 218 toggleKey(Key.A); 216 - checkReleased(TestAction.Shift, 1, 1, 1, 0, 0); 217 - checkPressed(TestAction.Shift_A, 1, 1, 1, 1, 1); 219 + checkPressed(TestAction.AnyShift_A, 0, 0, 0, 1, 1); 220 + checkPressed(TestAction.LShift_A, 1, 1, 1, 1, 1); 221 + checkReleased(TestAction.AnyShift, 1, 1, 1, 0, 0); 222 + 218 223 toggleKey(Key.ShiftRight); 219 - checkPressed(TestAction.Shift, 0, 0, 0, 0, 0); 220 - checkReleased(TestAction.Shift_A, 0, 0, 0, 0, 0); 224 + checkReleased(TestAction.LShift_A, 1, 0, 0, 0, 0); 225 + checkPressed(TestAction.RShift, 1, 0, 0, 1, 1); 226 + checkPressed(TestAction.AnyShift, 0, 0, 0, 0, 0); 227 + 221 228 toggleKey(Key.ShiftLeft); 222 - checkReleased(TestAction.Shift, 0, 0, 0, 0, 0); 223 - checkReleased(TestAction.Shift_A, 0, 0, 0, 0, 0); 229 + checkReleased(TestAction.LShift, 0, 0, 0, 1, 1); 230 + checkReleased(TestAction.LShift_A, 0, 1, 1, 1, 1); 231 + checkReleased(TestAction.AnyShift, 0, 0, 0, 0, 0); 232 + 224 233 toggleKey(Key.ShiftRight); 225 - checkReleased(TestAction.Shift, 0, 0, 0, 1, 1); 226 - checkReleased(TestAction.Shift_A, 1, 1, 1, 1, 1); 234 + checkReleased(TestAction.AnyShift, 0, 0, 0, 1, 1); 235 + }); 236 + } 237 + 238 + [Test] 239 + public void BothSideModifierKeys() 240 + { 241 + wrapTest(() => 242 + { 243 + toggleKey(Key.AltLeft); 244 + checkPressed(TestAction.Alt, 1, 1, 1, 1, 1); 245 + toggleKey(Key.A); 246 + checkReleased(TestAction.Alt, 1, 1, 1, 0, 0); 247 + checkPressed(TestAction.Alt_A, 1, 1, 1, 1, 1); 248 + toggleKey(Key.AltRight); 249 + checkPressed(TestAction.Alt, 0, 0, 0, 0, 0); 250 + checkReleased(TestAction.Alt_A, 0, 0, 0, 0, 0); 251 + toggleKey(Key.AltLeft); 252 + checkReleased(TestAction.Alt, 0, 0, 0, 0, 0); 253 + checkReleased(TestAction.Alt_A, 0, 0, 0, 0, 0); 254 + toggleKey(Key.AltRight); 255 + checkReleased(TestAction.Alt, 0, 0, 0, 1, 1); 256 + checkReleased(TestAction.Alt_A, 1, 1, 1, 1, 1); 227 257 toggleKey(Key.A); 228 258 229 259 toggleKey(Key.ControlLeft); 230 - toggleKey(Key.ShiftLeft); 231 - checkPressed(TestAction.Ctrl_and_Shift, 1, 1, 1, 1, 1); 260 + toggleKey(Key.AltLeft); 261 + checkPressed(TestAction.Ctrl_and_Alt, 1, 1, 1, 1, 1); 232 262 }); 233 263 } 234 264 ··· 339 369 Alt_A, 340 370 Alt_S, 341 371 Alt_D_or_F, 342 - Shift_A, 343 - Shift_S, 344 - Shift_D_or_F, 372 + LShift_A, 373 + LShift_S, 374 + LShift_D_or_F, 375 + RShift_A, 376 + RShift_S, 377 + RShift_D_or_F, 345 378 Ctrl_Shift_A, 346 379 Ctrl_Shift_S, 347 380 Ctrl_Shift_D_or_F, 348 381 Ctrl, 349 - Shift, 382 + RShift, 383 + LShift, 350 384 Alt, 351 - Alt_and_Shift, 352 - Ctrl_and_Shift, 385 + Alt_and_LShift, 386 + Ctrl_and_Alt, 353 387 Ctrl_or_Shift, 354 388 LeftMouse, 355 389 RightMouse, ··· 358 392 WheelLeft, 359 393 WheelRight, 360 394 Ctrl_and_WheelUp, 395 + AnyShift_A, 396 + AnyShift 361 397 } 362 398 363 399 private class TestInputManager : KeyBindingContainer<TestAction> ··· 379 415 new KeyBinding(new[] { InputKey.Control, InputKey.D }, TestAction.Ctrl_D_or_F), 380 416 new KeyBinding(new[] { InputKey.Control, InputKey.F }, TestAction.Ctrl_D_or_F), 381 417 382 - new KeyBinding(new[] { InputKey.Shift, InputKey.A }, TestAction.Shift_A), 383 - new KeyBinding(new[] { InputKey.Shift, InputKey.S }, TestAction.Shift_S), 384 - new KeyBinding(new[] { InputKey.Shift, InputKey.D }, TestAction.Shift_D_or_F), 385 - new KeyBinding(new[] { InputKey.Shift, InputKey.F }, TestAction.Shift_D_or_F), 418 + new KeyBinding(new[] { InputKey.LShift, InputKey.A }, TestAction.LShift_A), 419 + new KeyBinding(new[] { InputKey.LShift, InputKey.S }, TestAction.LShift_S), 420 + new KeyBinding(new[] { InputKey.LShift, InputKey.D }, TestAction.LShift_D_or_F), 421 + new KeyBinding(new[] { InputKey.LShift, InputKey.F }, TestAction.LShift_D_or_F), 422 + 423 + new KeyBinding(new[] { InputKey.RShift, InputKey.A }, TestAction.RShift_A), 424 + new KeyBinding(new[] { InputKey.RShift, InputKey.S }, TestAction.RShift_S), 425 + new KeyBinding(new[] { InputKey.RShift, InputKey.D }, TestAction.RShift_D_or_F), 426 + new KeyBinding(new[] { InputKey.RShift, InputKey.F }, TestAction.RShift_D_or_F), 427 + 428 + new KeyBinding(new[] { InputKey.Shift, InputKey.A }, TestAction.AnyShift_A), 386 429 387 430 new KeyBinding(new[] { InputKey.Alt, InputKey.A }, TestAction.Alt_A), 388 431 new KeyBinding(new[] { InputKey.Alt, InputKey.S }, TestAction.Alt_S), ··· 395 438 new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, TestAction.Ctrl_Shift_D_or_F), 396 439 397 440 new KeyBinding(new[] { InputKey.Control }, TestAction.Ctrl), 398 - new KeyBinding(new[] { InputKey.Shift }, TestAction.Shift), 441 + new KeyBinding(new[] { InputKey.Shift }, TestAction.AnyShift), 442 + new KeyBinding(new[] { InputKey.LShift }, TestAction.LShift), 443 + new KeyBinding(new[] { InputKey.RShift }, TestAction.RShift), 399 444 new KeyBinding(new[] { InputKey.Alt }, TestAction.Alt), 400 - new KeyBinding(new[] { InputKey.Alt, InputKey.Shift }, TestAction.Alt_and_Shift), 401 - new KeyBinding(new[] { InputKey.Control, InputKey.Shift }, TestAction.Ctrl_and_Shift), 445 + new KeyBinding(new[] { InputKey.Alt, InputKey.LShift }, TestAction.Alt_and_LShift), 446 + new KeyBinding(new[] { InputKey.Control, InputKey.Alt }, TestAction.Ctrl_and_Alt), 402 447 new KeyBinding(new[] { InputKey.Control }, TestAction.Ctrl_or_Shift), 403 448 new KeyBinding(new[] { InputKey.Shift }, TestAction.Ctrl_or_Shift), 404 449 ··· 524 569 if (Action == action) 525 570 { 526 571 ++OnReleasedCount; 572 + 527 573 if (Concurrency != SimultaneousBindingMode.All) 528 574 Trace.Assert(OnPressedCount == OnReleasedCount); 529 575 else ··· 554 600 { 555 601 if (t.ToString().Contains("Wheel")) 556 602 return new ScrollTestButton(t, concurrency); 557 - else 558 - return new TestButton(t, concurrency); 603 + 604 + return new TestButton(t, concurrency); 559 605 }).ToArray(); 560 606 561 607 Children = new Drawable[]
+20
osu.Framework.Tests/Visual/Input/TestSceneMidi.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using System; 5 + using System.Linq; 6 + using osu.Framework.Allocation; 5 7 using osu.Framework.Graphics; 6 8 using osu.Framework.Graphics.Containers; 7 9 using osu.Framework.Graphics.Shapes; 8 10 using osu.Framework.Graphics.Sprites; 9 11 using osu.Framework.Input; 10 12 using osu.Framework.Input.Events; 13 + using osu.Framework.Input.Handlers.Midi; 14 + using osu.Framework.Platform; 11 15 using osuTK; 12 16 using osuTK.Graphics; 13 17 ··· 27 31 keyFlow.Add(new MidiKeyHandler(k)); 28 32 29 33 Child = keyFlow; 34 + } 35 + 36 + [Resolved] 37 + private GameHost host { get; set; } 38 + 39 + protected override void LoadComplete() 40 + { 41 + base.LoadComplete(); 42 + 43 + AddToggleStep("toggle midi handler", enabled => 44 + { 45 + var midiHandler = host.AvailableInputHandlers.OfType<MidiHandler>().FirstOrDefault(); 46 + 47 + if (midiHandler != null) 48 + midiHandler.Enabled.Value = enabled; 49 + }); 30 50 } 31 51 32 52 protected override bool OnMidiDown(MidiDownEvent e)
+2
osu.Framework.Tests/Visual/Input/TestSceneTabletInput.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #if NET5_0 4 5 using System.Linq; 5 6 using osu.Framework.Allocation; 6 7 using osu.Framework.Graphics; ··· 160 161 } 161 162 } 162 163 } 164 + #endif
+24
osu.Framework.Tests/Visual/Layout/TestSceneGridContainer.cs
··· 50 50 }; 51 51 }); 52 52 53 + [TestCase(false)] 54 + [TestCase(true)] 55 + public void TestAutoSizeDoesNotConsiderRelativeSizeChildren(bool row) 56 + { 57 + Box relativeBox = null; 58 + Box absoluteBox = null; 59 + 60 + setSingleDimensionContent(() => new[] 61 + { 62 + new Drawable[] 63 + { 64 + relativeBox = new FillBox { RelativeSizeAxes = Axes.Both }, 65 + absoluteBox = new FillBox 66 + { 67 + RelativeSizeAxes = Axes.None, 68 + Size = new Vector2(100) 69 + } 70 + } 71 + }, new[] { new Dimension(GridSizeMode.AutoSize) }, row); 72 + 73 + AddStep("resize absolute box", () => absoluteBox.Size = new Vector2(50)); 74 + AddAssert("relative box has length 50", () => Precision.AlmostEquals(row ? relativeBox.DrawHeight : relativeBox.DrawWidth, 50, 1)); 75 + } 76 + 53 77 [Test] 54 78 public void TestBlankGrid() 55 79 {
+1
osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs
··· 164 164 165 165 // borderless alignment tests 166 166 AddStep("switch to borderless", () => windowMode.Value = WindowMode.Borderless); 167 + AddAssert("check window position", () => new Point(window.Position.X + 1, window.Position.Y + 1) == display.Bounds.Location); 167 168 AddAssert("check window size", () => new Size(window.Size.Width - 1, window.Size.Height - 1) == display.Bounds.Size, desc2); 168 169 AddAssert("check current screen", () => window.CurrentDisplayBindable.Value.Index == display.Index); 169 170
+1
osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs
··· 103 103 if (window.SupportedWindowModes.Contains(WindowMode.Fullscreen)) 104 104 { 105 105 AddStep("change to fullscreen", () => windowMode.Value = WindowMode.Fullscreen); 106 + AddAssert("window position updated", () => ((SDL2DesktopWindow)window).Position == new Point(0, 0)); 106 107 testResolution(1920, 1080); 107 108 testResolution(1280, 960); 108 109 testResolution(9999, 9999);
+13 -5
osu.Framework.Tests/Visual/UserInterface/TestSceneFocusedOverlayContainer.cs
··· 47 47 AddAssert("not visible", () => overlayContainer.State.Value == Visibility.Hidden); 48 48 } 49 49 50 - [Test] 51 - public void TestScrollBlocking() 50 + [TestCase(true)] 51 + [TestCase(false)] 52 + public void TestScrollBlocking(bool isBlocking) 52 53 { 53 54 AddStep("create container", () => 54 55 { ··· 57 58 RelativeSizeAxes = Axes.Both, 58 59 Children = new Drawable[] 59 60 { 60 - overlayContainer = new TestFocusedOverlayContainer() 61 + overlayContainer = new TestFocusedOverlayContainer(blockScrollInput: isBlocking) 61 62 } 62 63 }; 63 64 }); ··· 75 76 InputManager.ScrollVerticalBy(1); 76 77 }); 77 78 78 - AddAssert("scroll not received by parent", () => parentContainer.ScrollReceived == initialScrollCount); 79 + if (isBlocking) 80 + AddAssert("scroll not received by parent", () => parentContainer.ScrollReceived == initialScrollCount); 81 + else 82 + AddAssert("scroll received by parent", () => parentContainer.ScrollReceived == ++initialScrollCount); 79 83 80 84 AddStep("scroll outside", () => 81 85 { ··· 94 98 95 99 protected override bool BlockNonPositionalInput => false; 96 100 97 - public TestFocusedOverlayContainer(bool startHidden = true) 101 + protected override bool BlockScrollInput { get; } 102 + 103 + public TestFocusedOverlayContainer(bool startHidden = true, bool blockScrollInput = true) 98 104 { 105 + BlockScrollInput = blockScrollInput; 106 + 99 107 StartHidden = startHidden; 100 108 101 109 Size = new Vector2(0.5f);
+1 -1
osu.Framework.Tests/osu.Framework.Tests.csproj
··· 11 11 </ItemGroup> 12 12 <ItemGroup Label="Package References"> 13 13 <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> 14 - <PackageReference Include="NUnit" Version="3.13.1" /> 14 + <PackageReference Include="NUnit" Version="3.13.2" /> 15 15 <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> 16 16 <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> 17 17 </ItemGroup>
+2 -2
osu.Framework.iOS.props
··· 69 69 <PackageReference Include="ppy.osuTK.NS20" Version="1.0.173" /> 70 70 <PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" /> 71 71 <PackageReference Include="ppy.Microsoft.Diagnostics.Runtime" Version="0.9.180305.1" /> 72 - <PackageReference Include="NUnit" Version="3.12.0" /> 72 + <PackageReference Include="NUnit" Version="3.13.2" /> 73 73 <PackageReference Include="System.Net.Http" Version="4.3.4" /> 74 74 <PackageReference Include="ManagedBass" Version="3.1.0"/> 75 75 <PackageReference Include="ManagedBass.Fx" Version="2.0.1"/> ··· 80 80 <PackageReference Include="JetBrains.Annotations" Version="2020.3.0" /> 81 81 82 82 <!-- Manually downgrade for 4.5.1-4.5.3. Should be removed when 4.6.0 released. --> 83 - <PackageReference Include="System.Memory" Version="4.5.0" NoWarn="NU1605" /> 83 + <PackageReference Include="System.Memory" Version="4.5.4" NoWarn="NU1605" /> 84 84 </ItemGroup> 85 85 </Project>
+16 -28
osu.Framework/Audio/AudioManager.cs
··· 105 105 private readonly Lazy<TrackStore> globalTrackStore; 106 106 private readonly Lazy<SampleStore> globalSampleStore; 107 107 108 - private bool didInitialise; 109 - 110 108 /// <summary> 111 109 /// Constructs an AudioStore given a track resource store, and a sample resource store. 112 110 /// </summary> ··· 157 155 { 158 156 } 159 157 } 160 - }) 161 - { 162 - IsBackground = true 163 - }.Start(); 158 + }) { IsBackground = true }.Start(); 164 159 }); 165 160 } 166 161 167 162 protected override void Dispose(bool disposing) 168 163 { 169 164 cancelSource.Cancel(); 165 + 170 166 thread.UnregisterManager(this); 171 167 172 168 OnNewDevice = null; 173 169 OnLostDevice = null; 174 - 175 - FreeBass(); 176 170 177 171 base.Dispose(disposing); 178 172 } ··· 186 180 { 187 181 scheduler.Add(() => 188 182 { 183 + if (cancelSource.IsCancellationRequested) 184 + return; 185 + 189 186 if (!IsCurrentDeviceValid()) 190 187 setAudioDevice(); 191 188 }); ··· 258 255 259 256 // initialize new device 260 257 bool initSuccess = InitBass(deviceIndex); 261 - 262 - if (Bass.LastError == Errors.Already) 263 - { 264 - // We check if the initialization error is that we already initialized the device 265 - // If it is, it means we can just tell Bass to use the already initialized device without much 266 - // other fuzz. 267 - Bass.CurrentDevice = deviceIndex; 268 - FreeBass(); 269 - initSuccess = InitBass(deviceIndex); 270 - } 271 - 272 - if (BassUtils.CheckFaulted(false)) 258 + if (Bass.LastError != Errors.Already && BassUtils.CheckFaulted(false)) 273 259 return false; 274 260 275 261 if (!initSuccess) ··· 326 312 // ensure there are no brief delays on audio operations (causing stream STALLs etc.) after periods of silence. 327 313 Bass.Configure(ManagedBass.Configuration.DevNonStop, true); 328 314 329 - didInitialise = true; 315 + bool didInit = Bass.Init(device); 330 316 331 - return Bass.Init(device); 332 - } 317 + // If the device was already initialised, the device can be used without much fuss. 318 + if (Bass.LastError == Errors.Already) 319 + { 320 + Bass.CurrentDevice = device; 321 + didInit = true; 322 + } 333 323 334 - protected void FreeBass() 335 - { 336 - if (!didInitialise) return; 324 + if (didInit) 325 + thread.RegisterInitialisedDevice(device); 337 326 338 - Bass.Free(); 339 - didInitialise = false; 327 + return didInit; 340 328 } 341 329 342 330 private void syncAudioDevices()
+18 -4
osu.Framework/Audio/Track/TrackBass.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using System; 5 + using System.Diagnostics; 5 6 using System.IO; 6 7 using System.Threading; 7 8 using ManagedBass; ··· 110 111 // Bass does not allow seeking to the end of the track, so the last available position is 1 sample before. 111 112 lastSeekablePosition = Bass.ChannelBytes2Seconds(activeStream, byteLength - BYTES_PER_SAMPLE) * 1000; 112 113 113 - bitrate = (int)Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Bitrate); 114 - 115 114 stopCallback = new SyncCallback((a, b, c, d) => RaiseFailed()); 116 115 endCallback = new SyncCallback((a, b, c, d) => 117 116 { ··· 157 156 158 157 BassFlags flags = Preview ? 0 : BassFlags.Decode | BassFlags.Prescan; 159 158 int stream = Bass.CreateStream(StreamSystem.NoBuffer, flags, fileCallbacks.Callbacks, fileCallbacks.Handle); 159 + 160 + bitrate = (int)Math.Round(Bass.ChannelGetAttribute(stream, ChannelAttribute.Bitrate)); 160 161 161 162 if (!Preview) 162 163 { ··· 210 211 base.UpdateState(); 211 212 212 213 var running = isRunningState(Bass.ChannelIsActive(activeStream)); 213 - var bytePosition = Bass.ChannelGetPosition(activeStream); 214 214 215 215 // because device validity check isn't done frequently, when switching to "No sound" device, 216 216 // there will be a brief time where this track will be stopped, before we resume it manually (see comments in UpdateDevice(int).) 217 217 // this makes us appear to be playing, even if we may not be. 218 218 isRunning = /*running ||*/ (isPlayed && !hasCompleted); 219 219 220 - Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, bytePosition) * 1000); 220 + updateCurrentTime(); 221 221 222 222 bassAmplitudeProcessor?.Update(); 223 223 } ··· 351 351 352 352 if (pos != mixer.GetChannelPosition(activeStream)) 353 353 mixer.SetChannelPosition(activeStream, pos); 354 + 355 + // current time updates are safe to perform from enqueued actions, 356 + // but not always safe to perform from BASS callbacks, since those can sometimes use a separate thread. 357 + // if it's not safe to update immediately here, the next UpdateState() call is guaranteed to update the time safely anyway. 358 + if (CanPerformInline) 359 + updateCurrentTime(); 360 + } 361 + 362 + private void updateCurrentTime() 363 + { 364 + Debug.Assert(CanPerformInline); 365 + 366 + var bytePosition = mixer.GetChannelPosition(activeStream); 367 + Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, bytePosition) * 1000); 354 368 } 355 369 356 370 private double currentTime;
+6 -6
osu.Framework/Bindables/Bindable.cs
··· 19 19 /// A generic implementation of a <see cref="IBindable"/> 20 20 /// </summary> 21 21 /// <typeparam name="T">The type of our stored <see cref="Value"/>.</typeparam> 22 - public class Bindable<T> : IBindable<T>, ISerializableBindable 22 + public class Bindable<T> : IBindable<T>, IBindable, IParseable, ISerializableBindable 23 23 { 24 24 /// <summary> 25 25 /// An event which is raised when <see cref="Value"/> has changed (or manually via <see cref="TriggerValueChange"/>). ··· 252 252 Value = bindable.Value; 253 253 break; 254 254 255 - case string s when underlyingType.IsEnum: 256 - Value = (T)Enum.Parse(underlyingType, s); 257 - break; 258 - 259 255 default: 260 - Value = (T)Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture); 256 + if (underlyingType.IsEnum) 257 + Value = (T)Enum.Parse(underlyingType, input.ToString()); 258 + else 259 + Value = (T)Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture); 260 + 261 261 break; 262 262 } 263 263 }
+1 -1
osu.Framework/Bindables/BindableList.cs
··· 11 11 12 12 namespace osu.Framework.Bindables 13 13 { 14 - public class BindableList<T> : IBindableList<T>, IList<T>, IList 14 + public class BindableList<T> : IBindableList<T>, IBindable, IParseable, IList<T>, IList 15 15 { 16 16 /// <summary> 17 17 /// An event which is raised when this <see cref="BindableList{T}"/> changes.
+5 -5
osu.Framework/Bindables/IBindable.cs
··· 8 8 /// <summary> 9 9 /// An interface which can be bound to other <see cref="IBindable"/>s in order to watch for (and react to) <see cref="ICanBeDisabled.Disabled">Disabled</see> changes. 10 10 /// </summary> 11 - public interface IBindable : IParseable, ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription 11 + public interface IBindable : ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription 12 12 { 13 13 /// <summary> 14 14 /// Binds ourselves to another bindable such that we receive any value limitations of the bindable we bind width. ··· 20 20 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios. 21 21 /// Passes the provided value as the foreign (more permanent) bindable. 22 22 /// </summary> 23 - public sealed IBindable BindTarget 23 + sealed IBindable BindTarget 24 24 { 25 25 set => BindTo(value); 26 26 } ··· 38 38 /// An interface which can be bound to other <see cref="IBindable{T}"/>s in order to watch for (and react to) <see cref="ICanBeDisabled.Disabled">Disabled</see> and <see cref="IBindable{T}.Value">Value</see> changes. 39 39 /// </summary> 40 40 /// <typeparam name="T">The type of value encapsulated by this <see cref="IBindable{T}"/>.</typeparam> 41 - public interface IBindable<T> : IBindable 41 + public interface IBindable<T> : ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription 42 42 { 43 43 /// <summary> 44 44 /// An event which is raised when <see cref="Value"/> has changed. ··· 65 65 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios. 66 66 /// Passes the provided value as the foreign (more permanent) bindable. 67 67 /// </summary> 68 - new IBindable<T> BindTarget 68 + IBindable<T> BindTarget 69 69 { 70 70 set => BindTo(value); 71 71 } ··· 83 83 /// a local reference. 84 84 /// </summary> 85 85 /// <returns>A weakly bound copy of the specified bindable.</returns> 86 - new IBindable<T> GetBoundCopy(); 86 + IBindable<T> GetBoundCopy(); 87 87 } 88 88 }
+3 -3
osu.Framework/Bindables/IBindableList.cs
··· 10 10 /// A readonly interface which can be bound to other <see cref="IBindableList{T}"/>s in order to watch for state and content changes. 11 11 /// </summary> 12 12 /// <typeparam name="T">The type of value encapsulated by this <see cref="IBindableList{T}"/>.</typeparam> 13 - public interface IBindableList<T> : IReadOnlyList<T>, IBindable, INotifyCollectionChanged 13 + public interface IBindableList<T> : IReadOnlyList<T>, ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription, INotifyCollectionChanged 14 14 { 15 15 /// <summary> 16 16 /// Binds self to another bindable such that we receive any values and value limitations of the bindable we bind width. ··· 30 30 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios. 31 31 /// Passes the provided value as the foreign (more permanent) bindable. 32 32 /// </summary> 33 - new sealed IBindableList<T> BindTarget 33 + sealed IBindableList<T> BindTarget 34 34 { 35 35 set => BindTo(value); 36 36 } ··· 41 41 /// a local reference. 42 42 /// </summary> 43 43 /// <returns>A weakly bound copy of the specified bindable.</returns> 44 - new IBindableList<T> GetBoundCopy(); 44 + IBindableList<T> GetBoundCopy(); 45 45 } 46 46 }
+5 -1
osu.Framework/Configuration/IniConfigManager.cs
··· 6 6 using System.IO; 7 7 using osu.Framework.Bindables; 8 8 using osu.Framework.Extensions.ObjectExtensions; 9 + using osu.Framework.Extensions.TypeExtensions; 9 10 using osu.Framework.Logging; 10 11 using osu.Framework.Platform; 11 12 ··· 59 60 { 60 61 try 61 62 { 62 - b.Parse(val); 63 + if (!(b is IParseable parseable)) 64 + throw new InvalidOperationException($"Bindable type {b.GetType().ReadableName()} is not {nameof(IParseable)}."); 65 + 66 + parseable.Parse(val); 63 67 } 64 68 catch (Exception e) 65 69 {
+72 -15
osu.Framework/Development/ThreadSafety.cs
··· 7 7 8 8 namespace osu.Framework.Development 9 9 { 10 - internal static class ThreadSafety 10 + /// <summary> 11 + /// Utilities to ensure correct thread usage throughout a game. 12 + /// </summary> 13 + public static class ThreadSafety 11 14 { 15 + /// <summary> 16 + /// Whether the current code is executing on the input thread. 17 + /// </summary> 18 + [field: ThreadStatic] 19 + public static bool IsInputThread { get; internal set; } 20 + 21 + /// <summary> 22 + /// Whether the current code is executing on the update thread. 23 + /// </summary> 24 + [field: ThreadStatic] 25 + public static bool IsUpdateThread { get; internal set; } 26 + 27 + /// <summary> 28 + /// Whether the current code is executing on the draw thread. 29 + /// </summary> 30 + [field: ThreadStatic] 31 + public static bool IsDrawThread { get; internal set; } 32 + 33 + /// <summary> 34 + /// Whether the current code is executing on the audio thread. 35 + /// </summary> 36 + [field: ThreadStatic] 37 + public static bool IsAudioThread { get; internal set; } 38 + 39 + /// <summary> 40 + /// Asserts that the current code is executing on the input thread. 41 + /// </summary> 42 + /// <remarks> 43 + /// Only asserts in debug builds due to performance concerns. 44 + /// </remarks> 45 + [Conditional("DEBUG")] 46 + internal static void EnsureInputThread() => Debug.Assert(IsInputThread); 47 + 48 + /// <summary> 49 + /// Asserts that the current code is executing on the update thread. 50 + /// </summary> 51 + /// <remarks> 52 + /// Only asserts in debug builds due to performance concerns. 53 + /// </remarks> 12 54 [Conditional("DEBUG")] 13 55 internal static void EnsureUpdateThread() => Debug.Assert(IsUpdateThread); 14 56 57 + /// <summary> 58 + /// Asserts that the current code is not executing on the update thread. 59 + /// </summary> 60 + /// <remarks> 61 + /// Only asserts in debug builds due to performance concerns. 62 + /// </remarks> 15 63 [Conditional("DEBUG")] 16 64 internal static void EnsureNotUpdateThread() => Debug.Assert(!IsUpdateThread); 17 65 66 + /// <summary> 67 + /// Asserts that the current code is executing on the draw thread. 68 + /// </summary> 69 + /// <remarks> 70 + /// Only asserts in debug builds due to performance concerns. 71 + /// </remarks> 18 72 [Conditional("DEBUG")] 19 73 internal static void EnsureDrawThread() => Debug.Assert(IsDrawThread); 20 74 75 + /// <summary> 76 + /// Asserts that the current code is executing on the audio thread. 77 + /// </summary> 78 + /// <remarks> 79 + /// Only asserts in debug builds due to performance concerns. 80 + /// </remarks> 81 + [Conditional("DEBUG")] 82 + internal static void EnsureAudioThread() => Debug.Assert(IsAudioThread); 83 + 84 + /// <summary> 85 + /// The current execution mode. 86 + /// </summary> 87 + internal static ExecutionMode ExecutionMode; 88 + 89 + /// <summary> 90 + /// Resets all statics for the current thread. 91 + /// </summary> 21 92 internal static void ResetAllForCurrentThread() 22 93 { 23 94 IsInputThread = false; ··· 25 96 IsDrawThread = false; 26 97 IsAudioThread = false; 27 98 } 28 - 29 - public static ExecutionMode ExecutionMode; 30 - 31 - [ThreadStatic] 32 - public static bool IsInputThread; 33 - 34 - [ThreadStatic] 35 - public static bool IsUpdateThread; 36 - 37 - [ThreadStatic] 38 - public static bool IsDrawThread; 39 - 40 - [ThreadStatic] 41 - public static bool IsAudioThread; 42 99 } 43 100 }
-2
osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs
··· 85 85 /// </summary> 86 86 /// <param name="colour">Original colour</param> 87 87 /// <param name="scalar">A scalar to multiply with</param> 88 - /// <returns></returns> 89 88 public static Color4 Multiply(this Color4 colour, float scalar) 90 89 { 91 90 if (scalar < 0) ··· 180 179 /// <param name="h">The hue, between 0 and 360.</param> 181 180 /// <param name="s">The saturation, between 0 and 1.</param> 182 181 /// <param name="v">The value, between 0 and 1.</param> 183 - /// <returns></returns> 184 182 public static Color4 FromHSV(float h, float s, float v) 185 183 { 186 184 if (h < 0 || h > 360)
+4 -3
osu.Framework/Extensions/ExtensionMethods.cs
··· 56 56 /// </summary> 57 57 /// <param name="dictionary">The dictionary.</param> 58 58 /// <param name="lookup">The lookup key.</param> 59 - /// <returns></returns> 60 59 public static TValue GetOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey lookup) => dictionary.TryGetValue(lookup, out TValue outVal) ? outVal : default; 61 60 62 61 /// <summary> ··· 175 174 } 176 175 177 176 public static string GetDescription(this object value) 178 - => value.GetType().GetField(value.ToString()) 179 - .GetCustomAttribute<DescriptionAttribute>()?.Description ?? value.ToString(); 177 + => value.GetType() 178 + .GetField(value.ToString())? 179 + .GetCustomAttribute<DescriptionAttribute>()?.Description 180 + ?? value.ToString(); 180 181 181 182 /// <summary> 182 183 /// Gets a SHA-2 (256bit) hash for the given stream, seeking the stream before and after.
-1
osu.Framework/Extensions/TypeExtensions/TypeExtensions.cs
··· 40 40 /// Return every base type until (and excluding) <see cref="object"/> 41 41 /// </summary> 42 42 /// <param name="t"></param> 43 - /// <returns></returns> 44 43 public static IEnumerable<Type> EnumerateBaseTypes(this Type t) 45 44 { 46 45 while (t != null && t != typeof(object))
-1
osu.Framework/Graphics/Colour4.cs
··· 89 89 /// The final alpha is clamped to the 0-1 range. 90 90 /// </summary> 91 91 /// <param name="scalar">The value that the existing alpha will be multiplied by.</param> 92 - /// <returns></returns> 93 92 public Colour4 MultiplyAlpha(float scalar) 94 93 { 95 94 if (scalar < 0)
+15 -2
osu.Framework/Graphics/Containers/GridContainer.cs
··· 6 6 using System.Linq; 7 7 using osu.Framework.Allocation; 8 8 using osu.Framework.Caching; 9 + using osu.Framework.Extensions.EnumExtensions; 9 10 using osu.Framework.Layout; 10 11 using osuTK; 11 12 ··· 267 268 { 268 269 // Go through each row and get the width of the cell at the indexed column 269 270 for (int r = 0; r < cellRows; r++) 270 - size = Math.Max(size, getCellWidth(Content[r]?[i])); 271 + { 272 + var cell = Content[r]?[i]; 273 + if (cell == null || cell.RelativeSizeAxes.HasFlagFast(axis)) 274 + continue; 275 + 276 + size = Math.Max(size, getCellWidth(cell)); 277 + } 271 278 } 272 279 else 273 280 { 274 281 // Go through each column and get the height of the cell at the indexed row 275 282 for (int c = 0; c < cellColumns; c++) 276 - size = Math.Max(size, getCellHeight(Content[i]?[c])); 283 + { 284 + var cell = Content[i]?[c]; 285 + if (cell == null || cell.RelativeSizeAxes.HasFlagFast(axis)) 286 + continue; 287 + 288 + size = Math.Max(size, getCellHeight(cell)); 289 + } 277 290 } 278 291 279 292 sizes[i] = size;
+8 -1
osu.Framework/Graphics/Containers/OverlayContainer.cs
··· 18 18 protected virtual bool BlockPositionalInput => true; 19 19 20 20 /// <summary> 21 + /// Scroll events are sometimes required to be handled differently to general positional input. 22 + /// This covers whether scroll events that occur within this overlay's bounds are blocked or not. 23 + /// Defaults to the same value as <see cref="BlockPositionalInput"/> 24 + /// </summary> 25 + protected virtual bool BlockScrollInput => BlockPositionalInput; 26 + 27 + /// <summary> 21 28 /// Whether we should block any non-positional input from interacting with things behind us. 22 29 /// </summary> 23 30 protected virtual bool BlockNonPositionalInput => false; ··· 41 48 switch (e) 42 49 { 43 50 case ScrollEvent _: 44 - if (BlockPositionalInput && base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) 51 + if (BlockScrollInput && base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) 45 52 return true; 46 53 47 54 break;
-1
osu.Framework/Graphics/Containers/ScrollContainer.cs
··· 135 135 /// </summary> 136 136 /// <param name="position">The value to clamp.</param> 137 137 /// <param name="extension">An extension value beyond the normal extent.</param> 138 - /// <returns></returns> 139 138 protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension); 140 139 141 140 protected override Container<T> Content => ScrollContent;
-13
osu.Framework/Graphics/Cursor/CursorContainer.cs
··· 7 7 using osu.Framework.Graphics.Shapes; 8 8 using osu.Framework.Input; 9 9 using osu.Framework.Input.Events; 10 - using osu.Framework.Utils; 11 10 using osuTK; 12 11 using osuTK.Graphics; 13 12 ··· 37 36 38 37 public override bool PropagatePositionalInputSubTree => IsPresent; // make sure we are still updating position during possible fade out. 39 38 40 - private Vector2? lastPosition; 41 - 42 39 protected override bool OnMouseMove(MouseMoveEvent e) 43 40 { 44 - // required due to IRequireHighFrequencyMousePosition firing with the last known position even when the source is not in a 45 - // valid state (ie. receiving updates from user or otherwise). in this case, we generally want the cursor to remain at its 46 - // last *relative* position. 47 - if (lastPosition.HasValue && Precision.AlmostEquals(e.ScreenSpaceMousePosition, lastPosition.Value)) 48 - return false; 49 - 50 - lastPosition = e.ScreenSpaceMousePosition; 51 - 52 - ActiveCursor.RelativePositionAxes = Axes.None; 53 41 ActiveCursor.Position = e.MousePosition; 54 - ActiveCursor.RelativePositionAxes = Axes.Both; 55 42 return base.OnMouseMove(e); 56 43 } 57 44
+12 -2
osu.Framework/Graphics/Drawable.cs
··· 18 18 using System; 19 19 using System.Collections.Concurrent; 20 20 using System.Collections.Generic; 21 + using System.ComponentModel; 21 22 using System.Diagnostics; 22 23 using System.Linq; 23 24 using System.Reflection; ··· 35 36 using osu.Framework.Testing; 36 37 using osu.Framework.Utils; 37 38 using osuTK.Input; 39 + using Container = osu.Framework.Graphics.Containers.Container; 38 40 39 41 namespace osu.Framework.Graphics 40 42 { ··· 423 425 /// </summary> 424 426 internal event Action OnUnbindAllBindables; 425 427 428 + /// <summary> 429 + /// A lock exclusively used for initial acquisition/construction of the <see cref="Scheduler"/>. 430 + /// </summary> 431 + private readonly object schedulerAcquisitionLock = new object(); 432 + 426 433 private Scheduler scheduler; 427 434 428 435 /// <summary> ··· 436 443 if (scheduler != null) 437 444 return scheduler; 438 445 439 - lock (LoadLock) 446 + lock (schedulerAcquisitionLock) 440 447 return scheduler ??= new Scheduler(() => ThreadSafety.IsUpdateThread, Clock); 441 448 } 442 449 } ··· 2866 2873 2867 2874 public enum RotationDirection 2868 2875 { 2876 + [Description("Clockwise")] 2869 2877 Clockwise, 2870 - CounterClockwise, 2878 + 2879 + [Description("Counterclockwise")] 2880 + Counterclockwise, 2871 2881 } 2872 2882 2873 2883 /// <summary>
-1
osu.Framework/Graphics/Lines/SmoothPath.cs
··· 78 78 /// Retrieves the colour from a position in the texture of the <see cref="Path"/>. 79 79 /// </summary> 80 80 /// <param name="position">The position within the texture. 0 indicates the outermost-point of the path, 1 indicates the centre of the path.</param> 81 - /// <returns></returns> 82 81 protected virtual Color4 ColourAt(float position) => Color4.White; 83 82 } 84 83 }
+25 -28
osu.Framework/Graphics/Performance/LifetimeEntry.cs
··· 21 21 public double LifetimeStart 22 22 { 23 23 get => lifetimeStart; 24 - set 25 - { 26 - if (lifetimeStart == value) 27 - return; 28 - 29 - if (RequestLifetimeUpdate != null) 30 - RequestLifetimeUpdate.Invoke(this, value, lifetimeEnd); 31 - else 32 - SetLifetime(value, lifetimeEnd); 33 - } 24 + // A method is used as C# doesn't allow the combination of a non-virtual getter and a virtual setter. 25 + set => SetLifetimeStart(value); 34 26 } 35 27 36 28 private double lifetimeEnd = double.MaxValue; ··· 41 33 public double LifetimeEnd 42 34 { 43 35 get => lifetimeEnd; 44 - set 45 - { 46 - if (lifetimeEnd == value) 47 - return; 36 + set => SetLifetimeEnd(value); 37 + } 38 + 39 + /// <summary> 40 + /// Invoked when <see cref="LifetimeStart"/> or <see cref="LifetimeEnd"/> is going to be changed. 41 + /// </summary> 42 + internal event Action<LifetimeEntry> RequestLifetimeUpdate; 48 43 49 - if (RequestLifetimeUpdate != null) 50 - RequestLifetimeUpdate.Invoke(this, lifetimeStart, value); 51 - else 52 - SetLifetime(lifetimeStart, value); 53 - } 44 + /// <summary> 45 + /// Update <see cref="LifetimeStart"/> of this <see cref="LifetimeEntry"/>. 46 + /// </summary> 47 + protected virtual void SetLifetimeStart(double start) 48 + { 49 + if (start != lifetimeStart) 50 + SetLifetime(start, lifetimeEnd); 54 51 } 55 52 56 53 /// <summary> 57 - /// Invoked when this <see cref="LifetimeEntry"/> is attached to a <see cref="LifetimeEntryManager"/> and either 58 - /// <see cref="LifetimeStart"/> or <see cref="LifetimeEnd"/> are changed. 54 + /// Update <see cref="LifetimeEnd"/> of this <see cref="LifetimeEntry"/>. 59 55 /// </summary> 60 - /// <remarks> 61 - /// If this is handled, make sure to call <see cref="SetLifetime"/> to continue with the lifetime update. 62 - /// </remarks> 63 - internal event RequestLifetimeUpdateDelegate RequestLifetimeUpdate; 56 + protected virtual void SetLifetimeEnd(double end) 57 + { 58 + if (end != lifetimeEnd) 59 + SetLifetime(lifetimeStart, end); 60 + } 64 61 65 62 /// <summary> 66 63 /// Updates the stored lifetimes of this <see cref="LifetimeEntry"/>. 67 64 /// </summary> 68 65 /// <param name="start">The new <see cref="LifetimeStart"/> value.</param> 69 66 /// <param name="end">The new <see cref="LifetimeEnd"/> value.</param> 70 - internal void SetLifetime(double start, double end) 67 + protected void SetLifetime(double start, double end) 71 68 { 69 + RequestLifetimeUpdate?.Invoke(this); 70 + 72 71 lifetimeStart = start; 73 72 lifetimeEnd = Math.Max(start, end); // Negative intervals are undesired. 74 73 } ··· 83 82 /// </summary> 84 83 internal ulong ChildId { get; set; } 85 84 } 86 - 87 - internal delegate void RequestLifetimeUpdateDelegate(LifetimeEntry entry, double lifetimeStart, double lifetimeEnd); 88 85 }
+2 -8
osu.Framework/Graphics/Performance/LifetimeEntryManager.cs
··· 156 156 } 157 157 158 158 /// <summary> 159 - /// Invoked when the lifetime of an entry has changed, so that an appropriate sorting order is maintained. 159 + /// Invoked when the lifetime of an entry is going to changed. 160 160 /// </summary> 161 - /// <param name="entry">The <see cref="LifetimeEntry"/> that changed.</param> 162 - /// <param name="lifetimeStart">The new start time.</param> 163 - /// <param name="lifetimeEnd">The new end time.</param> 164 - private void requestLifetimeUpdate(LifetimeEntry entry, double lifetimeStart, double lifetimeEnd) 161 + private void requestLifetimeUpdate(LifetimeEntry entry) 165 162 { 166 163 // Entries in the past/future sets need to be re-sorted to prevent the comparer from becoming unstable. 167 164 // To prevent, e.g. CompositeDrawable alive children changing during enumeration, the entry's state must not be updated immediately. ··· 178 175 // Enqueue the entry to be processed in the next Update(). 179 176 newEntries.Add(entry); 180 177 } 181 - 182 - // Since the comparer has now been resolved, the lifetime update can proceed. 183 - entry.SetLifetime(lifetimeStart, lifetimeEnd); 184 178 } 185 179 186 180 /// <summary>
-1
osu.Framework/Graphics/UserInterface/Menu.cs
··· 538 538 /// <summary> 539 539 /// Creates a sub-menu for <see cref="MenuItem.Items"/> of <see cref="MenuItem"/>s added to this <see cref="Menu"/>. 540 540 /// </summary> 541 - /// <returns></returns> 542 541 protected abstract Menu CreateSubMenu(); 543 542 544 543 /// <summary>
+49 -1
osu.Framework/Input/Bindings/InputKey.cs
··· 35 35 Alt = 5, 36 36 37 37 /// <summary> 38 - /// The win key. 38 + /// The windows/command key. 39 39 /// </summary> 40 40 [Order(4)] 41 41 Super = 7, ··· 813 813 /// The media next key. 814 814 /// </summary> 815 815 TrackNext, 816 + 817 + /// <summary> 818 + /// The left shift key. 819 + /// </summary> 820 + [Order(3)] 821 + LShift, 822 + 823 + /// <summary> 824 + /// The right shift key. 825 + /// </summary> 826 + [Order(3)] 827 + RShift, 828 + 829 + /// <summary> 830 + /// The left control key. 831 + /// </summary> 832 + [Order(1)] 833 + LControl, 834 + 835 + /// <summary> 836 + /// The right control key. 837 + /// </summary> 838 + [Order(1)] 839 + RControl, 840 + 841 + /// <summary> 842 + /// The left alt key. 843 + /// </summary> 844 + [Order(2)] 845 + LAlt, 846 + 847 + /// <summary> 848 + /// The right alt key. 849 + /// </summary> 850 + [Order(2)] 851 + RAlt, 852 + 853 + /// <summary> 854 + /// The left windows/command key. 855 + /// </summary> 856 + [Order(4)] 857 + LSuper, 858 + 859 + /// <summary> 860 + /// The right windows/command key. 861 + /// </summary> 862 + [Order(4)] 863 + RSuper, 816 864 817 865 /// <summary> 818 866 /// Indicates the first available joystick button.
+14 -5
osu.Framework/Input/Bindings/KeyBindingContainer.cs
··· 117 117 if (keyDown.Repeat && !SendRepeats) 118 118 return pressedBindings.Count > 0; 119 119 120 - return handleNewPressed(state, KeyCombination.FromKey(keyDown.Key), keyDown.Repeat); 120 + foreach (var key in KeyCombination.FromKey(keyDown.Key)) 121 + { 122 + if (handleNewPressed(state, key, keyDown.Repeat)) 123 + return true; 124 + } 125 + 126 + return false; 121 127 122 128 case KeyUpEvent keyUp: 123 - handleNewReleased(state, KeyCombination.FromKey(keyUp.Key)); 129 + // this is releasing the common shift when a remaining shift is still held. 130 + // ie. press LShift, press RShift, release RShift will result in InputKey.Shift being incorrectly released. 131 + foreach (var key in KeyCombination.FromKey(keyUp.Key)) 132 + handleNewReleased(state, key); 133 + 124 134 return false; 125 135 126 136 case JoystickPressEvent joystickPress: ··· 177 187 bool handled = false; 178 188 var bindings = (repeat ? KeyBindings : KeyBindings?.Except(pressedBindings)) ?? Enumerable.Empty<IKeyBinding>(); 179 189 var newlyPressed = bindings.Where(m => 180 - m.KeyCombination.Keys.Contains(newKey) // only handle bindings matching current key (not required for correct logic) 181 - && m.KeyCombination.IsPressed(pressedCombination, matchingMode)); 190 + m.KeyCombination.IsPressed(pressedCombination, matchingMode)); 182 191 183 192 if (KeyCombination.IsModifierKey(newKey)) 184 193 { ··· 289 298 // we don't want to consider exact matching here as we are dealing with bindings, not actions. 290 299 var newlyReleased = pressedBindings.Where(b => !b.KeyCombination.IsPressed(pressedCombination, KeyCombinationMatchingMode.Any)).ToList(); 291 300 292 - Trace.Assert(newlyReleased.All(b => b.KeyCombination.Keys.Contains(releasedKey))); 301 + Trace.Assert(newlyReleased.All(b => KeyCombination.ContainsKey(b.KeyCombination.Keys, releasedKey))); 293 302 294 303 foreach (var binding in newlyReleased) 295 304 {
+235 -65
osu.Framework/Input/Bindings/KeyCombination.cs
··· 78 78 if (Keys == pressedKeys.Keys) // Fast test for reference equality of underlying array 79 79 return true; 80 80 81 - switch (matchingMode) 82 - { 83 - case KeyCombinationMatchingMode.Any: 84 - return containsAll(pressedKeys.Keys, Keys, false); 85 - 86 - case KeyCombinationMatchingMode.Exact: 87 - // Keys are always ordered 88 - return pressedKeys.Keys.SequenceEqual(Keys); 89 - 90 - case KeyCombinationMatchingMode.Modifiers: 91 - return containsAll(pressedKeys.Keys, Keys, true); 92 - 93 - default: 94 - return false; 95 - } 81 + return ContainsAll(Keys, pressedKeys.Keys, matchingMode); 96 82 } 97 83 84 + /// <summary> 85 + /// Check whether the provided set of pressed keys matches the candidate binding. 86 + /// </summary> 87 + /// <param name="candidateKey">The candidate key binding to match against.</param> 88 + /// <param name="pressedKey">The keys which have been pressed by a user.</param> 89 + /// <param name="matchingMode">The matching mode to be used when checking.</param> 90 + /// <returns>Whether this is a match.</returns> 98 91 [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 - private static bool containsAll(ImmutableArray<InputKey> pressedKey, ImmutableArray<InputKey> candidateKey, bool exactModifiers) 92 + internal static bool ContainsAll(ImmutableArray<InputKey> candidateKey, ImmutableArray<InputKey> pressedKey, KeyCombinationMatchingMode matchingMode) 100 93 { 101 94 // can be local function once attribute on local functions are implemented 102 95 // optimized to avoid allocation 103 96 // Usually Keys.Count <= 3. Does not worth special logic for Contains(). 104 97 foreach (var key in candidateKey) 105 98 { 106 - if (!pressedKey.Contains(key)) 99 + if (!ContainsKey(pressedKey, key)) 107 100 return false; 108 101 } 109 102 110 - if (exactModifiers) 103 + switch (matchingMode) 111 104 { 112 - foreach (var key in pressedKey) 113 - { 114 - if (IsModifierKey(key) && 115 - !candidateKey.Contains(key)) 116 - return false; 117 - } 105 + case KeyCombinationMatchingMode.Exact: 106 + foreach (var key in pressedKey) 107 + { 108 + if (!ContainsKeyPermissive(candidateKey, key)) 109 + return false; 110 + } 111 + 112 + break; 113 + 114 + case KeyCombinationMatchingMode.Modifiers: 115 + foreach (var key in pressedKey) 116 + { 117 + if (IsModifierKey(key) && !ContainsKeyPermissive(candidateKey, key)) 118 + return false; 119 + } 120 + 121 + break; 118 122 } 119 123 120 124 return true; 121 125 } 122 126 127 + /// <summary> 128 + /// Check whether the provided key is part of the candidate binding. 129 + /// This will match bidirectionally for modifier keys (LShift and Shift being present in both of the two parameters in either order will return true). 130 + /// </summary> 131 + /// <param name="candidate">The candidate key binding to match against.</param> 132 + /// <param name="key">The key which has been pressed by a user.</param> 133 + /// <returns>Whether this is a match.</returns> 134 + internal static bool ContainsKeyPermissive(ImmutableArray<InputKey> candidate, InputKey key) 135 + { 136 + switch (key) 137 + { 138 + case InputKey.LControl: 139 + case InputKey.RControl: 140 + if (candidate.Contains(InputKey.Control)) 141 + return true; 142 + 143 + break; 144 + 145 + case InputKey.LShift: 146 + case InputKey.RShift: 147 + if (candidate.Contains(InputKey.Shift)) 148 + return true; 149 + 150 + break; 151 + 152 + case InputKey.RAlt: 153 + case InputKey.LAlt: 154 + if (candidate.Contains(InputKey.Alt)) 155 + return true; 156 + 157 + break; 158 + 159 + case InputKey.LSuper: 160 + case InputKey.RSuper: 161 + if (candidate.Contains(InputKey.Super)) 162 + return true; 163 + 164 + break; 165 + } 166 + 167 + return ContainsKey(candidate, key); 168 + } 169 + 170 + /// <summary> 171 + /// Check whether a single key from a candidate binding is relevant to the currently pressed keys. 172 + /// If the <paramref name="key"/> contains a left/right specific modifier, the <paramref name="candidate"/> must also for this to match. 173 + /// </summary> 174 + /// <param name="candidate">The candidate key binding to match against.</param> 175 + /// <param name="key">The key which has been pressed by a user.</param> 176 + /// <returns>Whether this is a match.</returns> 177 + internal static bool ContainsKey(ImmutableArray<InputKey> candidate, InputKey key) 178 + { 179 + switch (key) 180 + { 181 + case InputKey.Control: 182 + if (candidate.Contains(InputKey.LControl) || candidate.Contains(InputKey.RControl)) 183 + return true; 184 + 185 + break; 186 + 187 + case InputKey.Shift: 188 + if (candidate.Contains(InputKey.LShift) || candidate.Contains(InputKey.RShift)) 189 + return true; 190 + 191 + break; 192 + 193 + case InputKey.Alt: 194 + if (candidate.Contains(InputKey.LAlt) || candidate.Contains(InputKey.RAlt)) 195 + return true; 196 + 197 + break; 198 + 199 + case InputKey.Super: 200 + if (candidate.Contains(InputKey.LSuper) || candidate.Contains(InputKey.RSuper)) 201 + return true; 202 + 203 + break; 204 + } 205 + 206 + return candidate.Contains(key); 207 + } 208 + 123 209 public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); 124 210 125 211 public override bool Equals(object obj) => obj is KeyCombination kc && Equals(kc); ··· 146 232 147 233 public string ReadableString() 148 234 { 149 - var sortedKeys = Keys.GetValuesInOrder(); 150 - return string.Join('-', sortedKeys.Select(getReadableKey)); 235 + var sortedKeys = Keys.GetValuesInOrder().ToArray(); 236 + 237 + return string.Join('-', sortedKeys.Select(key => 238 + { 239 + switch (key) 240 + { 241 + case InputKey.Control: 242 + if (sortedKeys.Contains(InputKey.LControl) || sortedKeys.Contains(InputKey.RControl)) 243 + return null; 244 + 245 + break; 246 + 247 + case InputKey.Shift: 248 + if (sortedKeys.Contains(InputKey.LShift) || sortedKeys.Contains(InputKey.RShift)) 249 + return null; 250 + 251 + break; 252 + 253 + case InputKey.Alt: 254 + if (sortedKeys.Contains(InputKey.LAlt) || sortedKeys.Contains(InputKey.RAlt)) 255 + return null; 256 + 257 + break; 258 + 259 + case InputKey.Super: 260 + if (sortedKeys.Contains(InputKey.LSuper) || sortedKeys.Contains(InputKey.RSuper)) 261 + return null; 262 + 263 + break; 264 + } 265 + 266 + return getReadableKey(key); 267 + }).Where(s => !string.IsNullOrEmpty(s))); 151 268 } 152 269 153 270 [MethodImpl(MethodImplOptions.AggressiveInlining)] 154 - public static bool IsModifierKey(InputKey key) => key == InputKey.Control || key == InputKey.Shift || key == InputKey.Alt || key == InputKey.Super; 271 + public static bool IsModifierKey(InputKey key) 272 + { 273 + switch (key) 274 + { 275 + case InputKey.LControl: 276 + case InputKey.LShift: 277 + case InputKey.LAlt: 278 + case InputKey.LSuper: 279 + case InputKey.RControl: 280 + case InputKey.RShift: 281 + case InputKey.RAlt: 282 + case InputKey.RSuper: 283 + case InputKey.Control: 284 + case InputKey.Shift: 285 + case InputKey.Alt: 286 + case InputKey.Super: 287 + return true; 288 + } 289 + 290 + return false; 291 + } 155 292 156 - private string getReadableKey(InputKey key) 293 + private static string getReadableKey(InputKey key) 157 294 { 158 295 if (key >= InputKey.FirstTabletAuxiliaryButton) 159 296 return $"Tablet Aux {key - InputKey.FirstTabletAuxiliaryButton + 1}"; ··· 195 332 case InputKey.Super: 196 333 return "Win"; 197 334 335 + case InputKey.LShift: 336 + return "LShift"; 337 + 338 + case InputKey.LControl: 339 + return "LCtrl"; 340 + 341 + case InputKey.LAlt: 342 + return "LAlt"; 343 + 344 + case InputKey.LSuper: 345 + return "LWin"; 346 + 347 + case InputKey.RShift: 348 + return "RShift"; 349 + 350 + case InputKey.RControl: 351 + return "RCtrl"; 352 + 353 + case InputKey.RAlt: 354 + return "RAlt"; 355 + 356 + case InputKey.RSuper: 357 + return "RWin"; 358 + 198 359 case InputKey.Escape: 199 360 return "Esc"; 200 361 ··· 217 378 return "Caps"; 218 379 219 380 case InputKey.Number0: 220 - case InputKey.Keypad0: 221 381 return "0"; 382 + 383 + case InputKey.Keypad0: 384 + return "Numpad0"; 222 385 223 386 case InputKey.Number1: 224 - case InputKey.Keypad1: 225 387 return "1"; 388 + 389 + case InputKey.Keypad1: 390 + return "Numpad1"; 226 391 227 392 case InputKey.Number2: 228 - case InputKey.Keypad2: 229 393 return "2"; 394 + 395 + case InputKey.Keypad2: 396 + return "Numpad2"; 230 397 231 398 case InputKey.Number3: 232 - case InputKey.Keypad3: 233 399 return "3"; 400 + 401 + case InputKey.Keypad3: 402 + return "Numpad3"; 234 403 235 404 case InputKey.Number4: 236 - case InputKey.Keypad4: 237 405 return "4"; 406 + 407 + case InputKey.Keypad4: 408 + return "Numpad4"; 238 409 239 410 case InputKey.Number5: 240 - case InputKey.Keypad5: 241 411 return "5"; 412 + 413 + case InputKey.Keypad5: 414 + return "Numpad5"; 242 415 243 416 case InputKey.Number6: 244 - case InputKey.Keypad6: 245 417 return "6"; 418 + 419 + case InputKey.Keypad6: 420 + return "Numpad6"; 246 421 247 422 case InputKey.Number7: 248 - case InputKey.Keypad7: 249 423 return "7"; 424 + 425 + case InputKey.Keypad7: 426 + return "Numpad7"; 250 427 251 428 case InputKey.Number8: 252 - case InputKey.Keypad8: 253 429 return "8"; 430 + 431 + case InputKey.Keypad8: 432 + return "Numpad8"; 254 433 255 434 case InputKey.Number9: 256 - case InputKey.Keypad9: 257 435 return "9"; 436 + 437 + case InputKey.Keypad9: 438 + return "Numpad9"; 258 439 259 440 case InputKey.Tilde: 260 441 return "~"; ··· 364 545 } 365 546 } 366 547 367 - public static InputKey FromKey(Key key) 548 + public static InputKey[] FromKey(Key key) 368 549 { 369 550 switch (key) 370 551 { 371 - case Key.RShift: 372 - return InputKey.Shift; 552 + case Key.LShift: return new[] { InputKey.Shift, InputKey.LShift }; 553 + 554 + case Key.RShift: return new[] { InputKey.Shift, InputKey.RShift }; 555 + 556 + case Key.LControl: return new[] { InputKey.Control, InputKey.LControl }; 373 557 374 - case Key.RAlt: 375 - return InputKey.Alt; 558 + case Key.RControl: return new[] { InputKey.Control, InputKey.RControl }; 376 559 377 - case Key.RControl: 378 - return InputKey.Control; 560 + case Key.LAlt: return new[] { InputKey.Alt, InputKey.LAlt }; 379 561 380 - case Key.RWin: 381 - return InputKey.Super; 562 + case Key.RAlt: return new[] { InputKey.Alt, InputKey.RAlt }; 563 + 564 + case Key.LWin: return new[] { InputKey.Super, InputKey.LSuper }; 565 + 566 + case Key.RWin: return new[] { InputKey.Super, InputKey.RSuper }; 382 567 } 383 568 384 - return (InputKey)key; 569 + return new[] { (InputKey)key }; 385 570 } 386 571 387 572 public static InputKey FromMouseButton(MouseButton button) => (InputKey)((int)InputKey.FirstMouseButton + button); ··· 449 634 { 450 635 foreach (var key in state.Keyboard.Keys) 451 636 { 452 - InputKey iKey = FromKey(key); 453 - 454 - switch (key) 637 + foreach (var iKey in FromKey(key)) 455 638 { 456 - case Key.LShift: 457 - case Key.RShift: 458 - case Key.LAlt: 459 - case Key.RAlt: 460 - case Key.LControl: 461 - case Key.RControl: 462 - case Key.LWin: 463 - case Key.RWin: 464 - if (!keys.Contains(iKey)) 465 - keys.Add(iKey); 466 - break; 467 - 468 - default: 639 + if (!keys.Contains(iKey)) 469 640 keys.Add(iKey); 470 - break; 471 641 } 472 642 } 473 643 }
+5 -1
osu.Framework/Input/Handlers/Midi/MidiHandler.cs
··· 6 6 using System.Diagnostics; 7 7 using System.IO; 8 8 using System.Linq; 9 + using System.Threading.Tasks; 9 10 using Commons.Music.Midi; 10 11 using osu.Framework.Input.StateChanges; 11 12 using osu.Framework.Logging; ··· 110 111 111 112 private void closeDevice(IMidiInput device) 112 113 { 113 - device.CloseAsync().Wait(); 114 114 device.MessageReceived -= onMidiMessageReceived; 115 + 116 + // some devices may take some time to close, so this should be fire-and-forget. 117 + // the internal implementations look to have their own (eventual) timeout logic. 118 + Task.Factory.StartNew(() => device.CloseAsync(), TaskCreationOptions.LongRunning); 115 119 } 116 120 117 121 private void onMidiMessageReceived(object sender, MidiReceivedEventArgs e)
+7 -2
osu.Framework/Input/Handlers/Mouse/MouseHandler.cs
··· 51 51 /// </summary> 52 52 private bool absolutePositionReceived; 53 53 54 + /// <summary> 55 + /// Whether the application should be handling the cursor. 56 + /// </summary> 57 + private bool cursorCaptured => isActive.Value && (window.CursorInWindow.Value || window.CursorState.HasFlagFast(CursorState.Confined)); 58 + 54 59 public override bool Initialize(GameHost host) 55 60 { 56 61 if (!base.Initialize(host)) ··· 106 111 if (!Enabled.Value) 107 112 return; 108 113 109 - if (!isSelfFeedback) 114 + if (!isSelfFeedback && isActive.Value) 110 115 // if another handler has updated the cursor position, handle updating the OS cursor so we can seamlessly revert 111 116 // to mouse control at any point. 112 117 window.UpdateMousePosition(position); ··· 151 156 // relative mode requires at least one absolute input to arrive, to gain an additional position to work with. 152 157 && absolutePositionReceived 153 158 // relative mode only works when the window is active and the cursor is contained. aka the OS cursor isn't being displayed outside the window. 154 - && (isActive.Value && (window.CursorInWindow.Value || window.CursorState.HasFlagFast(CursorState.Confined))) 159 + && cursorCaptured 155 160 // relative mode shouldn't ever be enabled if the framework or a consumer has chosen not to hide the cursor. 156 161 && window.CursorState.HasFlagFast(CursorState.Hidden); 157 162
+3
osu.Framework/Input/InputManager.cs
··· 523 523 524 524 foreach (var h in InputHandlers) 525 525 { 526 + if (!h.IsActive) 527 + continue; 528 + 526 529 dequeuedInputs.Clear(); 527 530 h.CollectPendingInputs(dequeuedInputs); 528 531
+4 -3
osu.Framework/Input/MouseButtonEventManager.cs
··· 208 208 209 209 if (DraggedDrawable == null) return; 210 210 211 - PropagateButtonEvent(new[] { DraggedDrawable }, new DragEndEvent(state, Button, MouseDownPosition)); 211 + var previousDragged = DraggedDrawable; 212 + previousDragged.IsDragged = false; 213 + DraggedDrawable = null; 212 214 213 - DraggedDrawable.IsDragged = false; 214 - DraggedDrawable = null; 215 + PropagateButtonEvent(new[] { previousDragged }, new DragEndEvent(state, Button, MouseDownPosition)); 215 216 } 216 217 } 217 218 }
-1
osu.Framework/Platform/Display.cs
··· 59 59 /// <param name="size">The <see cref="Size"/> to match.</param> 60 60 /// <param name="bitsPerPixel">The bits per pixel to match. If null, the highest available bits per pixel will be used.</param> 61 61 /// <param name="refreshRate">The refresh rate in hertz. If null, the highest available refresh rate will be used.</param> 62 - /// <returns></returns> 63 62 public DisplayMode FindDisplayMode(Size size, int? bitsPerPixel = null, int? refreshRate = null) => 64 63 DisplayModes.Where(mode => mode.Size.Width <= size.Width && mode.Size.Height <= size.Height && 65 64 (bitsPerPixel == null || mode.BitsPerPixel == bitsPerPixel) &&
-2
osu.Framework/Platform/IWindow.cs
··· 151 151 /// Convert a screen based coordinate to local window space. 152 152 /// </summary> 153 153 /// <param name="point"></param> 154 - /// <returns></returns> 155 154 Point PointToClient(Point point); 156 155 157 156 /// <summary> 158 157 /// Convert a window based coordinate to global screen space. 159 158 /// </summary> 160 159 /// <param name="point"></param> 161 - /// <returns></returns> 162 160 Point PointToScreen(Point point); 163 161 164 162 /// <summary>
+6 -4
osu.Framework/Platform/SDL2DesktopWindow.cs
··· 163 163 164 164 public Bindable<WindowMode> WindowMode { get; } = new Bindable<WindowMode>(); 165 165 166 - private readonly BindableBool isActive = new BindableBool(true); 166 + private readonly BindableBool isActive = new BindableBool(); 167 167 168 168 public IBindable<bool> IsActive => isActive; 169 169 170 - private readonly BindableBool cursorInWindow = new BindableBool(true); 170 + private readonly BindableBool cursorInWindow = new BindableBool(); 171 171 172 172 public IBindable<bool> CursorInWindow => cursorInWindow; 173 173 ··· 881 881 SDL.SDL_GetWindowPosition(SDLWindowHandle, out int x, out int y); 882 882 var newPosition = new Point(x, y); 883 883 884 - if (WindowMode.Value == Configuration.WindowMode.Windowed && !newPosition.Equals(Position)) 884 + if (!newPosition.Equals(Position)) 885 885 { 886 886 position = newPosition; 887 - storeWindowPositionToConfig(); 888 887 ScheduleEvent(() => Moved?.Invoke(newPosition)); 888 + 889 + if (WindowMode.Value == Configuration.WindowMode.Windowed) 890 + storeWindowPositionToConfig(); 889 891 } 890 892 891 893 break;
+1 -1
osu.Framework/Platform/Windows/WindowsWindow.cs
··· 45 45 46 46 // for now let's use the same 1px hack that we've always used to force borderless. 47 47 SDL.SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height); 48 - SDL.SDL_SetWindowPosition(SDLWindowHandle, newPosition.X, newPosition.Y); 48 + Position = newPosition; 49 49 50 50 return newSize; 51 51 }
-1
osu.Framework/Statistics/GlobalStatistics.cs
··· 33 33 /// <param name="group">The group specification.</param> 34 34 /// <param name="name">The name specification.</param> 35 35 /// <typeparam name="T">The type.</typeparam> 36 - /// <returns></returns> 37 36 public static GlobalStatistic<T> Get<T>(string group, string name) 38 37 { 39 38 lock (statistics)
+13
osu.Framework/Testing/ITypeReferenceBuilder.cs
··· 3 3 4 4 using System; 5 5 using System.Collections.Generic; 6 + using System.IO; 6 7 using System.Threading.Tasks; 8 + using osu.Framework.Extensions.TypeExtensions; 7 9 8 10 namespace osu.Framework.Testing 9 11 { ··· 35 37 /// Resets this <see cref="ITypeReferenceBuilder"/>. 36 38 /// </summary> 37 39 void Reset(); 40 + } 41 + 42 + /// <summary> 43 + /// Indicates that there was no link between a given test type and the changed file. 44 + /// </summary> 45 + internal class NoLinkBetweenTypesException : Exception 46 + { 47 + public NoLinkBetweenTypesException(Type testType, string changedFile) 48 + : base($"The changed file \"{Path.GetFileName(changedFile)}\" is not used by the test \"{testType.ReadableName()}\".") 49 + { 50 + } 38 51 } 39 52 }
+128 -54
osu.Framework/Testing/RoslynTypeReferenceBuilder.cs
··· 3 3 4 4 #if NET5_0 5 5 using System; 6 + using System.Collections.Concurrent; 6 7 using System.Collections.Generic; 7 8 using System.IO; 8 9 using System.Linq; ··· 28 29 29 30 private readonly Logger logger; 30 31 31 - private readonly Dictionary<TypeReference, IReadOnlyCollection<TypeReference>> referenceMap = new Dictionary<TypeReference, IReadOnlyCollection<TypeReference>>(); 32 - private readonly Dictionary<Project, Compilation> compilationCache = new Dictionary<Project, Compilation>(); 33 - private readonly Dictionary<string, SemanticModel> semanticModelCache = new Dictionary<string, SemanticModel>(); 34 - private readonly Dictionary<TypeReference, bool> typeInheritsFromGameCache = new Dictionary<TypeReference, bool>(); 35 - private readonly Dictionary<string, bool> syntaxExclusionMap = new Dictionary<string, bool>(); 36 - private readonly HashSet<string> assembliesContainingReferencedInternalMembers = new HashSet<string>(); 32 + private readonly ConcurrentDictionary<TypeReference, IReadOnlyCollection<TypeReference>> referenceMap = new ConcurrentDictionary<TypeReference, IReadOnlyCollection<TypeReference>>(); 33 + private readonly ConcurrentDictionary<Project, Compilation> compilationCache = new ConcurrentDictionary<Project, Compilation>(); 34 + private readonly ConcurrentDictionary<string, SemanticModel> semanticModelCache = new ConcurrentDictionary<string, SemanticModel>(); 35 + private readonly ConcurrentDictionary<TypeReference, bool> typeInheritsFromGameCache = new ConcurrentDictionary<TypeReference, bool>(); 36 + private readonly ConcurrentDictionary<string, bool> syntaxExclusionMap = new ConcurrentDictionary<string, bool>(); 37 + private readonly ConcurrentDictionary<string, byte> assembliesContainingReferencedInternalMembers = new ConcurrentDictionary<string, byte>(); 37 38 38 39 private Solution solution; 39 40 ··· 56 57 57 58 await buildReferenceMapAsync(testType, changedFile).ConfigureAwait(false); 58 59 59 - var directedGraph = getDirectedGraph(); 60 + var sources = getTypesFromFile(changedFile).ToArray(); 61 + if (sources.Length == 0) 62 + throw new NoLinkBetweenTypesException(testType, changedFile); 60 63 61 - return getReferencedFiles(getTypesFromFile(changedFile), directedGraph); 64 + return getReferencedFiles(sources, getDirectedGraph()); 62 65 } 63 66 64 67 public async Task<IReadOnlyCollection<AssemblyReference>> GetReferencedAssemblies(Type testType, string changedFile) => await Task.Run(() => ··· 67 70 68 71 var assemblies = new HashSet<AssemblyReference>(); 69 72 73 + foreach (var asm in compilationCache.Values.SelectMany(c => c.ReferencedAssemblyNames)) 74 + addReference(Assembly.Load(asm.Name), false); 70 75 foreach (var asm in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic)) 71 76 addReference(asm, false); 72 77 addReference(typeof(JetBrains.Annotations.NotNullAttribute).Assembly, true); ··· 85 90 if (!force && loadedTypes.Any(t => t.Namespace == jetbrains_annotations_namespace)) 86 91 return; 87 92 88 - bool containsReferencedInternalMember = assembliesContainingReferencedInternalMembers.Any(i => assembly.FullName?.Contains(i) == true); 93 + bool containsReferencedInternalMember = assembliesContainingReferencedInternalMembers.Any(i => assembly.FullName?.Contains(i.Key) == true); 89 94 assemblies.Add(new AssemblyReference(assembly, containsReferencedInternalMember)); 90 95 } 91 96 }).ConfigureAwait(false); ··· 143 148 144 149 foreach (var t in oldTypes) 145 150 { 146 - referenceMap.Remove(t); 147 - typeInheritsFromGameCache.Remove(t); 151 + referenceMap.TryRemove(t, out _); 152 + typeInheritsFromGameCache.TryRemove(t, out _); 148 153 } 149 154 150 155 foreach (var t in oldTypes) ··· 166 171 var semanticModel = await getSemanticModelAsync(syntaxTree).ConfigureAwait(false); 167 172 var referencedTypes = await getReferencedTypesAsync(semanticModel).ConfigureAwait(false); 168 173 169 - referenceMap[TypeReference.FromSymbol(t.Symbol)] = referencedTypes; 174 + referenceMap[TypeReference.FromSymbol(t.Symbol)] = referencedTypes.ToHashSet(); 170 175 171 176 foreach (var referenced in referencedTypes) 172 177 await buildReferenceMapRecursiveAsync(referenced).ConfigureAwait(false); ··· 189 194 /// <param name="rootReference">The root, where the map should start being build from.</param> 190 195 private async Task buildReferenceMapRecursiveAsync(TypeReference rootReference) 191 196 { 192 - var searchQueue = new Queue<TypeReference>(); 193 - searchQueue.Enqueue(rootReference); 197 + var searchQueue = new ConcurrentBag<TypeReference> { rootReference }; 194 198 195 199 while (searchQueue.Count > 0) 196 200 { 197 - var toCheck = searchQueue.Dequeue(); 198 - var referencedTypes = await getReferencedTypesAsync(toCheck).ConfigureAwait(false); 201 + var toProcess = searchQueue.ToArray(); 202 + searchQueue.Clear(); 199 203 200 - referenceMap[toCheck] = referencedTypes; 204 + await Task.WhenAll(toProcess.Select(async toCheck => 205 + { 206 + var referencedTypes = await getReferencedTypesAsync(toCheck).ConfigureAwait(false); 207 + referenceMap[toCheck] = referencedTypes; 201 208 202 - foreach (var referenced in referencedTypes) 203 - { 204 - // We don't want to cycle over types that have already been explored. 205 - if (!referenceMap.ContainsKey(referenced)) 209 + foreach (var referenced in referencedTypes) 206 210 { 207 - // Used for de-duping, so it must be added to the dictionary immediately. 208 - referenceMap[referenced] = null; 209 - searchQueue.Enqueue(referenced); 211 + // We don't want to cycle over types that have already been explored. 212 + if (referenceMap.TryAdd(referenced, null)) 213 + searchQueue.Add(referenced); 210 214 } 211 - } 215 + })).ConfigureAwait(false); 212 216 } 213 217 } 214 218 ··· 238 242 /// </summary> 239 243 /// <param name="semanticModel">The target <see cref="SemanticModel"/>.</param> 240 244 /// <returns>All <see cref="TypeReference"/>s referenced by <paramref name="semanticModel"/>.</returns> 241 - private async Task<HashSet<TypeReference>> getReferencedTypesAsync(SemanticModel semanticModel) 245 + private async Task<ICollection<TypeReference>> getReferencedTypesAsync(SemanticModel semanticModel) 242 246 { 243 - var result = new HashSet<TypeReference>(); 247 + var result = new ConcurrentDictionary<TypeReference, byte>(); 244 248 245 249 var root = await semanticModel.SyntaxTree.GetRootAsync().ConfigureAwait(false); 246 - 247 250 var descendantNodes = root.DescendantNodes(n => 248 251 { 249 252 var kind = n.Kind(); ··· 252 255 // - Entire using lines. 253 256 // - Namespace names (not entire namespaces). 254 257 // - Entire static classes. 258 + // - Variable declarators (names of variables). 259 + // - The single IdentifierName child of an assignment expression (variable name), below. 260 + // - The single IdentifierName child of an argument syntax (variable name), below. 261 + // - The name of namespace declarations. 262 + // - Name-colon syntaxes. 263 + // - The expression of invocation expressions. Static classes are explicitly disallowed so the target type of an invocation must be available elsewhere in the syntax tree. 264 + // - The single IdentifierName child of a foreach expression (source variable name), below. 265 + // - The single 'var' IdentifierName child of a variable declaration, below. 266 + // - Element access expressions. 255 267 256 268 return kind != SyntaxKind.UsingDirective 257 269 && kind != SyntaxKind.NamespaceKeyword 258 - && (kind != SyntaxKind.ClassDeclaration || ((ClassDeclarationSyntax)n).Modifiers.All(m => m.Kind() != SyntaxKind.StaticKeyword)); 270 + && (kind != SyntaxKind.ClassDeclaration || ((ClassDeclarationSyntax)n).Modifiers.All(m => m.Kind() != SyntaxKind.StaticKeyword)) 271 + && (kind != SyntaxKind.QualifiedName || !(n.Parent is NamespaceDeclarationSyntax)) 272 + && kind != SyntaxKind.NameColon 273 + && (kind != SyntaxKind.QualifiedName || n.Parent?.Kind() != SyntaxKind.NamespaceDeclaration) 274 + && kind != SyntaxKind.NameColon 275 + && kind != SyntaxKind.ElementAccessExpression 276 + && (n.Parent?.Kind() != SyntaxKind.InvocationExpression || n != ((InvocationExpressionSyntax)n.Parent).Expression); 259 277 }); 260 278 261 - // Find all the named type symbols in the syntax tree, and mark + recursively iterate through them. 262 - foreach (var node in descendantNodes) 279 + // This hashset is used to prevent re-exploring syntaxes with the same name. 280 + // Todo: This can be used across all files, but care needs to be taken for redefined types (via using X = y), using the same-named type from a different namespace, or via type hiding. 281 + var seenTypes = new ConcurrentDictionary<string, byte>(); 282 + 283 + await Task.WhenAll(descendantNodes.Select(node => Task.Run(() => 263 284 { 285 + if (node.Kind() == SyntaxKind.IdentifierName && node.Parent != null) 286 + { 287 + // Ignore the variable name of assignment expressions. 288 + if (node.Parent is AssignmentExpressionSyntax) 289 + return; 290 + 291 + switch (node.Parent.Kind()) 292 + { 293 + case SyntaxKind.VariableDeclarator: // Ignore the variable name of variable declarators. 294 + case SyntaxKind.Argument: // Ignore the variable name of arguments. 295 + case SyntaxKind.InvocationExpression: // Ignore a single identifier name expression of an invocation expression (e.g. IdentifierName()). 296 + case SyntaxKind.ForEachStatement: // Ignore a single identifier of a foreach statement (the source). 297 + case SyntaxKind.VariableDeclaration when node.ToString() == "var": // Ignore the single 'var' identifier of a variable declaration. 298 + return; 299 + } 300 + } 301 + 264 302 switch (node.Kind()) 265 303 { 266 304 case SyntaxKind.GenericName: 267 305 case SyntaxKind.IdentifierName: 268 306 { 269 - if (semanticModel.GetSymbolInfo(node).Symbol is INamedTypeSymbol t) 270 - addTypeSymbol(t); 307 + string syntaxName = node.ToString(); 308 + 309 + if (seenTypes.ContainsKey(syntaxName)) 310 + return; 311 + 312 + if (!tryNode(node, out var symbol)) 313 + return; 314 + 315 + // The node has been processed so we want to avoid re-processing the same node again if possible, as this is a costly operation. 316 + // Note that the syntax name may differ from the finalised symbol name (e.g. member access). 317 + // We can only prevent future reprocessing if the symbol name and syntax name exactly match because we can't determine that the type won't be accessed later, such as: 318 + // 319 + // A.X = 5; // Syntax name = A, Symbol name = B 320 + // B.X = 5; // Syntax name = B, Symbol name = A 321 + // public A B; 322 + // public B A; 323 + // 324 + if (symbol.Name == syntaxName) 325 + seenTypes.TryAdd(symbol.Name, 0); 326 + 271 327 break; 272 328 } 329 + } 330 + }))).ConfigureAwait(false); 331 + 332 + return result.Keys; 273 333 274 - case SyntaxKind.AsExpression: 275 - case SyntaxKind.IsExpression: 276 - case SyntaxKind.SizeOfExpression: 277 - case SyntaxKind.TypeOfExpression: 278 - case SyntaxKind.CastExpression: 279 - case SyntaxKind.ObjectCreationExpression: 280 - { 281 - if (semanticModel.GetTypeInfo(node).Type is INamedTypeSymbol t) 282 - addTypeSymbol(t); 283 - break; 284 - } 334 + bool tryNode(SyntaxNode node, out INamedTypeSymbol symbol) 335 + { 336 + if (semanticModel.GetSymbolInfo(node).Symbol is INamedTypeSymbol sType) 337 + { 338 + addTypeSymbol(sType); 339 + symbol = sType; 340 + return true; 341 + } 342 + 343 + if (semanticModel.GetTypeInfo(node).Type is INamedTypeSymbol tType) 344 + { 345 + addTypeSymbol(tType); 346 + symbol = tType; 347 + return true; 285 348 } 286 - } 287 349 288 - return result; 350 + // Todo: Reduce the number of cases that fall through here. 351 + symbol = null; 352 + return false; 353 + } 289 354 290 355 void addTypeSymbol(INamedTypeSymbol typeSymbol) 291 356 { ··· 316 381 } 317 382 318 383 if (typeSymbol.DeclaredAccessibility == Accessibility.Internal) 319 - assembliesContainingReferencedInternalMembers.Add(typeSymbol.ContainingAssembly.Name); 384 + assembliesContainingReferencedInternalMembers.TryAdd(typeSymbol.ContainingAssembly.Name, 0); 320 385 321 - result.Add(reference); 386 + result.TryAdd(reference, 0); 322 387 } 323 388 } 324 389 ··· 463 528 // Follow through the process for all parents. 464 529 foreach (var p in node.Parents) 465 530 { 531 + int nextLevel = level + 1; 532 + 466 533 // Right-bound outlier test - exclude parents greater than 3x IQR. Always expand left-bound parents as they are unlikely to cause compilation errors. 467 534 if (p.ExpansionFactor > rightBound) 535 + { 536 + logger.Add($"{(nextLevel > 0 ? $".{new string(' ', nextLevel * 2 - 1)}| " : string.Empty)} {node.ExpansionFactor} (rb: {rightBound}): {node} (!! EXCLUDED !!)"); 468 537 continue; 538 + } 469 539 470 - getReferencedFilesRecursive(p, result, seenTypes, level + 1, expansions); 540 + getReferencedFilesRecursive(p, result, seenTypes, nextLevel, expansions); 471 541 } 472 542 } 473 543 ··· 490 560 491 561 // When used via a nuget package, the local type name seems to always be more qualified than the symbol's type name. 492 562 // E.g. Type name: osu.Framework.Game, symbol name: Framework.Game. 493 - if (typeof(Game).FullName?.Contains(reference.Symbol.ToString()) == true) 563 + if (typeof(Game).FullName?.Contains(reference.ToString()) == true) 494 564 return typeInheritsFromGameCache[reference] = true; 495 565 496 566 if (reference.Symbol.BaseType == null) ··· 584 654 private readonly struct TypeReference : IEquatable<TypeReference> 585 655 { 586 656 public readonly INamedTypeSymbol Symbol; 657 + public readonly string ContainingNamespace; 658 + public readonly string SymbolName; 587 659 588 660 public TypeReference(INamedTypeSymbol symbol) 589 661 { 590 662 Symbol = symbol; 663 + ContainingNamespace = symbol.ContainingNamespace.ToString(); 664 + SymbolName = symbol.ToString(); 591 665 } 592 666 593 667 public bool Equals(TypeReference other) 594 - => Symbol.ContainingNamespace.ToString() == other.Symbol.ContainingNamespace.ToString() 595 - && Symbol.ToString() == other.Symbol.ToString(); 668 + => ContainingNamespace == other.ContainingNamespace 669 + && SymbolName == other.SymbolName; 596 670 597 671 public override int GetHashCode() 598 672 { 599 673 var hash = new HashCode(); 600 - hash.Add(Symbol.ToString(), StringComparer.Ordinal); 674 + hash.Add(SymbolName, StringComparer.Ordinal); 601 675 return hash.ToHashCode(); 602 676 } 603 677 604 - public override string ToString() => Symbol.ToString(); 678 + public override string ToString() => SymbolName; 605 679 606 680 public static TypeReference FromSymbol(INamedTypeSymbol symbol) => new TypeReference(symbol); 607 681 }
-1
osu.Framework/Testing/TestScene.cs
··· 394 394 /// Remove the "TestScene" prefix from a name. 395 395 /// </summary> 396 396 /// <param name="name"></param> 397 - /// <returns></returns> 398 397 public static string RemovePrefix(string name) 399 398 { 400 399 return name.Replace("TestCase", string.Empty) // TestScene used to be called TestCase. This handles consumer projects which haven't updated their naming for the near future.
+28 -3
osu.Framework/Threading/AudioThread.cs
··· 4 4 using osu.Framework.Statistics; 5 5 using System; 6 6 using System.Collections.Generic; 7 + using System.Diagnostics; 7 8 using ManagedBass; 8 9 using osu.Framework.Audio; 9 10 using osu.Framework.Development; ··· 45 46 }; 46 47 47 48 private readonly List<AudioManager> managers = new List<AudioManager>(); 49 + private readonly HashSet<int> initialisedDevices = new HashSet<int>(); 48 50 49 51 private static readonly GlobalStatistic<double> cpu_usage = GlobalStatistics.Get<double>("Audio", "Bass CPU%"); 50 52 ··· 62 64 } 63 65 } 64 66 65 - public void RegisterManager(AudioManager manager) 67 + internal void RegisterManager(AudioManager manager) 66 68 { 67 69 lock (managers) 68 70 { ··· 73 75 } 74 76 } 75 77 76 - public void UnregisterManager(AudioManager manager) 78 + internal void UnregisterManager(AudioManager manager) 77 79 { 78 80 lock (managers) 79 81 managers.Remove(manager); 80 82 } 81 83 84 + internal void RegisterInitialisedDevice(int deviceId) 85 + { 86 + Debug.Assert(ThreadSafety.IsAudioThread); 87 + initialisedDevices.Add(deviceId); 88 + } 89 + 82 90 protected override void PerformExit() 83 91 { 84 92 base.PerformExit(); ··· 103 111 // Safety net to ensure we have freed all devices before exiting. 104 112 // This is mainly required for device-lost scenarios. 105 113 // See https://github.com/ppy/osu-framework/pull/3378 for further discussion. 106 - while (Bass.Free()) { } 114 + foreach (var d in initialisedDevices) 115 + freeDevice(d); 116 + } 117 + 118 + private void freeDevice(int deviceId) 119 + { 120 + int lastDevice = Bass.CurrentDevice; 121 + 122 + // Freeing the 0 device on linux can cause deadlocks. This doesn't always happen immediately. 123 + // Todo: Reproduce in native code and report to BASS at some point. 124 + if (deviceId != 0 || RuntimeInfo.OS != RuntimeInfo.Platform.Linux) 125 + { 126 + Bass.CurrentDevice = deviceId; 127 + Bass.Free(); 128 + } 129 + 130 + if (lastDevice != deviceId) 131 + Bass.CurrentDevice = lastDevice; 107 132 } 108 133 } 109 134 }
+11 -3
osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #nullable enable 5 + 4 6 using System; 5 7 6 8 namespace osu.Framework.Timing ··· 33 35 /// <summary> 34 36 /// We need to be able to pass on adjustments to the source if it supports them. 35 37 /// </summary> 36 - private IAdjustableClock adjustableSource => Source as IAdjustableClock; 38 + private IAdjustableClock? adjustableSource => Source as IAdjustableClock; 37 39 38 40 public override double CurrentTime => currentTime; 39 41 ··· 52 54 public override double Rate 53 55 { 54 56 get => Source?.Rate ?? 1; 55 - set => adjustableSource.Rate = value; 57 + set 58 + { 59 + if (adjustableSource == null) 60 + throw new NotSupportedException("Source is not adjustable."); 61 + 62 + adjustableSource.Rate = value; 63 + } 56 64 } 57 65 58 66 public void ResetSpeedAdjustments() => Rate = 1; ··· 111 119 currentTime = elapsedFrameTime < 0 ? Math.Min(currentTime, proposedTime) : Math.Max(currentTime, proposedTime); 112 120 } 113 121 114 - public override void ChangeSource(IClock source) 122 + public override void ChangeSource(IClock? source) 115 123 { 116 124 if (source == null) return; 117 125
+7 -3
osu.Framework/Timing/FramedClock.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #nullable enable 5 + 4 6 using osu.Framework.Extensions.TypeExtensions; 5 7 using System; 6 8 ··· 19 21 /// </summary> 20 22 /// <param name="source">A source clock which will be used as the backing time source. If null, a StopwatchClock will be created. When provided, the CurrentTime of <paramref name="source"/> will be transferred instantly.</param> 21 23 /// <param name="processSource">Whether the source clock's <see cref="ProcessFrame"/> method should be called during this clock's process call.</param> 22 - public FramedClock(IClock source = null, bool processSource = true) 24 + public FramedClock(IClock? source = null, bool processSource = true) 23 25 { 24 26 this.processSource = processSource; 25 - ChangeSource(source ?? new StopwatchClock(true)); 27 + Source = source ?? new StopwatchClock(true); 28 + 29 + ChangeSource(Source); 26 30 } 27 31 28 32 public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; ··· 39 43 40 44 public double ElapsedFrameTime => CurrentTime - LastFrameTime; 41 45 42 - public bool IsRunning => Source?.IsRunning ?? false; 46 + public bool IsRunning => Source.IsRunning; 43 47 44 48 private readonly bool processSource; 45 49
+2
osu.Framework/Timing/FramedOffsetClock.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #nullable enable 5 + 4 6 namespace osu.Framework.Timing 5 7 { 6 8 /// <summary>
+8 -6
osu.Framework/Timing/InterpolatingFramedClock.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #nullable enable 5 + 4 6 using System; 5 7 6 8 namespace osu.Framework.Timing ··· 13 15 { 14 16 private readonly FramedClock clock = new FramedClock(new StopwatchClock(true)); 15 17 16 - public IClock Source { get; private set; } 18 + public IClock? Source { get; private set; } 17 19 18 - protected IFrameBasedClock FramedSourceClock; 20 + protected IFrameBasedClock? FramedSourceClock; 19 21 protected double LastInterpolatedTime; 20 22 protected double CurrentInterpolatedTime; 21 23 ··· 23 25 24 26 public double FramesPerSecond => 0; 25 27 26 - public virtual void ChangeSource(IClock source) 28 + public virtual void ChangeSource(IClock? source) 27 29 { 28 30 if (source != null) 29 31 { ··· 35 37 CurrentInterpolatedTime = 0; 36 38 } 37 39 38 - public InterpolatingFramedClock(IClock source = null) 40 + public InterpolatingFramedClock(IClock? source = null) 39 41 { 40 42 ChangeSource(source); 41 43 } ··· 53 55 54 56 public virtual double Rate 55 57 { 56 - get => FramedSourceClock.Rate; 58 + get => FramedSourceClock?.Rate ?? 1; 57 59 set => throw new NotSupportedException(); 58 60 } 59 61 60 62 public virtual bool IsRunning => sourceIsRunning; 61 63 62 - public virtual double Drift => CurrentTime - FramedSourceClock.CurrentTime; 64 + public virtual double Drift => CurrentTime - (FramedSourceClock?.CurrentTime ?? 0); 63 65 64 66 public virtual double ElapsedFrameTime => CurrentInterpolatedTime - LastInterpolatedTime; 65 67
+2
osu.Framework/Timing/ThrottledFrameClock.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #nullable enable 5 + 4 6 using System; 5 7 using System.Diagnostics; 6 8 using System.Threading;
-1
osu.Framework/Utils/Interpolation.cs
··· 26 26 /// <param name="final">The end value.</param> 27 27 /// <param name="base">The base of the exponential. The valid range is [0, 1], where smaller values mean that the final value is achieved more quickly, and values closer to 1 results in slow convergence to the final value.</param> 28 28 /// <param name="exponent">The exponent of the exponential. An exponent of 0 results in the start values, whereas larger exponents make the result converge to the final value.</param> 29 - /// <returns></returns> 30 29 public static double Damp(double start, double final, double @base, double exponent) 31 30 { 32 31 if (@base < 0 || @base > 1)
-1
osu.Framework/Utils/PathApproximator.cs
··· 314 314 /// Computes various properties that can be used to approximate the circular arc. 315 315 /// </summary> 316 316 /// <param name="controlPoints">Three distinct points on the arc.</param> 317 - /// <returns></returns> 318 317 private static CircularArcProperties circularArcProperties(ReadOnlySpan<Vector2> controlPoints) 319 318 { 320 319 Vector2 a = controlPoints[0];
+7 -7
osu.Framework/osu.Framework.csproj
··· 25 25 <PackageReference Include="ManagedBass.Mix" Version="2.1.0" /> 26 26 <PackageReference Include="Markdig" Version="0.22.1" /> 27 27 <PackageReference Include="FFmpeg.AutoGen" Version="4.3.0.1" /> 28 - <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.4" /> 28 + <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.5" /> 29 29 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> 30 - <PackageReference Include="NuGet.ProjectModel" Version="5.9.0" /> 30 + <PackageReference Include="NuGet.ProjectModel" Version="5.9.1" /> 31 31 <PackageReference Include="SharpFNT" Version="2.0.0" /> 32 32 <!-- Preview version of ImageSharp causes NU5104. --> 33 33 <PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" /> 34 34 <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" /> 35 - <PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.217201" /> 36 - <PackageReference Include="NUnit" Version="3.13.1" /> 35 + <PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.222201" /> 36 + <PackageReference Include="NUnit" Version="3.13.2" /> 37 37 <PackageReference Include="ManagedBass" Version="3.1.0" /> 38 38 <PackageReference Include="ManagedBass.Fx" Version="2.0.1" /> 39 39 <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> 40 - <PackageReference Include="JetBrains.Annotations" Version="2020.3.0" /> 40 + <PackageReference Include="JetBrains.Annotations" Version="2021.1.0" /> 41 41 <PackageReference Include="ppy.osuTK.NS20" Version="1.0.173" /> 42 42 <PackageReference Include="StbiSharp" Version="1.0.13" /> 43 - <PackageReference Include="ppy.SDL2-CS" Version="1.0.114-alpha" /> 43 + <PackageReference Include="ppy.SDL2-CS" Version="1.0.225-alpha" /> 44 44 </ItemGroup> 45 45 <ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETCoreApp'"> 46 46 <ProjectReference Include="..\osu.Framework.NativeLibs\osu.Framework.NativeLibs.csproj" /> 47 47 <!-- DO NOT use ProjectReference for native packaging project. 48 48 See https://github.com/NuGet/Home/issues/4514 and https://github.com/dotnet/sdk/issues/765 . --> 49 49 <PackageReference Include="Microsoft.Build.Locator" Version="1.4.1" /> 50 - <PackageReference Include="OpenTabletDriver" Version="0.5.2.3" /> 50 + <PackageReference Include="OpenTabletDriver" Version="0.5.3.1" /> 51 51 <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" /> 52 52 <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.9.0"> 53 53 <NoWarn>NU1701</NoWarn> <!-- Requires .NETFramework for MSBuild, but we use Microsoft.Build.Locator which allows this package to work in .NETCoreApp. -->