mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 16:12:19 +08:00
Merge branch 'master' into auto-addition2
This commit is contained in:
commit
5939c78760
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -133,10 +133,7 @@ jobs:
|
|||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
- name: Install .NET Workloads
|
- name: Install .NET Workloads
|
||||||
run: dotnet workload install maui-ios
|
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||||
|
|
||||||
- name: Select Xcode 16
|
|
||||||
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build -c Debug osu.iOS
|
run: dotnet build -c Debug osu.iOS
|
||||||
|
56
.github/workflows/diffcalc.yml
vendored
56
.github/workflows/diffcalc.yml
vendored
@ -104,6 +104,25 @@ env:
|
|||||||
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
master-environment:
|
||||||
|
name: Save master environment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
HEAD: ${{ steps.get-head.outputs.HEAD }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout osu
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
sparse-checkout: |
|
||||||
|
README.md
|
||||||
|
|
||||||
|
- name: Get HEAD ref
|
||||||
|
id: get-head
|
||||||
|
run: |
|
||||||
|
ref=$(git log -1 --format='%H')
|
||||||
|
echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
check-permissions:
|
check-permissions:
|
||||||
name: Check permissions
|
name: Check permissions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -121,7 +140,7 @@ jobs:
|
|||||||
|
|
||||||
create-comment:
|
create-comment:
|
||||||
name: Create PR comment
|
name: Create PR comment
|
||||||
needs: check-permissions
|
needs: [ master-environment, check-permissions ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||||
steps:
|
steps:
|
||||||
@ -158,7 +177,7 @@ jobs:
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: Setup environment
|
name: Setup environment
|
||||||
needs: directory
|
needs: [ master-environment, directory ]
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
env:
|
env:
|
||||||
VARS_JSON: ${{ toJSON(vars) }}
|
VARS_JSON: ${{ toJSON(vars) }}
|
||||||
@ -182,6 +201,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Add master environment
|
||||||
|
run: |
|
||||||
|
sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
|
||||||
- name: Add pull-request environment
|
- name: Add pull-request environment
|
||||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||||
run: |
|
run: |
|
||||||
@ -250,22 +273,23 @@ jobs:
|
|||||||
|
|
||||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Restore cache
|
- name: Restore cache
|
||||||
id: restore-cache
|
id: restore-cache
|
||||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||||
|
|
||||||
- name: Download
|
- name: Download
|
||||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||||
|
|
||||||
- name: Extract
|
- name: Extract
|
||||||
run: |
|
run: |
|
||||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
@ -281,22 +305,23 @@ jobs:
|
|||||||
|
|
||||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||||
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Restore cache
|
- name: Restore cache
|
||||||
id: restore-cache
|
id: restore-cache
|
||||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||||
|
|
||||||
- name: Download
|
- name: Download
|
||||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||||
|
|
||||||
- name: Extract
|
- name: Extract
|
||||||
run: |
|
run: |
|
||||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
@ -316,9 +341,12 @@ jobs:
|
|||||||
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
|
||||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||||
docker-compose up --build generator
|
|
||||||
|
|
||||||
link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
docker compose up --build --detach
|
||||||
|
docker compose logs --follow &
|
||||||
|
docker compose wait generator
|
||||||
|
|
||||||
|
link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
||||||
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
||||||
|
|
||||||
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
||||||
@ -328,7 +356,7 @@ jobs:
|
|||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||||
docker-compose down -v
|
docker compose down --volumes
|
||||||
|
|
||||||
output-cli:
|
output-cli:
|
||||||
name: Output info
|
name: Output info
|
||||||
@ -361,8 +389,7 @@ jobs:
|
|||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
mode: upsert
|
mode: recreate
|
||||||
create_if_not_exists: false
|
|
||||||
message: |
|
message: |
|
||||||
Target: ${{ needs.generator.outputs.TARGET }}
|
Target: ${{ needs.generator.outputs.TARGET }}
|
||||||
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
||||||
@ -372,8 +399,7 @@ jobs:
|
|||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
mode: upsert
|
mode: recreate
|
||||||
create_if_not_exists: false
|
|
||||||
message: |
|
message: |
|
||||||
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"ms-dotnettools.csharp"
|
"editorconfig.editorconfig",
|
||||||
|
"ms-dotnettools.csdevkit"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ Please make sure you have the following prerequisites:
|
|||||||
|
|
||||||
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
|
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||||
|
|
||||||
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
|
||||||
|
|
||||||
### Downloading the source code
|
### Downloading the source code
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.927.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -6,28 +6,29 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Android
|
namespace osu.Android
|
||||||
{
|
{
|
||||||
public partial class GameplayScreenRotationLocker : Component
|
public partial class GameplayScreenRotationLocker : Component
|
||||||
{
|
{
|
||||||
private Bindable<bool> localUserPlaying = null!;
|
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuGameActivity gameActivity { get; set; } = null!;
|
private OsuGameActivity gameActivity { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuGame game)
|
private void load(ILocalUserPlayInfo localUserPlayInfo)
|
||||||
{
|
{
|
||||||
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
|
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
|
||||||
localUserPlaying.BindValueChanged(updateLock, true);
|
localUserPlaying.BindValueChanged(updateLock, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLock(ValueChangedEvent<bool> userPlaying)
|
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
|
||||||
{
|
{
|
||||||
gameActivity.RunOnUiThread(() =>
|
gameActivity.RunOnUiThread(() =>
|
||||||
{
|
{
|
||||||
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
gameActivity.RequestedOrientation = userPlaying.NewValue != LocalUserPlayingState.NotPlaying ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,10 +279,12 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
||||||
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
||||||
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
// Also, spaces don't count. Because reasons, clearly.
|
||||||
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
|
// That all seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||||
if (str.Length < 2)
|
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end. After making sure to trim whitespace.
|
||||||
return str.PadRight(2, '\u200B');
|
string trimmed = str.Trim();
|
||||||
|
if (trimmed.Length < 2)
|
||||||
|
return trimmed.PadRight(2, '\u200B');
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
||||||
return str;
|
return str;
|
||||||
|
@ -25,6 +25,8 @@ namespace osu.Desktop.Updater
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
||||||
|
|
||||||
|
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
|
||||||
|
|
||||||
private UpdateInfo? pendingUpdate;
|
private UpdateInfo? pendingUpdate;
|
||||||
|
|
||||||
public VelopackUpdateManager()
|
public VelopackUpdateManager()
|
||||||
@ -43,7 +45,7 @@ namespace osu.Desktop.Updater
|
|||||||
|
|
||||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null)
|
private async Task<bool> checkForUpdateAsync()
|
||||||
{
|
{
|
||||||
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
|
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
|
||||||
bool scheduleRecheck = false;
|
bool scheduleRecheck = false;
|
||||||
@ -51,10 +53,10 @@ namespace osu.Desktop.Updater
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Avoid any kind of update checking while gameplay is running.
|
// Avoid any kind of update checking while gameplay is running.
|
||||||
if (localUserInfo?.IsPlaying.Value == true)
|
if (isInGameplay)
|
||||||
{
|
{
|
||||||
scheduleRecheck = true;
|
scheduleRecheck = true;
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
||||||
@ -84,9 +86,7 @@ namespace osu.Desktop.Updater
|
|||||||
}
|
}
|
||||||
|
|
||||||
// An update is found, let's notify the user and start downloading it.
|
// An update is found, let's notify the user and start downloading it.
|
||||||
if (notification == null)
|
UpdateProgressNotification notification = new UpdateProgressNotification
|
||||||
{
|
|
||||||
notification = new UpdateProgressNotification
|
|
||||||
{
|
{
|
||||||
CompletionClickAction = () =>
|
CompletionClickAction = () =>
|
||||||
{
|
{
|
||||||
@ -95,16 +95,13 @@ namespace osu.Desktop.Updater
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Schedule(() => notificationOverlay.Post(notification));
|
runOutsideOfGameplay(() => notificationOverlay.Post(notification));
|
||||||
}
|
|
||||||
|
|
||||||
notification.StartDownload();
|
notification.StartDownload();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||||
|
runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed);
|
||||||
notification.State = ProgressNotificationState.Completed;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -131,6 +128,17 @@ namespace osu.Desktop.Updater
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void runOutsideOfGameplay(Action action)
|
||||||
|
{
|
||||||
|
if (isInGameplay)
|
||||||
|
{
|
||||||
|
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task restartToApplyUpdate()
|
private async Task restartToApplyUpdate()
|
||||||
{
|
{
|
||||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Desktop.Windows
|
|||||||
public partial class GameplayWinKeyBlocker : Component
|
public partial class GameplayWinKeyBlocker : Component
|
||||||
{
|
{
|
||||||
private Bindable<bool> disableWinKey = null!;
|
private Bindable<bool> disableWinKey = null!;
|
||||||
private IBindable<bool> localUserPlaying = null!;
|
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
||||||
private IBindable<bool> isActive = null!;
|
private IBindable<bool> isActive = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -22,7 +22,7 @@ namespace osu.Desktop.Windows
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
|
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
|
localUserPlaying = localUserInfo.PlayingState.GetBoundCopy();
|
||||||
localUserPlaying.BindValueChanged(_ => updateBlocking());
|
localUserPlaying.BindValueChanged(_ => updateBlocking());
|
||||||
|
|
||||||
isActive = host.IsActive.GetBoundCopy();
|
isActive = host.IsActive.GetBoundCopy();
|
||||||
@ -34,7 +34,7 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
private void updateBlocking()
|
private void updateBlocking()
|
||||||
{
|
{
|
||||||
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value;
|
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value == LocalUserPlayingState.Playing;
|
||||||
|
|
||||||
if (shouldDisable)
|
if (shouldDisable)
|
||||||
host.InputThread.Scheduler.Add(WindowsKey.Disable);
|
host.InputThread.Scheduler.Add(WindowsKey.Disable);
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
@ -31,6 +32,7 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch
|
namespace osu.Game.Rulesets.Catch
|
||||||
{
|
{
|
||||||
@ -223,10 +225,28 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new DifficultySection(),
|
new DifficultySection(),
|
||||||
new ColoursSection(),
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(SetupScreen.SPACING),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ResourcesSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
|
new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Caching;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
@ -172,7 +173,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
editablePath.AddVertex(rightMouseDownPosition);
|
editablePath.AddVertex(rightMouseDownPosition);
|
||||||
});
|
})
|
||||||
|
{
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestKeyCountChange()
|
public void TestKeyCountChange()
|
||||||
{
|
{
|
||||||
LabelledSliderBar<float> keyCount = null!;
|
FormSliderBar<float> keyCount = null!;
|
||||||
|
|
||||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null);
|
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<float>>().First(), () => Is.Not.Null);
|
||||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||||
AddStep("change key count to 8", () =>
|
AddStep("change key count to 8", () =>
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
private readonly bool isForCurrentRuleset;
|
private readonly bool isForCurrentRuleset;
|
||||||
private readonly double originalOverallDifficulty;
|
private readonly double originalOverallDifficulty;
|
||||||
|
|
||||||
public override int Version => 20230817;
|
public override int Version => 20241007;
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
|
@ -19,12 +19,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
|||||||
{
|
{
|
||||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
|
private FormSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||||
private LabelledSwitchButton specialStyle { get; set; } = null!;
|
private FormCheckBox specialStyle { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Editor? editor { get; set; }
|
private Editor? editor { get; set; }
|
||||||
@ -37,77 +37,81 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
keyCountSlider = new LabelledSliderBar<float>
|
keyCountSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsCsMania,
|
Caption = BeatmapsetsStrings.ShowStatsCsMania,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = "The number of columns in the beatmap",
|
||||||
Description = "The number of columns in the beatmap",
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
specialStyle = new LabelledSwitchButton
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
specialStyle = new FormCheckBox
|
||||||
{
|
{
|
||||||
Label = "Use special (N+1) style",
|
Caption = "Use special (N+1) style",
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
|
||||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||||
},
|
},
|
||||||
healthDrainSlider = new LabelledSliderBar<float>
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
Description = EditorSetupStrings.DrainRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overallDifficultySlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseVelocitySlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.BaseVelocity,
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
{
|
{
|
||||||
Default = 1.4,
|
Default = 1.4,
|
||||||
MinValue = 0.4,
|
MinValue = 0.4,
|
||||||
MaxValue = 3.6,
|
MaxValue = 3.6,
|
||||||
Precision = 0.01f,
|
Precision = 0.01f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tickRateSlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.TickRate,
|
Caption = EditorSetupStrings.TickRate,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
Description = EditorSetupStrings.TickRateDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
{
|
{
|
||||||
Default = 1,
|
Default = 1,
|
||||||
MinValue = 1,
|
MinValue = 1,
|
||||||
MaxValue = 4,
|
MaxValue = 4,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -419,9 +419,12 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
return new ManiaFilterCriteria();
|
return new ManiaFilterCriteria();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new ManiaDifficultySection(),
|
new ManiaDifficultySection(),
|
||||||
|
new ResourcesSection(),
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
{
|
{
|
||||||
@ -10,5 +21,66 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
public partial class TestSceneOsuEditor : EditorTestScene
|
public partial class TestSceneOsuEditor : EditorTestScene
|
||||||
{
|
{
|
||||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchInputAfterTouchingComposeArea()
|
||||||
|
{
|
||||||
|
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
|
||||||
|
|
||||||
|
// this input is just for interacting with compose area
|
||||||
|
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||||
|
|
||||||
|
AddStep("move current time", () => InputManager.Key(Key.Right));
|
||||||
|
|
||||||
|
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(10, 10))));
|
||||||
|
AddAssert("circle placed correctly", () =>
|
||||||
|
{
|
||||||
|
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f));
|
||||||
|
Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f));
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
|
||||||
|
|
||||||
|
// this input is just for interacting with compose area
|
||||||
|
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||||
|
|
||||||
|
AddStep("move current time", () => InputManager.Key(Key.Right));
|
||||||
|
|
||||||
|
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
|
||||||
|
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
|
||||||
|
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
|
||||||
|
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
|
||||||
|
AddAssert("slider placed correctly", () =>
|
||||||
|
{
|
||||||
|
var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f));
|
||||||
|
Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f));
|
||||||
|
Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||||
|
|
||||||
|
// the final position may be slightly off from the mouse position when drawing, account for that.
|
||||||
|
Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5));
|
||||||
|
Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre);
|
||||||
|
|
||||||
|
private void tap(Vector2 position)
|
||||||
|
{
|
||||||
|
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
|
||||||
|
InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
||||||
{
|
{
|
||||||
|
AddAssert($"grid type is {typeof(T).Name}", () => this.ChildrenOfType<T>().Any());
|
||||||
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
||||||
AddStep("move cursor to spacing + (1, 1)", () =>
|
AddStep("move cursor to spacing + (1, 1)", () =>
|
||||||
{
|
{
|
||||||
@ -161,7 +163,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
return grid switch
|
return grid switch
|
||||||
{
|
{
|
||||||
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
||||||
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
|
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(
|
||||||
|
new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
|
||||||
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
|
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
|
||||||
_ => Vector2.Zero
|
_ => Vector2.Zero
|
||||||
};
|
};
|
||||||
@ -170,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGridSizeToggling()
|
public void TestGridSizeToggling()
|
||||||
{
|
{
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
gridSizeIs(4);
|
gridSizeIs(4);
|
||||||
|
|
||||||
@ -189,5 +192,97 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
private void gridSizeIs(int size)
|
private void gridSizeIs(int size)
|
||||||
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
||||||
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridTypeToggling()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
|
nextGridTypeIs<TriangularPositionSnapGrid>();
|
||||||
|
nextGridTypeIs<CircularPositionSnapGrid>();
|
||||||
|
nextGridTypeIs<RectangularPositionSnapGrid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextGridTypeIs<T>() where T : PositionSnapGrid
|
||||||
|
{
|
||||||
|
AddStep("toggle to next grid type", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
|
InputManager.Key(Key.G);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
|
gridActive<T>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridPlacementTool()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
|
||||||
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
|
AddStep("move cursor to slider head + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).Position + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddStep("move cursor to slider tail + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
AddAssert("grid position at slider head", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).Position, composer.StartPosition.Value);
|
||||||
|
});
|
||||||
|
AddAssert("grid spacing is distance to slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||||
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
|
||||||
|
});
|
||||||
|
AddAssert("grid rotation points to slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
|
AddStep("move cursor to slider tail + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("double click", () =>
|
||||||
|
{
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("move cursor to (0, 0)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(Vector2.Zero));
|
||||||
|
});
|
||||||
|
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
AddAssert("grid position at slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).EndPosition, composer.StartPosition.Value);
|
||||||
|
});
|
||||||
|
AddAssert("grid spacing and rotation unchanged", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||||
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
|
||||||
|
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@ -392,6 +393,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
assertFinalControlPointType(3, null);
|
assertFinalControlPointType(3, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderDrawingViaTouch()
|
||||||
|
{
|
||||||
|
Vector2 startPoint = new Vector2(200);
|
||||||
|
|
||||||
|
AddStep("move mouse to a random point", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(Vector2.Zero)));
|
||||||
|
AddStep("begin touch at start point", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, InputManager.ToScreenSpace(startPoint))));
|
||||||
|
|
||||||
|
for (int i = 1; i < 20; i++)
|
||||||
|
addTouchMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50));
|
||||||
|
|
||||||
|
AddStep("release touch at end point", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertLength(808, tolerance: 10);
|
||||||
|
assertControlPointCount(5);
|
||||||
|
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||||
|
assertFinalControlPointType(1, null);
|
||||||
|
assertFinalControlPointType(2, null);
|
||||||
|
assertFinalControlPointType(3, null);
|
||||||
|
assertFinalControlPointType(4, null);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
|
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
|
||||||
{
|
{
|
||||||
@ -492,6 +516,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||||
|
|
||||||
|
private void addTouchMovementStep(Vector2 position) => AddStep($"move touch1 to {position}", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, InputManager.ToScreenSpace(position))));
|
||||||
|
|
||||||
private void addClickStep(MouseButton button)
|
private void addClickStep(MouseButton button)
|
||||||
{
|
{
|
||||||
AddStep($"click {button}", () => InputManager.Click(button));
|
AddStep($"click {button}", () => InputManager.Click(button));
|
||||||
|
64
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs
Normal file
64
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneOsuModMirror : OsuModTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestCorrectReflections([Values] OsuModMirror.MirrorType type, [Values] bool withStrictTracking) => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Autoplay = true,
|
||||||
|
Beatmap = new OsuBeatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
Position = new Vector2(0),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(100, 0))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TickDistanceMultiplier = 0.5,
|
||||||
|
RepeatCount = 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Mods = withStrictTracking
|
||||||
|
? [new OsuModMirror { Reflection = { Value = type } }, new OsuModStrictTracking()]
|
||||||
|
: [new OsuModMirror { Reflection = { Value = type } }],
|
||||||
|
PassCondition = () =>
|
||||||
|
{
|
||||||
|
var slider = this.ChildrenOfType<DrawableSlider>().SingleOrDefault();
|
||||||
|
var playfield = this.ChildrenOfType<OsuPlayfield>().Single();
|
||||||
|
|
||||||
|
if (slider == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Precision.AlmostEquals(playfield.ToLocalSpace(slider.HeadCircle.ScreenSpaceDrawQuad.Centre), slider.HitObject.Position)
|
||||||
|
&& Precision.AlmostEquals(playfield.ToLocalSpace(slider.TailCircle.ScreenSpaceDrawQuad.Centre), slider.HitObject.Position)
|
||||||
|
&& Precision.AlmostEquals(playfield.ToLocalSpace(slider.NestedHitObjects.OfType<DrawableSliderRepeat>().Single().ScreenSpaceDrawQuad.Centre),
|
||||||
|
slider.HitObject.Position + slider.HitObject.Path.PositionAt(1))
|
||||||
|
&& Precision.AlmostEquals(playfield.ToLocalSpace(slider.NestedHitObjects.OfType<DrawableSliderTick>().First().ScreenSpaceDrawQuad.Centre),
|
||||||
|
slider.HitObject.Position + slider.HitObject.Path.PositionAt(0.7f));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||||
|
|
||||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||||
[TestCase(0.14102693012101306d, 2, "nan-slider")]
|
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(8.9742952703071666d, 239, "diffcalc-test")]
|
[TestCase(8.9825709931204205d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.743180218215227d, 54, "zero-length-sliders")]
|
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.55071082800473514d, 4, "very-fast-slider")]
|
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -10,8 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
{
|
{
|
||||||
public static class RhythmEvaluator
|
public static class RhythmEvaluator
|
||||||
{
|
{
|
||||||
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
|
private const int history_time_max = 5 * 1000; // 5 seconds
|
||||||
private const double rhythm_multiplier = 0.75;
|
private const int history_objects_max = 32;
|
||||||
|
private const double rhythm_overall_multiplier = 0.95;
|
||||||
|
private const double rhythm_ratio_multiplier = 12.0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||||
@ -21,15 +25,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
if (current.BaseObject is Spinner)
|
if (current.BaseObject is Spinner)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
int previousIslandSize = 0;
|
|
||||||
|
|
||||||
double rhythmComplexitySum = 0;
|
double rhythmComplexitySum = 0;
|
||||||
int islandSize = 1;
|
|
||||||
|
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
|
||||||
|
|
||||||
|
var island = new Island(deltaDifferenceEpsilon);
|
||||||
|
var previousIsland = new Island(deltaDifferenceEpsilon);
|
||||||
|
|
||||||
|
// we can't use dictionary here because we need to compare island with a tolerance
|
||||||
|
// which is impossible to pass into the hash comparer
|
||||||
|
var islandCounts = new List<(Island Island, int Count)>();
|
||||||
|
|
||||||
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
||||||
|
|
||||||
bool firstDeltaSwitch = false;
|
bool firstDeltaSwitch = false;
|
||||||
|
|
||||||
int historicalNoteCount = Math.Min(current.Index, 32);
|
int historicalNoteCount = Math.Min(current.Index, history_objects_max);
|
||||||
|
|
||||||
int rhythmStart = 0;
|
int rhythmStart = 0;
|
||||||
|
|
||||||
@ -39,74 +50,177 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
||||||
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
|
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
|
||||||
|
|
||||||
|
// we go from the furthest object back to the current one
|
||||||
for (int i = rhythmStart; i > 0; i--)
|
for (int i = rhythmStart; i > 0; i--)
|
||||||
{
|
{
|
||||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
||||||
|
|
||||||
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
// scales note 0 to 1 from history to now
|
||||||
|
double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max;
|
||||||
|
double noteDecay = (double)(historicalNoteCount - i) / historicalNoteCount;
|
||||||
|
|
||||||
currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count.
|
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
||||||
|
|
||||||
double currDelta = currObj.StrainTime;
|
double currDelta = currObj.StrainTime;
|
||||||
double prevDelta = prevObj.StrainTime;
|
double prevDelta = prevObj.StrainTime;
|
||||||
double lastDelta = lastObj.StrainTime;
|
double lastDelta = lastObj.StrainTime;
|
||||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
|
||||||
|
|
||||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3));
|
// calculate how much current delta difference deserves a rhythm bonus
|
||||||
|
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||||
|
double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta);
|
||||||
|
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2));
|
||||||
|
|
||||||
windowPenalty = Math.Min(1, windowPenalty);
|
// reduce ratio bonus if delta difference is too big
|
||||||
|
double fraction = Math.Max(prevDelta / currDelta, currDelta / prevDelta);
|
||||||
|
double fractionMultiplier = Math.Clamp(2.0 - fraction / 8.0, 0.0, 1.0);
|
||||||
|
|
||||||
double effectiveRatio = windowPenalty * currRatio;
|
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
||||||
|
|
||||||
|
double effectiveRatio = windowPenalty * currRatio * fractionMultiplier;
|
||||||
|
|
||||||
if (firstDeltaSwitch)
|
if (firstDeltaSwitch)
|
||||||
{
|
{
|
||||||
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
||||||
{
|
{
|
||||||
if (islandSize < 7)
|
// island is still progressing
|
||||||
islandSize++; // island is still progressing, count size.
|
island.AddDelta((int)currDelta);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
// bpm change is into slider, this is easy acc window
|
||||||
|
if (currObj.BaseObject is Slider)
|
||||||
effectiveRatio *= 0.125;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
effectiveRatio *= 0.25;
|
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
|
if (prevObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.3;
|
||||||
|
|
||||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||||
effectiveRatio *= 0.25;
|
if (island.IsSimilarPolarity(previousIsland))
|
||||||
|
effectiveRatio *= 0.5;
|
||||||
|
|
||||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
// previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||||
effectiveRatio *= 0.50;
|
if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon)
|
||||||
|
|
||||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
|
||||||
effectiveRatio *= 0.125;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
// repeated island size (ex: triplet -> triplet)
|
||||||
|
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
||||||
|
if (previousIsland.DeltaCount == island.DeltaCount)
|
||||||
|
effectiveRatio *= 0.5;
|
||||||
|
|
||||||
|
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
|
||||||
|
|
||||||
|
if (islandCount != default)
|
||||||
|
{
|
||||||
|
int countIndex = islandCounts.IndexOf(islandCount);
|
||||||
|
|
||||||
|
// only add island to island counts if they're going one after another
|
||||||
|
if (previousIsland.Equals(island))
|
||||||
|
islandCount.Count++;
|
||||||
|
|
||||||
|
// repeated island (ex: triplet -> triplet)
|
||||||
|
double power = logistic(island.Delta, 2.75, 0.24, 14);
|
||||||
|
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
|
||||||
|
|
||||||
|
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
islandCounts.Add((island, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// scale down the difficulty if the object is doubletappable
|
||||||
|
double doubletapness = prevObj.GetDoubletapness(currObj);
|
||||||
|
effectiveRatio *= 1 - doubletapness * 0.75;
|
||||||
|
|
||||||
|
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay;
|
||||||
|
|
||||||
startRatio = effectiveRatio;
|
startRatio = effectiveRatio;
|
||||||
|
|
||||||
previousIslandSize = islandSize; // log the last island size.
|
previousIsland = island;
|
||||||
|
|
||||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
if (prevDelta + deltaDifferenceEpsilon < currDelta) // we're slowing down, stop counting
|
||||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||||
|
|
||||||
islandSize = 1;
|
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
else if (prevDelta > currDelta + deltaDifferenceEpsilon) // we're speeding up
|
||||||
{
|
{
|
||||||
// Begin counting island until we change speed again.
|
// Begin counting island until we change speed again.
|
||||||
firstDeltaSwitch = true;
|
firstDeltaSwitch = true;
|
||||||
|
|
||||||
|
// bpm change is into slider, this is easy acc window
|
||||||
|
if (currObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.6;
|
||||||
|
|
||||||
|
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
|
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
|
if (prevObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.6;
|
||||||
|
|
||||||
startRatio = effectiveRatio;
|
startRatio = effectiveRatio;
|
||||||
islandSize = 1;
|
|
||||||
|
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastObj = prevObj;
|
lastObj = prevObj;
|
||||||
prevObj = currObj;
|
prevObj = currObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
|
||||||
|
|
||||||
|
private class Island : IEquatable<Island>
|
||||||
|
{
|
||||||
|
private readonly double deltaDifferenceEpsilon;
|
||||||
|
|
||||||
|
public Island(double epsilon)
|
||||||
|
{
|
||||||
|
deltaDifferenceEpsilon = epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Island(int delta, double epsilon)
|
||||||
|
{
|
||||||
|
deltaDifferenceEpsilon = epsilon;
|
||||||
|
Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME);
|
||||||
|
DeltaCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Delta { get; private set; } = int.MaxValue;
|
||||||
|
public int DeltaCount { get; private set; }
|
||||||
|
|
||||||
|
public void AddDelta(int delta)
|
||||||
|
{
|
||||||
|
if (Delta == int.MaxValue)
|
||||||
|
Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME);
|
||||||
|
|
||||||
|
DeltaCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSimilarPolarity(Island other)
|
||||||
|
{
|
||||||
|
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
||||||
|
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
||||||
|
return DeltaCount % 2 == other.DeltaCount % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(Island? other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon &&
|
||||||
|
DeltaCount == other.DeltaCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Delta}x{DeltaCount}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
||||||
private const double min_speed_bonus = 75; // ~200BPM
|
private const double min_speed_bonus = 75; // ~200BPM
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
|
private const double distance_multiplier = 0.94;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates the difficulty of tapping the current object, based on:
|
/// Evaluates the difficulty of tapping the current object, based on:
|
||||||
@ -30,32 +31,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
// derive strainTime for calculation
|
// derive strainTime for calculation
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||||
var osuNextObj = (OsuDifficultyHitObject?)current.Next(0);
|
|
||||||
|
|
||||||
double strainTime = osuCurrObj.StrainTime;
|
double strainTime = osuCurrObj.StrainTime;
|
||||||
double doubletapness = 1;
|
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
||||||
|
|
||||||
// Nerf doubletappable doubles.
|
|
||||||
if (osuNextObj != null)
|
|
||||||
{
|
|
||||||
double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime);
|
|
||||||
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
|
||||||
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
|
||||||
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
|
||||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / osuCurrObj.HitWindowGreat), 2);
|
|
||||||
doubletapness = Math.Pow(speedRatio, 1 - windowRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap deltatime to the OD 300 hitwindow.
|
// Cap deltatime to the OD 300 hitwindow.
|
||||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||||
|
|
||||||
// speedBonus will be 1.0 for BPM < 200
|
// speedBonus will be 0.0 for BPM < 200
|
||||||
double speedBonus = 1.0;
|
double speedBonus = 0.0;
|
||||||
|
|
||||||
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||||
if (strainTime < min_speed_bonus)
|
if (strainTime < min_speed_bonus)
|
||||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||||
|
|
||||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||||
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||||
@ -63,11 +52,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
// Cap distance at single_spacing_threshold
|
// Cap distance at single_spacing_threshold
|
||||||
distance = Math.Min(distance, single_spacing_threshold);
|
distance = Math.Min(distance, single_spacing_threshold);
|
||||||
|
|
||||||
// Max distance bonus is 2 at single_spacing_threshold
|
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||||
double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5);
|
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||||
|
|
||||||
// Base difficulty with all bonuses
|
// Base difficulty with all bonuses
|
||||||
double difficulty = speedBonus * distanceBonus * 1000 / strainTime;
|
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||||
|
|
||||||
// Apply penalty if there's doubletappable doubles
|
// Apply penalty if there's doubletappable doubles
|
||||||
return difficulty * doubletapness;
|
return difficulty * doubletapness;
|
||||||
|
@ -46,6 +46,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("slider_factor")]
|
[JsonProperty("slider_factor")]
|
||||||
public double SliderFactor { get; set; }
|
public double SliderFactor { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("aim_difficult_strain_count")]
|
||||||
|
public double AimDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("speed_difficult_strain_count")]
|
||||||
|
public double SpeedDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -99,6 +105,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||||
|
|
||||||
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
|
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
|
||||||
|
|
||||||
|
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
|
||||||
|
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,8 +122,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
|
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||||
|
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||||
|
|
||||||
DrainRate = onlineInfo.DrainRate;
|
DrainRate = onlineInfo.DrainRate;
|
||||||
HitCircleCount = onlineInfo.CircleCount;
|
HitCircleCount = onlineInfo.CircleCount;
|
||||||
SliderCount = onlineInfo.SliderCount;
|
SliderCount = onlineInfo.SliderCount;
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 0.0675;
|
private const double difficulty_multiplier = 0.0675;
|
||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20241007;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
@ -48,6 +48,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||||
|
|
||||||
|
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains();
|
||||||
|
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains();
|
||||||
|
|
||||||
if (mods.Any(m => m is OsuModTouchDevice))
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
{
|
{
|
||||||
aimRating = Math.Pow(aimRating, 0.8);
|
aimRating = Math.Pow(aimRating, 0.8);
|
||||||
@ -100,6 +103,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedNoteCount = speedNotes,
|
SpeedNoteCount = speedNotes,
|
||||||
FlashlightDifficulty = flashlightRating,
|
FlashlightDifficulty = flashlightRating,
|
||||||
SliderFactor = sliderFactor,
|
SliderFactor = sliderFactor,
|
||||||
|
AimDifficultStrainCount = aimDifficultyStrainCount,
|
||||||
|
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||||
DrainRate = drainRate,
|
DrainRate = drainRate,
|
||||||
|
@ -14,7 +14,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||||
{
|
{
|
||||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
|
|
||||||
|
private bool usingClassicSliderAccuracy;
|
||||||
|
|
||||||
private double accuracy;
|
private double accuracy;
|
||||||
private int scoreMaxCombo;
|
private int scoreMaxCombo;
|
||||||
@ -23,6 +25,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Missed slider ticks that includes missed reverse arrows. Will only be correct on non-classic scores
|
||||||
|
/// </summary>
|
||||||
|
private int countSliderTickMiss;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of missed slider tails that don't break combo. Will only be correct on non-classic scores
|
||||||
|
/// </summary>
|
||||||
|
private int countSliderEndsDropped;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimated total amount of combo breaks
|
||||||
|
/// </summary>
|
||||||
private double effectiveMissCount;
|
private double effectiveMissCount;
|
||||||
|
|
||||||
public OsuPerformanceCalculator()
|
public OsuPerformanceCalculator()
|
||||||
@ -34,13 +49,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
||||||
|
|
||||||
|
usingClassicSliderAccuracy = score.Mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);
|
||||||
|
|
||||||
accuracy = score.Accuracy;
|
accuracy = score.Accuracy;
|
||||||
scoreMaxCombo = score.MaxCombo;
|
scoreMaxCombo = score.MaxCombo;
|
||||||
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
|
countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
|
||||||
|
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||||
|
|
||||||
|
if (osuAttributes.SliderCount > 0)
|
||||||
|
{
|
||||||
|
if (usingClassicSliderAccuracy)
|
||||||
|
{
|
||||||
|
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||||
|
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
|
||||||
|
|
||||||
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
|
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
|
|
||||||
|
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||||
|
effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
|
||||||
|
|
||||||
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
|
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
|
|
||||||
|
// Combine regular misses with tick misses since tick misses break combo as well
|
||||||
|
effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
||||||
|
|
||||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||||
|
|
||||||
@ -93,11 +139,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
aimValue *= lengthBonus;
|
aimValue *= lengthBonus;
|
||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
|
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
||||||
|
|
||||||
aimValue *= getComboScalingFactor(attributes);
|
|
||||||
|
|
||||||
double approachRateFactor = 0.0;
|
double approachRateFactor = 0.0;
|
||||||
if (attributes.ApproachRate > 10.33)
|
if (attributes.ApproachRate > 10.33)
|
||||||
@ -123,8 +166,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
if (attributes.SliderCount > 0)
|
if (attributes.SliderCount > 0)
|
||||||
{
|
{
|
||||||
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
double estimateImproperlyFollowedDifficultSliders;
|
||||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
|
||||||
|
if (usingClassicSliderAccuracy)
|
||||||
|
{
|
||||||
|
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||||
|
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||||
|
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||||
|
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||||
|
estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
|
||||||
|
}
|
||||||
|
|
||||||
|
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||||
aimValue *= sliderNerfFactor;
|
aimValue *= sliderNerfFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,11 +203,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
speedValue *= lengthBonus;
|
speedValue *= lengthBonus;
|
||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
||||||
|
|
||||||
speedValue *= getComboScalingFactor(attributes);
|
|
||||||
|
|
||||||
double approachRateFactor = 0.0;
|
double approachRateFactor = 0.0;
|
||||||
if (attributes.ApproachRate > 10.33)
|
if (attributes.ApproachRate > 10.33)
|
||||||
@ -177,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||||
|
|
||||||
// Scale the speed value with accuracy and OD.
|
// Scale the speed value with accuracy and OD.
|
||||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||||
|
|
||||||
// Scale the speed value with # of 50s to punish doubletapping.
|
// Scale the speed value with # of 50s to punish doubletapping.
|
||||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||||
@ -193,6 +247,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||||
double betterAccuracyPercentage;
|
double betterAccuracyPercentage;
|
||||||
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
||||||
|
if (!usingClassicSliderAccuracy)
|
||||||
|
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||||
|
|
||||||
if (amountHitObjectsWithAccuracy > 0)
|
if (amountHitObjectsWithAccuracy > 0)
|
||||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||||
@ -247,25 +303,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return flashlightValue;
|
return flashlightValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
|
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||||
{
|
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||||
// Guess the number of misses + slider breaks from combo
|
// to make it more punishing on maps with lower amount of hard sections.
|
||||||
double comboBasedMissCount = 0.0;
|
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||||
|
|
||||||
if (attributes.SliderCount > 0)
|
|
||||||
{
|
|
||||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
|
||||||
if (scoreMaxCombo < fullComboThreshold)
|
|
||||||
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp miss count to maximum amount of possible breaks
|
|
||||||
comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
|
|
||||||
|
|
||||||
return Math.Max(countMiss, comboBasedMissCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
|
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||||
|
|
||||||
private const int min_delta_time = 25;
|
public const int MIN_DELTA_TIME = 25;
|
||||||
|
|
||||||
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
|
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
|
||||||
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
this.lastObject = (OsuHitObject)lastObject;
|
this.lastObject = (OsuHitObject)lastObject;
|
||||||
|
|
||||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||||
StrainTime = Math.Max(DeltaTime, min_delta_time);
|
StrainTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
||||||
|
|
||||||
if (BaseObject is Slider sliderObject)
|
if (BaseObject is Slider sliderObject)
|
||||||
{
|
{
|
||||||
@ -136,6 +137,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
|
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns how possible is it to doubletap this object together with the next one and get perfect judgement in range from 0 to 1
|
||||||
|
/// </summary>
|
||||||
|
public double GetDoubletapness(OsuDifficultyHitObject? osuNextObj)
|
||||||
|
{
|
||||||
|
if (osuNextObj != null)
|
||||||
|
{
|
||||||
|
double currDeltaTime = Math.Max(1, DeltaTime);
|
||||||
|
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
||||||
|
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
||||||
|
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
||||||
|
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindowGreat), 2);
|
||||||
|
return 1.0 - Math.Pow(speedRatio, 1 - windowRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private void setDistances(double clockRate)
|
private void setDistances(double clockRate)
|
||||||
{
|
{
|
||||||
if (BaseObject is Slider currentSlider)
|
if (BaseObject is Slider currentSlider)
|
||||||
@ -143,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
computeSliderCursorPosition(currentSlider);
|
computeSliderCursorPosition(currentSlider);
|
||||||
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||||
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
||||||
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
|
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||||
@ -167,8 +186,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
if (lastObject is Slider lastSlider)
|
if (lastObject is Slider lastSlider)
|
||||||
{
|
{
|
||||||
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
|
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
|
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, MIN_DELTA_TIME);
|
||||||
|
|
||||||
//
|
//
|
||||||
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
|
||||||
private double skillMultiplier => 24.963;
|
private double skillMultiplier => 25.18;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||||
|
ObjectStrains.Add(currentStrain);
|
||||||
|
|
||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual double ReducedStrainBaseline => 0.75;
|
protected virtual double ReducedStrainBaseline => 0.75;
|
||||||
|
|
||||||
|
protected List<double> ObjectStrains = new List<double>();
|
||||||
|
protected double Difficulty;
|
||||||
|
|
||||||
protected OsuStrainSkill(Mod[] mods)
|
protected OsuStrainSkill(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
@ -30,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
public override double DifficultyValue()
|
public override double DifficultyValue()
|
||||||
{
|
{
|
||||||
double difficulty = 0;
|
Difficulty = 0;
|
||||||
double weight = 1;
|
double weight = 1;
|
||||||
|
|
||||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
@ -50,11 +53,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
// We're sorting from highest to lowest strain.
|
// We're sorting from highest to lowest strain.
|
||||||
foreach (double strain in strains.OrderDescending())
|
foreach (double strain in strains.OrderDescending())
|
||||||
{
|
{
|
||||||
difficulty += strain * weight;
|
Difficulty += strain * weight;
|
||||||
weight *= DecayWeight;
|
weight *= DecayWeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return difficulty;
|
return Difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of strains weighted against the top strain.
|
||||||
|
/// The result is scaled by clock rate as it affects the total number of strains.
|
||||||
|
/// </summary>
|
||||||
|
public double CountDifficultStrains()
|
||||||
|
{
|
||||||
|
if (Difficulty == 0)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
|
||||||
|
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||||
|
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||||
|
@ -6,7 +6,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override int ReducedSectionCount => 5;
|
protected override int ReducedSectionCount => 5;
|
||||||
|
|
||||||
private readonly List<double> objectStrains = new List<double>();
|
|
||||||
|
|
||||||
public Speed(Mod[] mods)
|
public Speed(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
@ -43,23 +40,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||||
|
|
||||||
double totalStrain = currentStrain * currentRhythm;
|
double totalStrain = currentStrain * currentRhythm;
|
||||||
|
ObjectStrains.Add(totalStrain);
|
||||||
objectStrains.Add(totalStrain);
|
|
||||||
|
|
||||||
return totalStrain;
|
return totalStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double RelevantNoteCount()
|
public double RelevantNoteCount()
|
||||||
{
|
{
|
||||||
if (objectStrains.Count == 0)
|
if (ObjectStrains.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double maxStrain = objectStrains.Max();
|
double maxStrain = ObjectStrains.Max();
|
||||||
|
|
||||||
if (maxStrain == 0)
|
if (maxStrain == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs
Normal file
126
osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||||
|
{
|
||||||
|
public partial class GridPlacementBlueprint : PlacementBlueprint
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private HitObjectComposer? hitObjectComposer { get; set; }
|
||||||
|
|
||||||
|
private OsuGridToolboxGroup gridToolboxGroup = null!;
|
||||||
|
private Vector2 originalOrigin;
|
||||||
|
private float originalSpacing;
|
||||||
|
private float originalRotation;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuGridToolboxGroup gridToolboxGroup)
|
||||||
|
{
|
||||||
|
this.gridToolboxGroup = gridToolboxGroup;
|
||||||
|
originalOrigin = gridToolboxGroup.StartPosition.Value;
|
||||||
|
originalSpacing = gridToolboxGroup.Spacing.Value;
|
||||||
|
originalRotation = gridToolboxGroup.GridLinesRotation.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void EndPlacement(bool commit)
|
||||||
|
{
|
||||||
|
if (!commit && PlacementActive != PlacementState.Finished)
|
||||||
|
{
|
||||||
|
gridToolboxGroup.StartPosition.Value = originalOrigin;
|
||||||
|
gridToolboxGroup.Spacing.Value = originalSpacing;
|
||||||
|
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||||
|
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.EndPlacement(commit);
|
||||||
|
|
||||||
|
// You typically only place the grid once, so we switch back to the last tool after placement.
|
||||||
|
if (commit && hitObjectComposer is OsuHitObjectComposer osuHitObjectComposer)
|
||||||
|
osuHitObjectComposer.SetLastTool();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButton.Left)
|
||||||
|
{
|
||||||
|
switch (PlacementActive)
|
||||||
|
{
|
||||||
|
case PlacementState.Waiting:
|
||||||
|
BeginPlacement(true);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlacementState.Active:
|
||||||
|
EndPlacement(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnClick(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButton.Right)
|
||||||
|
{
|
||||||
|
// Reset the grid to the default values.
|
||||||
|
gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default;
|
||||||
|
gridToolboxGroup.Spacing.Value = gridToolboxGroup.Spacing.Default;
|
||||||
|
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||||
|
gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default;
|
||||||
|
EndPlacement(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnMouseDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButton.Left)
|
||||||
|
{
|
||||||
|
BeginPlacement(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnDragStart(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
{
|
||||||
|
if (PlacementActive == PlacementState.Active)
|
||||||
|
EndPlacement(true);
|
||||||
|
|
||||||
|
base.OnDragEnd(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SnapType SnapType => ~SnapType.GlobalGrids;
|
||||||
|
|
||||||
|
public override void UpdateTimeAndPosition(SnapResult result)
|
||||||
|
{
|
||||||
|
var pos = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
|
|
||||||
|
if (PlacementActive != PlacementState.Active)
|
||||||
|
gridToolboxGroup.StartPosition.Value = pos;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default to the original spacing and rotation if the distance is too small.
|
||||||
|
if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2)
|
||||||
|
{
|
||||||
|
gridToolboxGroup.Spacing.Value = originalSpacing;
|
||||||
|
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||||
|
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (segment.Count == 0)
|
if (segment.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var first = segment[0];
|
PathControlPoint first = segment[0];
|
||||||
|
|
||||||
if (first.Type != PathType.PERFECT_CURVE)
|
if (first.Type != PathType.PERFECT_CURVE)
|
||||||
return;
|
return;
|
||||||
@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
private bool isSplittable(PathControlPointPiece<T> p) =>
|
private bool isSplittable(PathControlPointPiece<T> p) =>
|
||||||
// A hit object can only be split on control points which connect two different path segments.
|
// A hit object can only be split on control points which connect two different path segments.
|
||||||
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
|
p.ControlPoint.Type.HasValue && p.ControlPoint != controlPoints.FirstOrDefault() && p.ControlPoint != controlPoints.LastOrDefault();
|
||||||
|
|
||||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (selectedPieces.Length != 1)
|
if (selectedPieces.Length != 1)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var selectedPiece = selectedPieces.Single();
|
PathControlPointPiece<T> selectedPiece = selectedPieces.Single();
|
||||||
var selectedPoint = selectedPiece.ControlPoint;
|
PathControlPoint selectedPoint = selectedPiece.ControlPoint;
|
||||||
|
|
||||||
var validTypes = path_types;
|
PathType?[] validTypes = path_types;
|
||||||
|
|
||||||
if (selectedPoint == controlPoints[0])
|
if (selectedPoint == controlPoints[0])
|
||||||
validTypes = validTypes.Where(t => t != null).ToArray();
|
validTypes = validTypes.Where(t => t != null).ToArray();
|
||||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (Pieces.All(p => !p.IsSelected.Value))
|
if (Pieces.All(p => !p.IsSelected.Value))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var type = path_types[e.Key - Key.Number1];
|
PathType? type = path_types[e.Key - Key.Number1];
|
||||||
|
|
||||||
// The first control point can never be inherit type
|
// The first control point can never be inherit type
|
||||||
if (Pieces[0].IsSelected.Value && type == null)
|
if (Pieces[0].IsSelected.Value && type == null)
|
||||||
@ -357,7 +357,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||||
{
|
{
|
||||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
List<PathControlPoint> pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||||
|
|
||||||
if (type?.Type == SplineType.PerfectCurve)
|
if (type?.Type == SplineType.PerfectCurve)
|
||||||
@ -412,14 +412,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public void DragInProgress(DragEvent e)
|
public void DragInProgress(DragEvent e)
|
||||||
{
|
{
|
||||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||||
var oldPosition = hitObject.Position;
|
Vector2 oldPosition = hitObject.Position;
|
||||||
double oldStartTime = hitObject.StartTime;
|
double oldStartTime = hitObject.StartTime;
|
||||||
|
|
||||||
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
||||||
{
|
{
|
||||||
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
||||||
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||||
|
|
||||||
@ -428,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||||
{
|
{
|
||||||
var controlPoint = hitObject.Path.ControlPoints[i];
|
PathControlPoint controlPoint = hitObject.Path.ControlPoints[i];
|
||||||
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
||||||
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
||||||
// All other selected control points (if any) will move together with the head point
|
// All other selected control points (if any) will move together with the head point
|
||||||
@ -439,13 +439,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||||
|
|
||||||
for (int i = 0; i < controlPoints.Count; ++i)
|
for (int i = 0; i < controlPoints.Count; ++i)
|
||||||
{
|
{
|
||||||
var controlPoint = controlPoints[i];
|
PathControlPoint controlPoint = controlPoints[i];
|
||||||
if (selectedControlPoints.Contains(controlPoint))
|
if (selectedControlPoints.Contains(controlPoint))
|
||||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||||
}
|
}
|
||||||
@ -495,8 +495,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
curveTypeItems = new List<MenuItem>();
|
curveTypeItems = new List<MenuItem>();
|
||||||
|
|
||||||
foreach (PathType? type in path_types)
|
for (int i = 0; i < path_types.Length; ++i)
|
||||||
{
|
{
|
||||||
|
PathType? type = path_types[i];
|
||||||
|
|
||||||
// special inherit case
|
// special inherit case
|
||||||
if (type == null)
|
if (type == null)
|
||||||
{
|
{
|
||||||
@ -506,7 +508,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||||
}
|
}
|
||||||
|
|
||||||
curveTypeItems.Add(createMenuItemForPathType(type));
|
curveTypeItems.Add(createMenuItemForPathType(type, InputKey.Number1 + i));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||||
@ -540,7 +542,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
return menuItems.ToArray();
|
return menuItems.ToArray();
|
||||||
|
|
||||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
|
CurveTypeMenuItem createMenuItemForPathType(PathType? type, InputKey? key = null)
|
||||||
|
{
|
||||||
|
Hotkey hotkey = default;
|
||||||
|
|
||||||
|
if (key != null)
|
||||||
|
hotkey = new Hotkey(new KeyCombination(InputKey.Alt, key.Value));
|
||||||
|
|
||||||
|
return new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)) { Hotkey = hotkey };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Caching;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -593,8 +594,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
addControlPoint(lastRightClickPosition);
|
addControlPoint(lastRightClickPosition);
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
}),
|
})
|
||||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
{
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||||
|
},
|
||||||
|
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
|
||||||
|
{
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
|
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
|
||||||
|
29
osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs
Normal file
29
osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class GridFromPointsTool : CompositionTool
|
||||||
|
{
|
||||||
|
public GridFromPointsTool()
|
||||||
|
: base("Grid")
|
||||||
|
{
|
||||||
|
TooltipText = """
|
||||||
|
Left click to set the origin.
|
||||||
|
Left click again to set the spacing and rotation.
|
||||||
|
Right click to reset to default.
|
||||||
|
Click and drag to set the origin, spacing and rotation.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.DraftingCompass };
|
||||||
|
|
||||||
|
public override PlacementBlueprint CreatePlacementBlueprint() => new GridPlacementBlueprint();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -37,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = 0f,
|
MinValue = 0f,
|
||||||
MaxValue = OsuPlayfield.BASE_SIZE.X,
|
MaxValue = OsuPlayfield.BASE_SIZE.X,
|
||||||
Precision = 1f
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = 0f,
|
MinValue = 0f,
|
||||||
MaxValue = OsuPlayfield.BASE_SIZE.Y,
|
MaxValue = OsuPlayfield.BASE_SIZE.Y,
|
||||||
Precision = 1f
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = 4f,
|
MinValue = 4f,
|
||||||
MaxValue = 128f,
|
MaxValue = 128f,
|
||||||
Precision = 1f
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -67,14 +65,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
MinValue = -180f,
|
MinValue = -180f,
|
||||||
MaxValue = 180f,
|
MaxValue = 180f,
|
||||||
Precision = 1f
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read-only bindable representing the grid's origin.
|
/// Read-only bindable representing the grid's origin.
|
||||||
/// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code>
|
/// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>();
|
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>(OsuPlayfield.BASE_SIZE / 2);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read-only bindable representing the grid's spacing in both the X and Y dimension.
|
/// Read-only bindable representing the grid's spacing in both the X and Y dimension.
|
||||||
@ -97,6 +94,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private const float max_automatic_spacing = 64;
|
private const float max_automatic_spacing = 64;
|
||||||
|
|
||||||
|
public void SetGridFromPoints(Vector2 point1, Vector2 point2)
|
||||||
|
{
|
||||||
|
StartPositionX.Value = point1.X;
|
||||||
|
StartPositionY.Value = point1.Y;
|
||||||
|
|
||||||
|
// Get the angle between the two points and normalize to the valid range.
|
||||||
|
if (!GridLinesRotation.Disabled)
|
||||||
|
{
|
||||||
|
float period = GridLinesRotation.MaxValue - GridLinesRotation.MinValue;
|
||||||
|
GridLinesRotation.Value = normalizeRotation(MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X)), period);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide the distance so that there is a good density of grid lines.
|
||||||
|
// This matches the maximum grid size of the grid size cycling hotkey.
|
||||||
|
float dist = Vector2.Distance(point1, point2);
|
||||||
|
while (dist >= max_automatic_spacing)
|
||||||
|
dist /= 2;
|
||||||
|
Spacing.Value = dist;
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -160,22 +177,28 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
StartPositionX.BindValueChanged(x =>
|
StartPositionX.BindValueChanged(x =>
|
||||||
{
|
{
|
||||||
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
|
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
|
||||||
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}";
|
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}";
|
||||||
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
StartPositionY.BindValueChanged(y =>
|
StartPositionY.BindValueChanged(y =>
|
||||||
{
|
{
|
||||||
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}";
|
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
|
||||||
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}";
|
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}";
|
||||||
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
StartPosition.BindValueChanged(pos =>
|
||||||
|
{
|
||||||
|
StartPositionX.Value = pos.NewValue.X;
|
||||||
|
StartPositionY.Value = pos.NewValue.Y;
|
||||||
|
});
|
||||||
|
|
||||||
Spacing.BindValueChanged(spacing =>
|
Spacing.BindValueChanged(spacing =>
|
||||||
{
|
{
|
||||||
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}";
|
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
|
||||||
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}";
|
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
|
||||||
SpacingVector.Value = new Vector2(spacing.NewValue);
|
SpacingVector.Value = new Vector2(spacing.NewValue);
|
||||||
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
|
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
@ -186,44 +209,50 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
expandingContainer?.Expanded.BindValueChanged(v =>
|
|
||||||
{
|
|
||||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
|
||||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
GridType.BindValueChanged(v =>
|
GridType.BindValueChanged(v =>
|
||||||
{
|
{
|
||||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||||
|
|
||||||
|
gridTypeButtons.Items[(int)v.NewValue].Select();
|
||||||
|
|
||||||
switch (v.NewValue)
|
switch (v.NewValue)
|
||||||
{
|
{
|
||||||
case PositionSnapGridType.Square:
|
case PositionSnapGridType.Square:
|
||||||
GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45;
|
GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 90);
|
||||||
GridLinesRotation.MinValue = -45;
|
GridLinesRotation.MinValue = -45;
|
||||||
GridLinesRotation.MaxValue = 45;
|
GridLinesRotation.MaxValue = 45;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PositionSnapGridType.Triangle:
|
case PositionSnapGridType.Triangle:
|
||||||
GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30;
|
GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 60);
|
||||||
GridLinesRotation.MinValue = -30;
|
GridLinesRotation.MinValue = -30;
|
||||||
GridLinesRotation.MaxValue = 30;
|
GridLinesRotation.MaxValue = 30;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||||
|
{
|
||||||
|
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||||
|
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void nextGridSize()
|
private float normalizeRotation(float rotation, float period)
|
||||||
{
|
{
|
||||||
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.EditorCycleGridDisplayMode:
|
case GlobalAction.EditorCycleGridSpacing:
|
||||||
nextGridSize();
|
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case GlobalAction.EditorCycleGridType:
|
||||||
|
GridType.Value = (PositionSnapGridType)(((int)GridType.Value + 1) % Enum.GetValues<PositionSnapGridType>().Length);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
new HitCircleCompositionTool(),
|
new HitCircleCompositionTool(),
|
||||||
new SliderCompositionTool(),
|
new SliderCompositionTool(),
|
||||||
new SpinnerCompositionTool()
|
new SpinnerCompositionTool(),
|
||||||
|
new GridFromPointsTool()
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
|
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
|
||||||
@ -79,13 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
// Give a bit of breathing room around the playfield content.
|
// Give a bit of breathing room around the playfield content.
|
||||||
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||||
|
|
||||||
LayerBelowRuleset.AddRange(new Drawable[]
|
LayerBelowRuleset.Add(
|
||||||
{
|
|
||||||
distanceSnapGridContainer = new Container
|
distanceSnapGridContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
||||||
selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();
|
selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();
|
||||||
|
@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
points = originalConvexHull!;
|
points = originalConvexHull!;
|
||||||
|
|
||||||
foreach (var point in points)
|
foreach (var point in points)
|
||||||
{
|
scale = clampToBounds(scale, point, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||||
scale = clampToBound(scale, point, Vector2.Zero);
|
|
||||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
return scale;
|
||||||
|
|
||||||
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
// Clamps the scale vector s such that the point p scaled by s is within the rectangle defined by lowerBounds and upperBounds
|
||||||
|
Vector2 clampToBounds(Vector2 s, Vector2 p, Vector2 lowerBounds, Vector2 upperBounds)
|
||||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
|
||||||
{
|
{
|
||||||
p -= actualOrigin;
|
p -= actualOrigin;
|
||||||
bound -= actualOrigin;
|
lowerBounds -= actualOrigin;
|
||||||
|
upperBounds -= actualOrigin;
|
||||||
|
// a.X is the rotated X component of p with respect to the X bounds
|
||||||
|
// a.Y is the rotated X component of p with respect to the Y bounds
|
||||||
|
// b.X is the rotated Y component of p with respect to the X bounds
|
||||||
|
// b.Y is the rotated Y component of p with respect to the Y bounds
|
||||||
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||||
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||||
|
|
||||||
|
float sLowerBound, sUpperBound;
|
||||||
|
|
||||||
switch (adjustAxis)
|
switch (adjustAxis)
|
||||||
{
|
{
|
||||||
case Axes.X:
|
case Axes.X:
|
||||||
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
|
||||||
|
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Axes.Y:
|
case Axes.Y:
|
||||||
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
|
||||||
|
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Axes.Both:
|
case Axes.Both:
|
||||||
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
// Here we compute the bounds for the magnitude multiplier of the scale vector
|
||||||
|
// Therefore the ratio s.X / s.Y will be maintained
|
||||||
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
|
||||||
|
s.X = s.X < 0
|
||||||
|
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||||
|
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||||
|
s.Y = s.Y < 0
|
||||||
|
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||||
|
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Computes the bounds for the magnitude of the scaled point p with respect to the bounds lowerBounds and upperBounds
|
||||||
|
(float, float) computeBounds(Vector2 lowerBounds, Vector2 upperBounds, Vector2 p)
|
||||||
|
{
|
||||||
|
var sLowerBounds = Vector2.Divide(lowerBounds, p);
|
||||||
|
var sUpperBounds = Vector2.Divide(upperBounds, p);
|
||||||
|
|
||||||
|
// If the point is negative, then the bounds are flipped
|
||||||
|
if (p.X < 0)
|
||||||
|
(sLowerBounds.X, sUpperBounds.X) = (sUpperBounds.X, sLowerBounds.X);
|
||||||
|
if (p.Y < 0)
|
||||||
|
(sLowerBounds.Y, sUpperBounds.Y) = (sUpperBounds.Y, sLowerBounds.Y);
|
||||||
|
|
||||||
|
// If the point is at zero, then any scale will have no effect on the point so the bounds are infinite
|
||||||
|
// The float division would already give us infinity for the bounds, but the sign is not consistent so we have to manually set it
|
||||||
|
if (Precision.AlmostEquals(p.X, 0))
|
||||||
|
(sLowerBounds.X, sUpperBounds.X) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||||
|
if (Precision.AlmostEquals(p.Y, 0))
|
||||||
|
(sLowerBounds.Y, sUpperBounds.Y) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||||
|
|
||||||
|
return (MathF.Max(sLowerBounds.X, sLowerBounds.Y), MathF.Min(sUpperBounds.X, sUpperBounds.Y));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
|
@ -136,8 +136,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
});
|
});
|
||||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
xCheckBox.Current.BindValueChanged(_ =>
|
||||||
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
{
|
||||||
|
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
|
||||||
|
{
|
||||||
|
yCheckBox.Current.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAxes();
|
||||||
|
});
|
||||||
|
yCheckBox.Current.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
|
||||||
|
{
|
||||||
|
xCheckBox.Current.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAxes();
|
||||||
|
});
|
||||||
|
|
||||||
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
@ -152,6 +170,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateAxes()
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { XAxis = xCheckBox.Current.Value, YAxis = yCheckBox.Current.Value };
|
||||||
|
updateMinMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateAxisCheckBoxesEnabled()
|
private void updateAxisCheckBoxesEnabled()
|
||||||
{
|
{
|
||||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||||
@ -175,12 +199,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
axisBindable.Disabled = !available;
|
axisBindable.Disabled = !available;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMaxScale()
|
private void updateMinMaxScale()
|
||||||
{
|
{
|
||||||
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const float min_scale = 0.5f;
|
||||||
const float max_scale = 10;
|
const float max_scale = 10;
|
||||||
|
|
||||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
if (!scaleInfo.Value.XAxis)
|
if (!scaleInfo.Value.XAxis)
|
||||||
@ -189,12 +215,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
scale.Y = max_scale;
|
scale.Y = max_scale;
|
||||||
|
|
||||||
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||||
|
|
||||||
|
scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(min_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
|
if (!scaleInfo.Value.XAxis)
|
||||||
|
scale.X = min_scale;
|
||||||
|
if (!scaleInfo.Value.YAxis)
|
||||||
|
scale.Y = min_scale;
|
||||||
|
|
||||||
|
scaleInputBindable.MinValue = MathF.Min(1, MathF.Max(scale.X, scale.Y));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOrigin(ScaleOrigin origin)
|
private void setOrigin(ScaleOrigin origin)
|
||||||
{
|
{
|
||||||
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||||
updateMaxScale();
|
updateMinMaxScale();
|
||||||
updateAxisCheckBoxesEnabled();
|
updateAxisCheckBoxesEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,21 +254,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
private Axes getAdjustAxis(PreciseScaleInfo scale)
|
||||||
|
{
|
||||||
|
var result = Axes.None;
|
||||||
|
|
||||||
|
if (scale.XAxis)
|
||||||
|
result |= Axes.X;
|
||||||
|
|
||||||
|
if (scale.YAxis)
|
||||||
|
result |= Axes.Y;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||||
|
|
||||||
private void setAxis(bool x, bool y)
|
|
||||||
{
|
|
||||||
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
|
||||||
updateMaxScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
base.PopIn();
|
base.PopIn();
|
||||||
scaleHandler.Begin();
|
scaleHandler.Begin();
|
||||||
updateMaxScale();
|
updateMinMaxScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
|
@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
|||||||
{
|
{
|
||||||
public partial class OsuDifficultySection : SetupSection
|
public partial class OsuDifficultySection : SetupSection
|
||||||
{
|
{
|
||||||
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
|
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
|
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> stackLeniency { get; set; } = null!;
|
private FormSliderBar<float> stackLeniency { get; set; } = null!;
|
||||||
|
|
||||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
@ -31,103 +31,110 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
circleSizeSlider = new LabelledSliderBar<float>
|
circleSizeSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsCs,
|
Caption = BeatmapsetsStrings.ShowStatsCs,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.CircleSizeDescription,
|
||||||
Description = EditorSetupStrings.CircleSizeDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
healthDrainSlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
Description = EditorSetupStrings.DrainRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
approachRateSlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
approachRateSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAr,
|
Caption = BeatmapsetsStrings.ShowStatsAr,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.ApproachRateDescription,
|
||||||
Description = EditorSetupStrings.ApproachRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overallDifficultySlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseVelocitySlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.BaseVelocity,
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
{
|
{
|
||||||
Default = 1.4,
|
Default = 1.4,
|
||||||
MinValue = 0.4,
|
MinValue = 0.4,
|
||||||
MaxValue = 3.6,
|
MaxValue = 3.6,
|
||||||
Precision = 0.01f,
|
Precision = 0.01f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tickRateSlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.TickRate,
|
Caption = EditorSetupStrings.TickRate,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
Description = EditorSetupStrings.TickRateDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
{
|
{
|
||||||
Default = 1,
|
Default = 1,
|
||||||
MinValue = 1,
|
MinValue = 1,
|
||||||
MaxValue = 4,
|
MaxValue = 4,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
stackLeniency = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
stackLeniency = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = "Stack Leniency",
|
Caption = "Stack Leniency",
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||||
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
|
||||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||||
{
|
{
|
||||||
Default = 0.7f,
|
Default = 0.7f,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 1,
|
MaxValue = 1,
|
||||||
Precision = 0.1f
|
Precision = 0.1f
|
||||||
}
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
@ -25,5 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Update(Playfield playfield)
|
||||||
|
{
|
||||||
|
base.Update(playfield);
|
||||||
|
OsuPlayfield osuPlayfield = (OsuPlayfield)playfield;
|
||||||
|
Debug.Assert(osuPlayfield.Cursor != null);
|
||||||
|
|
||||||
|
osuPlayfield.Cursor.ActiveCursor.Rotation = -CurrentRotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,6 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -150,6 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -204,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
SpanStartTime = e.SpanStartTime,
|
SpanStartTime = e.SpanStartTime,
|
||||||
StartTime = e.Time,
|
StartTime = e.Time,
|
||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -236,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -248,14 +250,27 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
endPositionCache.Invalidate();
|
endPositionCache.Invalidate();
|
||||||
|
|
||||||
if (HeadCircle != null)
|
foreach (var nested in NestedHitObjects)
|
||||||
HeadCircle.Position = Position;
|
{
|
||||||
|
switch (nested)
|
||||||
|
{
|
||||||
|
case SliderHeadCircle headCircle:
|
||||||
|
headCircle.Position = Position;
|
||||||
|
break;
|
||||||
|
|
||||||
if (TailCircle != null)
|
case SliderTailCircle tailCircle:
|
||||||
TailCircle.Position = EndPosition;
|
tailCircle.Position = EndPosition;
|
||||||
|
break;
|
||||||
|
|
||||||
if (LastRepeat != null)
|
case SliderRepeat repeat:
|
||||||
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
repeat.Position = Position + Path.PositionAt(repeat.PathProgress);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SliderTick tick:
|
||||||
|
tick.Position = Position + Path.PositionAt(tick.PathProgress);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void UpdateNestedSamples()
|
protected void UpdateNestedSamples()
|
||||||
|
@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
public class SliderRepeat : SliderEndCircle
|
public class SliderRepeat : SliderEndCircle
|
||||||
{
|
{
|
||||||
|
public double PathProgress { get; set; }
|
||||||
|
|
||||||
public SliderRepeat(Slider slider)
|
public SliderRepeat(Slider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
public int SpanIndex { get; set; }
|
public int SpanIndex { get; set; }
|
||||||
public double SpanStartTime { get; set; }
|
public double SpanStartTime { get; set; }
|
||||||
|
public double PathProgress { get; set; }
|
||||||
|
|
||||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
@ -39,6 +40,7 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu
|
namespace osu.Game.Rulesets.Osu
|
||||||
{
|
{
|
||||||
@ -336,10 +338,28 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new OsuDifficultySection(),
|
new OsuDifficultySection(),
|
||||||
new ColoursSection(),
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(SetupScreen.SPACING),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ResourcesSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
|
new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
@ -117,10 +116,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (osuObject is not Slider slider)
|
if (osuObject is not Slider slider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
|
|
||||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
modifySlider(slider, reflectControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -134,10 +132,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (osuObject is not Slider slider)
|
if (osuObject is not Slider slider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
|
|
||||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
modifySlider(slider, reflectControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -146,10 +143,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// <param name="slider">The slider to be flipped.</param>
|
/// <param name="slider">The slider to be flipped.</param>
|
||||||
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
||||||
{
|
{
|
||||||
void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
|
|
||||||
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, flipNestedObject, flipControlPoint);
|
modifySlider(slider, flipControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -159,18 +155,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
||||||
public static void RotateSlider(Slider slider, float rotation)
|
public static void RotateSlider(Slider slider, float rotation)
|
||||||
{
|
{
|
||||||
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
|
||||||
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
||||||
|
|
||||||
modifySlider(slider, rotateNestedObject, rotateControlPoint);
|
modifySlider(slider, rotateControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void modifySlider(Slider slider, Action<OsuHitObject> modifyNestedObject, Action<PathControlPoint> modifyControlPoint)
|
private static void modifySlider(Slider slider, Action<PathControlPoint> modifyControlPoint)
|
||||||
{
|
{
|
||||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
|
||||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(modifyNestedObject);
|
|
||||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(modifyNestedObject);
|
|
||||||
|
|
||||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||||
foreach (var point in controlPoints)
|
foreach (var point in controlPoints)
|
||||||
modifyControlPoint(point);
|
modifyControlPoint(point);
|
||||||
|
@ -232,8 +232,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||||
workingObject.EndPositionModified = slider.EndPosition;
|
workingObject.EndPositionModified = slider.EndPosition;
|
||||||
|
|
||||||
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
|
|
||||||
|
|
||||||
return workingObject.PositionModified - previousPosition;
|
return workingObject.PositionModified - previousPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,22 +305,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
return new RectangleF(left, top, right - left, bottom - top);
|
return new RectangleF(left, top, right - left, bottom - top);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
|
|
||||||
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
|
|
||||||
private static void shiftNestedObjects(Slider slider, Vector2 shift)
|
|
||||||
{
|
|
||||||
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
|
|
||||||
{
|
|
||||||
if (!(hitObject is OsuHitObject osuHitObject))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
osuHitObject.Position += shift;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a position to playfield, keeping a specified distance from the edges.
|
/// Clamp a position to playfield, keeping a specified distance from the edges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -431,7 +413,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
private class WorkingObject
|
private class WorkingObject
|
||||||
{
|
{
|
||||||
public float RotationOriginal { get; }
|
public float RotationOriginal { get; }
|
||||||
public Vector2 PositionOriginal { get; }
|
|
||||||
public Vector2 PositionModified { get; set; }
|
public Vector2 PositionModified { get; set; }
|
||||||
public Vector2 EndPositionModified { get; set; }
|
public Vector2 EndPositionModified { get; set; }
|
||||||
|
|
||||||
@ -442,7 +423,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
{
|
{
|
||||||
PositionInfo = positionInfo;
|
PositionInfo = positionInfo;
|
||||||
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
||||||
PositionModified = PositionOriginal = HitObject.Position;
|
PositionModified = HitObject.Position;
|
||||||
EndPositionModified = HitObject.EndPosition;
|
EndPositionModified = HitObject.EndPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneEditorPlacement : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacementBlueprintDoesNotCauseCrashes()
|
||||||
|
{
|
||||||
|
AddStep("clear objects", () => EditorBeatmap.Clear());
|
||||||
|
AddStep("add two objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(new Hit { StartTime = 1818 });
|
||||||
|
EditorBeatmap.Add(new Hit { StartTime = 1584 });
|
||||||
|
});
|
||||||
|
AddStep("seek back", () => EditorClock.Seek(1584));
|
||||||
|
AddStep("choose hit placement tool", () => InputManager.Key(Key.Number2));
|
||||||
|
AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(1)));
|
||||||
|
AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(0)));
|
||||||
|
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||||
|
AddUntilStep("context menu open", () => Editor.ChildrenOfType<OsuContextMenu>().Any(menu => menu.State == MenuState.Open));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
[JsonProperty("great_hit_window")]
|
[JsonProperty("great_hit_window")]
|
||||||
public double GreatHitWindow { get; set; }
|
public double GreatHitWindow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("ok_hit_window")]
|
||||||
|
public double OkHitWindow { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||||
{
|
{
|
||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
@ -50,6 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
|
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@ -58,6 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
|
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||||
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||||
|
|
||||||
public override int Version => 20221107;
|
public override int Version => 20241007;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
ColourDifficulty = colourRating,
|
ColourDifficulty = colourRating,
|
||||||
PeakDifficulty = combinedRating,
|
PeakDifficulty = combinedRating,
|
||||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||||
|
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||||
MaxCombo = beatmap.GetMaxCombo(),
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
[JsonProperty("effective_miss_count")]
|
[JsonProperty("effective_miss_count")]
|
||||||
public double EffectiveMissCount { get; set; }
|
public double EffectiveMissCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("estimated_unstable_rate")]
|
||||||
|
public double? EstimatedUnstableRate { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
{
|
{
|
||||||
foreach (var attribute in base.GetAttributesForDisplay())
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
{
|
{
|
||||||
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
private int countOk;
|
private int countOk;
|
||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
private double accuracy;
|
private double? estimatedUnstableRate;
|
||||||
|
|
||||||
private double effectiveMissCount;
|
private double effectiveMissCount;
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
accuracy = customAccuracy;
|
estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
|
||||||
|
|
||||||
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
||||||
if (totalSuccessfulHits > 0)
|
if (totalSuccessfulHits > 0)
|
||||||
@ -65,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
Difficulty = difficultyValue,
|
Difficulty = difficultyValue,
|
||||||
Accuracy = accuracyValue,
|
Accuracy = accuracyValue,
|
||||||
EffectiveMissCount = effectiveMissCount,
|
EffectiveMissCount = effectiveMissCount,
|
||||||
|
EstimatedUnstableRate = estimatedUnstableRate,
|
||||||
Total = totalValue
|
Total = totalValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -85,35 +87,94 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
difficultyValue *= 1.025;
|
difficultyValue *= 1.025;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHardRock))
|
if (score.Mods.Any(m => m is ModHardRock))
|
||||||
difficultyValue *= 1.050;
|
difficultyValue *= 1.10;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||||
difficultyValue *= 1.050 * lengthBonus;
|
difficultyValue *= 1.050 * lengthBonus;
|
||||||
|
|
||||||
return difficultyValue * Math.Pow(accuracy, 2.0);
|
if (estimatedUnstableRate == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUnstableRate.Value)), 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||||
{
|
{
|
||||||
if (attributes.GreatHitWindow <= 0)
|
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
|
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
|
||||||
|
|
||||||
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
||||||
accuracyValue *= lengthBonus;
|
|
||||||
|
|
||||||
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
|
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
|
||||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
|
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
|
||||||
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);
|
accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus);
|
||||||
|
|
||||||
return accuracyValue;
|
return accuracyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders,
|
||||||
|
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
|
||||||
|
/// two SS scores on the same map with the same settings will always return the same deviation.
|
||||||
|
/// </summary>
|
||||||
|
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
double h300 = attributes.GreatHitWindow;
|
||||||
|
double h100 = attributes.OkHitWindow;
|
||||||
|
|
||||||
|
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||||
|
|
||||||
|
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
|
||||||
|
double? calcDeviationGreatWindow()
|
||||||
|
{
|
||||||
|
if (countGreat == 0) return null;
|
||||||
|
|
||||||
|
double n = totalHits;
|
||||||
|
|
||||||
|
// Proportion of greats hit.
|
||||||
|
double p = countGreat / n;
|
||||||
|
|
||||||
|
// We can be 99% confident that p is at least this value.
|
||||||
|
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||||
|
|
||||||
|
// We can be 99% confident that the deviation is not higher than:
|
||||||
|
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
|
||||||
|
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
|
||||||
|
double? calcDeviationGoodWindow()
|
||||||
|
{
|
||||||
|
if (totalSuccessfulHits == 0) return null;
|
||||||
|
|
||||||
|
double n = totalHits;
|
||||||
|
|
||||||
|
// Proportion of greats + goods hit.
|
||||||
|
double p = totalSuccessfulHits / n;
|
||||||
|
|
||||||
|
// We can be 99% confident that p is at least this value.
|
||||||
|
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||||
|
|
||||||
|
// We can be 99% confident that the deviation is not higher than:
|
||||||
|
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||||
|
}
|
||||||
|
|
||||||
|
double? deviationGreatWindow = calcDeviationGreatWindow();
|
||||||
|
double? deviationGoodWindow = calcDeviationGoodWindow();
|
||||||
|
|
||||||
|
if (deviationGreatWindow is null)
|
||||||
|
return deviationGoodWindow;
|
||||||
|
|
||||||
|
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
|
|
||||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||||
|
|
||||||
private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
|||||||
{
|
{
|
||||||
public partial class TaikoDifficultySection : SetupSection
|
public partial class TaikoDifficultySection : SetupSection
|
||||||
{
|
{
|
||||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
|
|
||||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
@ -28,64 +28,68 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
healthDrainSlider = new LabelledSliderBar<float>
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
Description = EditorSetupStrings.DrainRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overallDifficultySlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseVelocitySlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.BaseVelocity,
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
{
|
{
|
||||||
Default = 1.4,
|
Default = 1.4,
|
||||||
MinValue = 0.4,
|
MinValue = 0.4,
|
||||||
MaxValue = 3.6,
|
MaxValue = 3.6,
|
||||||
Precision = 0.01f,
|
Precision = 0.01f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tickRateSlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.TickRate,
|
Caption = EditorSetupStrings.TickRate,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
Description = EditorSetupStrings.TickRateDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
{
|
{
|
||||||
Default = 1,
|
Default = 1,
|
||||||
MinValue = 1,
|
MinValue = 1,
|
||||||
MaxValue = 4,
|
MaxValue = 4,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -86,10 +87,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||||
{
|
{
|
||||||
if (selection.All(s => s.Item is Hit))
|
if (selection.All(s => s.Item is Hit))
|
||||||
yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
{
|
||||||
|
yield return new TernaryStateToggleMenuItem("Rim")
|
||||||
|
{
|
||||||
|
State = { BindTarget = selectionRimState },
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.W), new KeyCombination(InputKey.R)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (selection.All(s => s.Item is TaikoHitObject))
|
if (selection.All(s => s.Item is TaikoHitObject))
|
||||||
yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
{
|
||||||
|
yield return new TernaryStateToggleMenuItem("Strong")
|
||||||
|
{
|
||||||
|
State = { BindTarget = selectionStrongState },
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.E)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||||
yield return item;
|
yield return item;
|
||||||
|
@ -190,9 +190,12 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new TaikoDifficultySection(),
|
new TaikoDifficultySection(),
|
||||||
|
new ResourcesSection(),
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
||||||
|
@ -22,9 +22,9 @@ namespace osu.Game.Tests.Database
|
|||||||
[HeadlessTest]
|
[HeadlessTest]
|
||||||
public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
|
public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
|
||||||
{
|
{
|
||||||
public IBindable<bool> IsPlaying => isPlaying;
|
public IBindable<LocalUserPlayingState> PlayingState => isPlaying;
|
||||||
|
|
||||||
private readonly Bindable<bool> isPlaying = new Bindable<bool>();
|
private readonly Bindable<LocalUserPlayingState> isPlaying = new Bindable<LocalUserPlayingState>();
|
||||||
|
|
||||||
private BeatmapSetInfo importedSet = null!;
|
private BeatmapSetInfo importedSet = null!;
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ namespace osu.Game.Tests.Database
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Set not playing", () => isPlaying.Value = false);
|
AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Set playing", () => isPlaying.Value = true);
|
AddStep("Set playing", () => isPlaying.Value = LocalUserPlayingState.Playing);
|
||||||
|
|
||||||
AddStep("Reset difficulty", () =>
|
AddStep("Reset difficulty", () =>
|
||||||
{
|
{
|
||||||
@ -117,7 +117,7 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Set not playing", () => isPlaying.Value = false);
|
AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
|
||||||
|
|
||||||
AddUntilStep("wait for difficulties repopulated", () =>
|
AddUntilStep("wait for difficulties repopulated", () =>
|
||||||
{
|
{
|
||||||
|
@ -716,7 +716,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
foreach (var entry in zip.Entries.ToArray())
|
foreach (var entry in zip.Entries.ToArray())
|
||||||
{
|
{
|
||||||
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
|
if (entry.Key!.EndsWith(".osu", StringComparison.InvariantCulture))
|
||||||
zip.RemoveEntry(entry);
|
zip.RemoveEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,23 +267,26 @@ namespace osu.Game.Tests.Editing
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestUseCurrentSnap()
|
public void TestUseCurrentSnap()
|
||||||
{
|
{
|
||||||
|
ExpandableButton getCurrentSnapButton() => composer.ChildrenOfType<EditorToolboxGroup>().Single(g => g.Name == "snapping")
|
||||||
|
.ChildrenOfType<ExpandableButton>().Single();
|
||||||
|
|
||||||
AddStep("add objects to beatmap", () =>
|
AddStep("add objects to beatmap", () =>
|
||||||
{
|
{
|
||||||
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
||||||
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
|
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(getCurrentSnapButton()));
|
||||||
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
|
AddUntilStep("use current snap expanded", () => getCurrentSnapButton().Expanded.Value, () => Is.True);
|
||||||
|
|
||||||
AddStep("seek before first object", () => EditorClock.Seek(0));
|
AddStep("seek before first object", () => EditorClock.Seek(0));
|
||||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||||
|
|
||||||
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
||||||
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
|
AddUntilStep("use current snap available", () => getCurrentSnapButton().Enabled.Value, () => Is.True);
|
||||||
|
|
||||||
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
||||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Configuration;
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Input;
|
using osu.Game.Input;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Input
|
namespace osu.Game.Tests.Input
|
||||||
@ -15,9 +17,20 @@ namespace osu.Game.Tests.Input
|
|||||||
[HeadlessTest]
|
[HeadlessTest]
|
||||||
public partial class ConfineMouseTrackerTest : OsuGameTestScene
|
public partial class ConfineMouseTrackerTest : OsuGameTestScene
|
||||||
{
|
{
|
||||||
|
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private FrameworkConfigManager frameworkConfigManager { get; set; } = null!;
|
private FrameworkConfigManager frameworkConfigManager { get; set; } = null!;
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
base.SetUpSteps();
|
||||||
|
|
||||||
|
// a bit dodgy.
|
||||||
|
AddStep("bind playing state", () => ((IBindable<LocalUserPlayingState>)playingState).BindTo(((ILocalUserPlayInfo)Game).PlayingState));
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(WindowMode.Windowed)]
|
[TestCase(WindowMode.Windowed)]
|
||||||
[TestCase(WindowMode.Borderless)]
|
[TestCase(WindowMode.Borderless)]
|
||||||
public void TestDisableConfining(WindowMode windowMode)
|
public void TestDisableConfining(WindowMode windowMode)
|
||||||
@ -88,7 +101,7 @@ namespace osu.Game.Tests.Input
|
|||||||
=> AddStep($"set {mode} game-side", () => Game.LocalConfig.SetValue(OsuSetting.ConfineMouseMode, mode));
|
=> AddStep($"set {mode} game-side", () => Game.LocalConfig.SetValue(OsuSetting.ConfineMouseMode, mode));
|
||||||
|
|
||||||
private void setLocalUserPlayingTo(bool playing)
|
private void setLocalUserPlayingTo(bool playing)
|
||||||
=> AddStep($"local user {(playing ? "playing" : "not playing")}", () => Game.LocalUserPlaying.Value = playing);
|
=> AddStep($"local user {(playing ? "playing" : "not playing")}", () => playingState.Value = playing ? LocalUserPlayingState.Playing : LocalUserPlayingState.NotPlaying);
|
||||||
|
|
||||||
private void gameSideModeIs(OsuConfineMouseMode mode)
|
private void gameSideModeIs(OsuConfineMouseMode mode)
|
||||||
=> AddAssert($"mode is {mode} game-side", () => Game.LocalConfig.Get<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode) == mode);
|
=> AddAssert($"mode is {mode} game-side", () => Game.LocalConfig.Get<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode) == mode);
|
||||||
|
@ -627,6 +627,87 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) =>
|
||||||
|
new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
private static readonly object[] ranked_date_valid_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
|
||||||
|
new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
|
||||||
|
new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
|
||||||
|
new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
|
||||||
|
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(ranked_date_valid_test_cases))]
|
||||||
|
public void TestValidRankedDateQueries(string query, DateTimeOffset expected, Func<FilterCriteria, DateTimeOffset?> f)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(true, filterCriteria.DateRanked.HasFilter);
|
||||||
|
Assert.AreEqual(expected, f(filterCriteria));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[] ranked_date_invalid_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "ranked<0" },
|
||||||
|
new object[] { "ranked=99999" },
|
||||||
|
new object[] { "ranked>=2012-03-05-04" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(ranked_date_invalid_test_cases))]
|
||||||
|
public void TestInvalidRankedDateQueries(string query)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(false, filterCriteria.DateRanked.HasFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[] submitted_date_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "submitted<2012", true },
|
||||||
|
new object[] { "submitted<2012.03", true },
|
||||||
|
new object[] { "submitted<2012/03/05", true },
|
||||||
|
new object[] { "submitted<2012-3-5", true },
|
||||||
|
|
||||||
|
new object[] { "submitted<0", false },
|
||||||
|
new object[] { "submitted=99999", false },
|
||||||
|
new object[] { "submitted>=2012-03-05-04", false },
|
||||||
|
new object[] { "submitted>=2012/03.05-04", false },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(submitted_date_test_cases))]
|
||||||
|
public void TestInvalidRankedDateQueries(string query, bool expected)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(expected, filterCriteria.DateSubmitted.HasFilter);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly object[] played_query_tests =
|
private static readonly object[] played_query_tests =
|
||||||
{
|
{
|
||||||
new object[] { "0", DateTimeOffset.MinValue, true },
|
new object[] { "0", DateTimeOffset.MinValue, true },
|
||||||
|
@ -6,10 +6,12 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Metadata;
|
using osu.Game.Online.Metadata;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual.Metadata;
|
using osu.Game.Tests.Visual.Metadata;
|
||||||
@ -81,6 +83,38 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
|||||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||||
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
||||||
|
AddAssert("notification posted", () => notificationOverlay.AllNotifications.OfType<SimpleNotification>().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestConclusionNotificationDoesNotFireOnDisconnect()
|
||||||
|
{
|
||||||
|
var room = new Room
|
||||||
|
{
|
||||||
|
RoomID = { Value = 1234 },
|
||||||
|
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||||
|
{
|
||||||
|
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||||
|
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
|
||||||
|
Category = { Value = RoomCategory.DailyChallenge }
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||||
|
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||||
|
|
||||||
|
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||||
|
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||||
|
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||||
|
AddStep("disconnect from metadata server", () => metadataClient.Disconnect());
|
||||||
|
AddUntilStep("wait for disconnection", () => metadataClient.DailyChallengeInfo.Value, () => Is.Null);
|
||||||
|
AddAssert("no notification posted", () => notificationOverlay.AllNotifications, () => Is.Empty);
|
||||||
|
AddStep("reconnect to metadata server", () => metadataClient.Reconnect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public partial class TestSceneColoursSection : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestNoBeatmapSkinColours()
|
||||||
|
{
|
||||||
|
LegacyBeatmapSkin skin = null!;
|
||||||
|
ColoursSection coloursSection = null!;
|
||||||
|
|
||||||
|
AddStep("create beatmap skin", () => skin = new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||||
|
AddStep("create colours section", () => Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
|
||||||
|
}, skin)),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||||
|
],
|
||||||
|
Child = coloursSection = new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AddAssert("beatmap skin has no colours", () => skin.Configuration.CustomComboColours, () => Is.Empty);
|
||||||
|
AddAssert("section displays default combo colours",
|
||||||
|
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
|
||||||
|
() => Is.EquivalentTo(new Colour4[]
|
||||||
|
{
|
||||||
|
SkinConfiguration.DefaultComboColours[1],
|
||||||
|
SkinConfiguration.DefaultComboColours[2],
|
||||||
|
SkinConfiguration.DefaultComboColours[3],
|
||||||
|
SkinConfiguration.DefaultComboColours[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
|
||||||
|
AddAssert("beatmap skin has colours",
|
||||||
|
() => skin.Configuration.CustomComboColours,
|
||||||
|
() => Is.EquivalentTo(new[]
|
||||||
|
{
|
||||||
|
SkinConfiguration.DefaultComboColours[1],
|
||||||
|
SkinConfiguration.DefaultComboColours[2],
|
||||||
|
SkinConfiguration.DefaultComboColours[3],
|
||||||
|
Color4.Aqua,
|
||||||
|
SkinConfiguration.DefaultComboColours[0],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExistingColours()
|
||||||
|
{
|
||||||
|
LegacyBeatmapSkin skin = null!;
|
||||||
|
ColoursSection coloursSection = null!;
|
||||||
|
|
||||||
|
AddStep("create beatmap skin", () =>
|
||||||
|
{
|
||||||
|
skin = new LegacyBeatmapSkin(new BeatmapInfo(), null);
|
||||||
|
skin.Configuration.CustomComboColours = new List<Color4>
|
||||||
|
{
|
||||||
|
Color4.Azure,
|
||||||
|
Color4.Beige,
|
||||||
|
Color4.Chartreuse
|
||||||
|
};
|
||||||
|
});
|
||||||
|
AddStep("create colours section", () => Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
|
||||||
|
}, skin)),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||||
|
],
|
||||||
|
Child = coloursSection = new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AddAssert("section displays combo colours",
|
||||||
|
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
|
||||||
|
() => Is.EquivalentTo(new[]
|
||||||
|
{
|
||||||
|
Colour4.Beige,
|
||||||
|
Colour4.Chartreuse,
|
||||||
|
Colour4.Azure,
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
|
||||||
|
AddAssert("beatmap skin has colours",
|
||||||
|
() => skin.Configuration.CustomComboColours,
|
||||||
|
() => Is.EquivalentTo(new[]
|
||||||
|
{
|
||||||
|
Color4.Azure,
|
||||||
|
Color4.Beige,
|
||||||
|
Color4.Aqua,
|
||||||
|
Color4.Chartreuse
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -82,7 +83,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNudgeSelection()
|
public void TestNudgeSelectionTime()
|
||||||
{
|
{
|
||||||
HitCircle[] addedObjects = null!;
|
HitCircle[] addedObjects = null!;
|
||||||
|
|
||||||
@ -103,6 +104,51 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
|
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNudgeSelectionPosition()
|
||||||
|
{
|
||||||
|
HitCircle addedObject = null!;
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
|
||||||
|
{
|
||||||
|
addedObject = new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("select object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("nudge up", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Up);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved up", () => addedObject.Position.Y, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
AddStep("nudge down", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Down);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved down", () => addedObject.Position.Y, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
AddStep("nudge left", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved left", () => addedObject.Position.X, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
AddStep("nudge right", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Right);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved right", () => addedObject.Position.X, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRotateHotkeys()
|
public void TestRotateHotkeys()
|
||||||
{
|
{
|
||||||
|
@ -7,12 +7,14 @@ using System;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private TestDesignSection designSection;
|
private TestDesignSection designSection;
|
||||||
private EditorBeatmap editorBeatmap { get; set; }
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
@ -42,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
(typeof(EditorBeatmap), editorBeatmap)
|
(typeof(EditorBeatmap), editorBeatmap)
|
||||||
},
|
},
|
||||||
Child = designSection = new TestDesignSection()
|
Child = designSection = new TestDesignSection { RelativeSizeAxes = Axes.X }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,11 +104,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private partial class TestDesignSection : DesignSection
|
private partial class TestDesignSection : DesignSection
|
||||||
{
|
{
|
||||||
public new LabelledSwitchButton EnableCountdown => base.EnableCountdown;
|
public new FormCheckBox EnableCountdown => base.EnableCountdown;
|
||||||
|
|
||||||
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
|
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
|
||||||
public new LabelledEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
public new FormEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
||||||
public new LabelledNumberBox CountdownOffset => base.CountdownOffset;
|
public new FormTextBox CountdownOffset => base.CountdownOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||||
|
|
||||||
if (i == 11)
|
if (i == 11)
|
||||||
{
|
{
|
||||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
EditorBeatmap.EndChange();
|
EditorBeatmap.EndChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||||
|
|
||||||
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
||||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
@ -135,9 +137,42 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
pressAndCheckTime(Key.Up, 0);
|
pressAndCheckTime(Key.Up, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pressAndCheckTime(Key key, double expectedTime)
|
[Test]
|
||||||
|
public void TestSeekBetweenObjects()
|
||||||
{
|
{
|
||||||
AddStep($"press {key}", () => InputManager.Key(key));
|
AddStep("add objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Clear();
|
||||||
|
EditorBeatmap.AddRange(new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000, },
|
||||||
|
new HitCircle { StartTime = 2250, },
|
||||||
|
new HitCircle { StartTime = 3600, },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
AddStep("seek to 0", () => EditorClock.Seek(0));
|
||||||
|
|
||||||
|
pressAndCheckTime(Key.Right, 1000, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Right, 2250, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Left, 2250, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pressAndCheckTime(Key key, double expectedTime, params Key[] modifiers)
|
||||||
|
{
|
||||||
|
AddStep($"press {key} with {(modifiers.Any() ? string.Join(',', modifiers) : "no modifiers")}", () =>
|
||||||
|
{
|
||||||
|
foreach (var modifier in modifiers)
|
||||||
|
InputManager.PressKey(modifier);
|
||||||
|
|
||||||
|
InputManager.Key(key);
|
||||||
|
|
||||||
|
foreach (var modifier in modifiers)
|
||||||
|
InputManager.ReleaseKey(modifier);
|
||||||
|
});
|
||||||
AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
|
AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
||||||
{
|
{
|
||||||
@ -201,7 +206,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createSection()
|
private void createSection()
|
||||||
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
|
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection { RelativeSizeAxes = Axes.X });
|
||||||
|
|
||||||
private void assertArtistMetadata(string expected)
|
private void assertArtistMetadata(string expected)
|
||||||
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
|
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
|
||||||
@ -226,11 +231,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private partial class TestMetadataSection : MetadataSection
|
private partial class TestMetadataSection : MetadataSection
|
||||||
{
|
{
|
||||||
public new LabelledTextBox ArtistTextBox => base.ArtistTextBox;
|
public new FormTextBox ArtistTextBox => base.ArtistTextBox;
|
||||||
public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
public new FormTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
||||||
|
|
||||||
public new LabelledTextBox TitleTextBox => base.TitleTextBox;
|
public new FormTextBox TitleTextBox => base.TitleTextBox;
|
||||||
public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
public new FormTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUrlDecodingOfArgs()
|
||||||
|
{
|
||||||
|
setUpEditor(new OsuRuleset().RulesetInfo);
|
||||||
|
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(new OsuRuleset().RulesetInfo));
|
||||||
|
|
||||||
|
AddStep("jump to encoded link", () => Game.HandleLink("osu://edit/00:14:142%20(1)"));
|
||||||
|
|
||||||
|
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
|
||||||
|
|
||||||
|
AddAssert("time is correct", () => editorClock.CurrentTime, () => Is.EqualTo(14_142));
|
||||||
|
AddAssert("selected object is correct", () => editorBeatmap.SelectedHitObjects.Single().StartTime, () => Is.EqualTo(14_142));
|
||||||
|
}
|
||||||
|
|
||||||
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
||||||
{
|
{
|
||||||
AddStep($"{step} {timestamp}", () =>
|
AddStep($"{step} {timestamp}", () =>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -12,7 +10,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
public partial class TestSceneOverlayActivation : OsuPlayerTestScene
|
public partial class TestSceneOverlayActivation : OsuPlayerTestScene
|
||||||
{
|
{
|
||||||
protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer;
|
protected new OverlayTestPlayer Player => (OverlayTestPlayer)base.Player;
|
||||||
|
|
||||||
public override void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
|
@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
{
|
{
|
||||||
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("disable shuffle", () => Game.MusicController.Shuffle.Value = false);
|
||||||
|
|
||||||
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
|
||||||
@ -77,5 +79,114 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
|
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
|
||||||
AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShuffleBackwards()
|
||||||
|
{
|
||||||
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||||
|
|
||||||
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||||
|
{
|
||||||
|
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||||
|
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||||
|
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||||
|
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||||
|
beatmap.Length = 60_000;
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("bind to track change", () =>
|
||||||
|
{
|
||||||
|
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||||
|
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000));
|
||||||
|
AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddAssert("no track change", () => trackChangeQueue.Count == 0);
|
||||||
|
AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 2);
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () =>
|
||||||
|
trackChangeQueue.Count == 3 && !trackChangeQueue.ElementAt(1).working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShuffleForwards()
|
||||||
|
{
|
||||||
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||||
|
|
||||||
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||||
|
{
|
||||||
|
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||||
|
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||||
|
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||||
|
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||||
|
beatmap.Length = 60_000;
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("bind to track change", () =>
|
||||||
|
{
|
||||||
|
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||||
|
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 2);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () =>
|
||||||
|
trackChangeQueue.Count == 3 && !trackChangeQueue.ElementAt(1).working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShuffleBackAndForth()
|
||||||
|
{
|
||||||
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||||
|
|
||||||
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||||
|
{
|
||||||
|
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||||
|
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||||
|
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||||
|
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||||
|
beatmap.Length = 60_000;
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("bind to track change", () =>
|
||||||
|
{
|
||||||
|
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||||
|
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () =>
|
||||||
|
trackChangeQueue.Count == 2 && !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Cached(typeof(ILocalUserPlayInfo))]
|
[Cached(typeof(ILocalUserPlayInfo))]
|
||||||
private ILocalUserPlayInfo localUserInfo;
|
private ILocalUserPlayInfo localUserInfo;
|
||||||
|
|
||||||
private readonly Bindable<bool> localUserPlaying = new Bindable<bool>();
|
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
|
||||||
|
|
||||||
private TextBox textBox => chatDisplay.ChildrenOfType<TextBox>().First();
|
private TextBox textBox => chatDisplay.ChildrenOfType<TextBox>().First();
|
||||||
|
|
||||||
public TestSceneGameplayChatDisplay()
|
public TestSceneGameplayChatDisplay()
|
||||||
{
|
{
|
||||||
var mockLocalUserInfo = new Mock<ILocalUserPlayInfo>();
|
var mockLocalUserInfo = new Mock<ILocalUserPlayInfo>();
|
||||||
mockLocalUserInfo.SetupGet(i => i.IsPlaying).Returns(localUserPlaying);
|
mockLocalUserInfo.SetupGet(i => i.PlayingState).Returns(playingState);
|
||||||
|
|
||||||
localUserInfo = mockLocalUserInfo.Object;
|
localUserInfo = mockLocalUserInfo.Object;
|
||||||
}
|
}
|
||||||
@ -124,6 +124,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
|
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
|
||||||
|
|
||||||
private void setLocalUserPlaying(bool playing) =>
|
private void setLocalUserPlaying(bool playing) =>
|
||||||
AddStep($"local user {(playing ? "playing" : "not playing")}", () => localUserPlaying.Value = playing);
|
AddStep($"local user {(playing ? "playing" : "not playing")}", () => playingState.Value = playing ? LocalUserPlayingState.Playing : LocalUserPlayingState.NotPlaying);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,11 +165,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("wait for room join", () => RoomJoined);
|
AddUntilStep("wait for room join", () => RoomJoined);
|
||||||
|
|
||||||
AddStep("join other user (ready)", () =>
|
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
|
||||||
{
|
AddUntilStep("wait for user populated", () => MultiplayerClient.ClientRoom!.Users.Single(u => u.UserID == PLAYER_1_ID).User, () => Is.Not.Null);
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID });
|
AddStep("other user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
|
||||||
MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerSpectateButton>();
|
ClickButtonWhenEnabled<MultiplayerSpectateButton>();
|
||||||
|
|
||||||
|
@ -648,6 +648,34 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
|
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFiltering()
|
||||||
|
{
|
||||||
|
AddStep("Show overlay", () => chatOverlay.Show());
|
||||||
|
joinTestChannel(1);
|
||||||
|
joinTestChannel(3);
|
||||||
|
joinTestChannel(5);
|
||||||
|
joinChannel(new Channel(new APIUser { Id = 2001, Username = "alice" }));
|
||||||
|
joinChannel(new Channel(new APIUser { Id = 2002, Username = "bob" }));
|
||||||
|
joinChannel(new Channel(new APIUser { Id = 2003, Username = "charley the plant" }));
|
||||||
|
|
||||||
|
AddStep("filter to \"c\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "c");
|
||||||
|
AddUntilStep("bob filtered out", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(5));
|
||||||
|
|
||||||
|
AddStep("filter to \"channel\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "channel");
|
||||||
|
AddUntilStep("only public channels left", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(3));
|
||||||
|
|
||||||
|
AddStep("commit textbox", () =>
|
||||||
|
{
|
||||||
|
chatOverlay.ChildrenOfType<SearchTextBox>().Single().TakeFocus();
|
||||||
|
Schedule(() => InputManager.PressKey(Key.Enter));
|
||||||
|
});
|
||||||
|
AddUntilStep("#channel-2 active", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo("#channel-2"));
|
||||||
|
|
||||||
|
AddStep("filter to \"channel-3\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "channel-3");
|
||||||
|
AddUntilStep("no channels left", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
private void joinTestChannel(int i)
|
private void joinTestChannel(int i)
|
||||||
{
|
{
|
||||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||||
|
@ -8,11 +8,13 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -90,6 +92,48 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
|
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitResultsWithSameNameAreGrouped()
|
||||||
|
{
|
||||||
|
AddStep("Load scores without user best", () =>
|
||||||
|
{
|
||||||
|
var allScores = createScores();
|
||||||
|
allScores.UserScore = null;
|
||||||
|
scoresContainer.Scores = allScores;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
|
AddAssert("only one column for slider end", () =>
|
||||||
|
{
|
||||||
|
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
|
||||||
|
return scoreTable.Columns.Count(c => c.Header.Equals("slider end")) == 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all rows show non-zero slider ends", () =>
|
||||||
|
{
|
||||||
|
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
|
||||||
|
int sliderEndColumnIndex = Array.FindIndex(scoreTable.Columns, c => c != null && c.Header.Equals("slider end"));
|
||||||
|
bool sliderEndFilledInEachRow = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < scoreTable.Content?.GetLength(0); i++)
|
||||||
|
{
|
||||||
|
switch (scoreTable.Content[i, sliderEndColumnIndex])
|
||||||
|
{
|
||||||
|
case OsuSpriteText text:
|
||||||
|
if (text.Text.Equals(0.0d.ToLocalisableString(@"N0")))
|
||||||
|
sliderEndFilledInEachRow = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sliderEndFilledInEachRow = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sliderEndFilledInEachRow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestUserBest()
|
public void TestUserBest()
|
||||||
{
|
{
|
||||||
@ -103,6 +147,18 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
|
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
|
||||||
|
|
||||||
|
AddStep("Load scores with personal best FC", () =>
|
||||||
|
{
|
||||||
|
var allScores = createScores();
|
||||||
|
allScores.UserScore = createUserBest();
|
||||||
|
allScores.UserScore.Score.Accuracy = 1;
|
||||||
|
scoresContainer.Beatmap.Value.MaxCombo = allScores.UserScore.Score.MaxCombo = 1337;
|
||||||
|
scoresContainer.Scores = allScores;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
|
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
|
||||||
|
|
||||||
AddStep("Load scores with personal best (null position)", () =>
|
AddStep("Load scores with personal best (null position)", () =>
|
||||||
{
|
{
|
||||||
var allScores = createScores();
|
var allScores = createScores();
|
||||||
@ -287,13 +343,17 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
const int initial_great_count = 2000;
|
const int initial_great_count = 2000;
|
||||||
const int initial_tick_count = 100;
|
const int initial_tick_count = 100;
|
||||||
|
const int initial_slider_end_count = 500;
|
||||||
|
|
||||||
int greatCount = initial_great_count;
|
int greatCount = initial_great_count;
|
||||||
int tickCount = initial_tick_count;
|
int tickCount = initial_tick_count;
|
||||||
|
int sliderEndCount = initial_slider_end_count;
|
||||||
|
|
||||||
foreach (var s in scores.Scores)
|
foreach (var (score, index) in scores.Scores.Select((s, i) => (s, i)))
|
||||||
{
|
{
|
||||||
s.Statistics = new Dictionary<HitResult, int>
|
HitResult sliderEndResult = index % 2 == 0 ? HitResult.SliderTailHit : HitResult.SmallTickHit;
|
||||||
|
|
||||||
|
score.Statistics = new Dictionary<HitResult, int>
|
||||||
{
|
{
|
||||||
{ HitResult.Great, greatCount },
|
{ HitResult.Great, greatCount },
|
||||||
{ HitResult.LargeTickHit, tickCount },
|
{ HitResult.LargeTickHit, tickCount },
|
||||||
@ -301,10 +361,19 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{ HitResult.Meh, RNG.Next(100) },
|
{ HitResult.Meh, RNG.Next(100) },
|
||||||
{ HitResult.Miss, initial_great_count - greatCount },
|
{ HitResult.Miss, initial_great_count - greatCount },
|
||||||
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
|
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
|
||||||
|
{ sliderEndResult, sliderEndCount },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some hit results, including SliderTailHit and SmallTickHit, are only displayed
|
||||||
|
// when the maximum number is known
|
||||||
|
score.MaximumStatistics = new Dictionary<HitResult, int>
|
||||||
|
{
|
||||||
|
{ sliderEndResult, initial_slider_end_count },
|
||||||
};
|
};
|
||||||
|
|
||||||
greatCount -= 100;
|
greatCount -= 100;
|
||||||
tickCount -= RNG.Next(1, 5);
|
tickCount -= RNG.Next(1, 5);
|
||||||
|
sliderEndCount -= 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
return scores;
|
return scores;
|
||||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("select EZ mod", () =>
|
AddStep("select EZ mod", () =>
|
||||||
{
|
{
|
||||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||||
SelectedMods.Value = new[] { ruleset.CreateMod<ModEasy>() };
|
advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModEasy>() };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
|
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
|
||||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("select HR mod", () =>
|
AddStep("select HR mod", () =>
|
||||||
{
|
{
|
||||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||||
SelectedMods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
|
advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
|
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
|
||||||
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||||
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
||||||
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
||||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||||
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
||||||
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
|
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
|
||||||
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
|
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
|
||||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Skinning.Components;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public partial class TestSceneBeatmapAttributeText : OsuTestScene
|
||||||
|
{
|
||||||
|
private readonly BeatmapAttributeText text;
|
||||||
|
|
||||||
|
public TestSceneBeatmapAttributeText()
|
||||||
|
{
|
||||||
|
Child = text = new BeatmapAttributeText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
BPM = 100,
|
||||||
|
DifficultyName = "_Difficulty",
|
||||||
|
Status = BeatmapOnlineStatus.Loved,
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = "_Title",
|
||||||
|
TitleUnicode = "_Title",
|
||||||
|
Artist = "_Artist",
|
||||||
|
ArtistUnicode = "_Artist",
|
||||||
|
Author = new RealmUser { Username = "_Creator" },
|
||||||
|
Source = "_Source",
|
||||||
|
},
|
||||||
|
Difficulty =
|
||||||
|
{
|
||||||
|
CircleSize = 1,
|
||||||
|
DrainRate = 2,
|
||||||
|
OverallDifficulty = 3,
|
||||||
|
ApproachRate = 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[TestCase(BeatmapAttribute.CircleSize, "Circle Size: 1.00")]
|
||||||
|
[TestCase(BeatmapAttribute.HPDrain, "HP Drain: 2.00")]
|
||||||
|
[TestCase(BeatmapAttribute.Accuracy, "Accuracy: 3.00")]
|
||||||
|
[TestCase(BeatmapAttribute.ApproachRate, "Approach Rate: 4.00")]
|
||||||
|
[TestCase(BeatmapAttribute.Title, "Title: _Title")]
|
||||||
|
[TestCase(BeatmapAttribute.Artist, "Artist: _Artist")]
|
||||||
|
[TestCase(BeatmapAttribute.Creator, "Creator: _Creator")]
|
||||||
|
[TestCase(BeatmapAttribute.DifficultyName, "Difficulty: _Difficulty")]
|
||||||
|
[TestCase(BeatmapAttribute.Source, "Source: _Source")]
|
||||||
|
[TestCase(BeatmapAttribute.RankedStatus, "Beatmap Status: Loved")]
|
||||||
|
public void TestAttributeDisplay(BeatmapAttribute attribute, string expectedText)
|
||||||
|
{
|
||||||
|
AddStep($"set attribute: {attribute}", () => text.Attribute.Value = attribute);
|
||||||
|
AddAssert("check correct text", getText, () => Is.EqualTo(expectedText));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeBeatmap()
|
||||||
|
{
|
||||||
|
AddStep("set title attribute", () => text.Attribute.Value = BeatmapAttribute.Title);
|
||||||
|
AddAssert("check initial title", getText, () => Is.EqualTo("Title: _Title"));
|
||||||
|
|
||||||
|
AddStep("change to beatmap with another title", () => Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = "Another"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddAssert("check new title", getText, () => Is.EqualTo("Title: Another"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Comments;
|
using osu.Game.Overlays.Comments;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -28,9 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private TestCancellableCommentEditor cancellableCommentEditor = null!;
|
private TestCancellableCommentEditor cancellableCommentEditor = null!;
|
||||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||||
|
|
||||||
[SetUp]
|
[SetUpSteps]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUpSteps()
|
||||||
Add(new FillFlowContainer
|
{
|
||||||
|
AddStep("create content", () => Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -43,7 +45,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
commentEditor = new TestCommentEditor(),
|
commentEditor = new TestCommentEditor(),
|
||||||
cancellableCommentEditor = new TestCancellableCommentEditor()
|
cancellableCommentEditor = new TestCancellableCommentEditor()
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCommitViaKeyboard()
|
public void TestCommitViaKeyboard()
|
||||||
@ -133,6 +136,34 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
assertLoggedInState();
|
assertLoggedInState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCommentsDisabled()
|
||||||
|
{
|
||||||
|
AddStep("no reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||||
|
{
|
||||||
|
CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes(),
|
||||||
|
});
|
||||||
|
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("specific reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||||
|
{
|
||||||
|
CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes
|
||||||
|
{
|
||||||
|
CanNewCommentReason = "This comment section is disabled. For reasons.",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AddAssert("textbox disabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.True);
|
||||||
|
|
||||||
|
AddStep("entire commentable meta missing", () => commentEditor.CommentableMeta.Value = null);
|
||||||
|
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("current user attributes missing", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||||
|
{
|
||||||
|
CurrentUserAttributes = null,
|
||||||
|
});
|
||||||
|
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCancelAction()
|
public void TestCancelAction()
|
||||||
{
|
{
|
||||||
@ -167,8 +198,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||||
isLoggedIn ? @"Commit" : "You're logged out!";
|
isLoggedIn ? @"Commit" : "You're logged out!";
|
||||||
|
|
||||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
protected override LocalisableString GetPlaceholderText() => @"This text box is empty";
|
||||||
isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class TestCancellableCommentEditor : CancellableCommentEditor
|
private partial class TestCancellableCommentEditor : CancellableCommentEditor
|
||||||
@ -189,7 +219,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
|
protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
|
||||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon";
|
protected override LocalisableString GetPlaceholderText() => @"Multiline textboxes soon";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
},
|
},
|
||||||
new FormSliderBar<float>
|
new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Caption = "Instantaneous slider",
|
Caption = "Slider",
|
||||||
Current = new BindableFloat
|
Current = new BindableFloat
|
||||||
{
|
{
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
@ -82,19 +82,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
},
|
},
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
new FormSliderBar<float>
|
|
||||||
{
|
|
||||||
Caption = "Non-instantaneous slider",
|
|
||||||
Current = new BindableFloat
|
|
||||||
{
|
|
||||||
MinValue = 0,
|
|
||||||
MaxValue = 10,
|
|
||||||
Value = 5,
|
|
||||||
Precision = 0.1f,
|
|
||||||
},
|
|
||||||
Instantaneous = false,
|
|
||||||
TabbableContentContainer = this,
|
|
||||||
},
|
|
||||||
new FormEnumDropdown<CountdownType>
|
new FormEnumDropdown<CountdownType>
|
||||||
{
|
{
|
||||||
Caption = EditorSetupStrings.EnableCountdown,
|
Caption = EditorSetupStrings.EnableCountdown,
|
||||||
@ -105,6 +92,17 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Caption = "Audio file",
|
Caption = "Audio file",
|
||||||
PlaceholderText = "Select an audio file",
|
PlaceholderText = "Select an audio file",
|
||||||
},
|
},
|
||||||
|
new FormColourPalette
|
||||||
|
{
|
||||||
|
Caption = "Combo colours",
|
||||||
|
Colours =
|
||||||
|
{
|
||||||
|
Colour4.Red,
|
||||||
|
Colour4.Green,
|
||||||
|
Colour4.Blue,
|
||||||
|
Colour4.Yellow,
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public partial class TestSceneFormSliderBar : OsuTestScene
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTransferValueOnCommit()
|
||||||
|
{
|
||||||
|
OsuSpriteText text;
|
||||||
|
FormSliderBar<float> slider = null!;
|
||||||
|
|
||||||
|
AddStep("create content", () =>
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Width = 0.5f,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
text = new OsuSpriteText(),
|
||||||
|
slider = new FormSliderBar<float>
|
||||||
|
{
|
||||||
|
Caption = "Slider",
|
||||||
|
Current = new BindableFloat
|
||||||
|
{
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 0.1f,
|
||||||
|
Default = 5f,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true);
|
||||||
|
});
|
||||||
|
AddToggleStep("toggle transfer value on commit", b =>
|
||||||
|
{
|
||||||
|
if (slider.IsNotNull())
|
||||||
|
slider.TransferValueOnCommit = b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public partial class TestSceneHotkeyDisplay : ThemeComparisonTestScene
|
||||||
|
{
|
||||||
|
protected override Drawable CreateContent() => new FillFlowContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
new HotkeyDisplay { Hotkey = new Hotkey(new KeyCombination(InputKey.MouseLeft)) },
|
||||||
|
new HotkeyDisplay { Hotkey = new Hotkey(GlobalAction.EditorDecreaseDistanceSpacing) },
|
||||||
|
new HotkeyDisplay { Hotkey = new Hotkey(PlatformAction.Save) },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
|
||||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -2,18 +2,16 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Tournament.Models;
|
using osu.Game.Tournament.Models;
|
||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Editors.Components
|
namespace osu.Game.Tournament.Screens.Editors.Components
|
||||||
{
|
{
|
||||||
public partial class DeleteRoundDialog : DangerousActionDialog
|
public partial class DeleteRoundDialog : DeletionDialog
|
||||||
{
|
{
|
||||||
public DeleteRoundDialog(TournamentRound round, Action action)
|
public DeleteRoundDialog(TournamentRound round, Action action)
|
||||||
{
|
{
|
||||||
HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?";
|
HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?";
|
||||||
Icon = FontAwesome.Solid.Trash;
|
|
||||||
DangerousAction = action;
|
DangerousAction = action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,18 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Tournament.Models;
|
using osu.Game.Tournament.Models;
|
||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Editors.Components
|
namespace osu.Game.Tournament.Screens.Editors.Components
|
||||||
{
|
{
|
||||||
public partial class DeleteTeamDialog : DangerousActionDialog
|
public partial class DeleteTeamDialog : DeletionDialog
|
||||||
{
|
{
|
||||||
public DeleteTeamDialog(TournamentTeam team, Action action)
|
public DeleteTeamDialog(TournamentTeam team, Action action)
|
||||||
{
|
{
|
||||||
HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" :
|
HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" :
|
||||||
team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" :
|
team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" :
|
||||||
@"Delete unnamed team?";
|
@"Delete unnamed team?";
|
||||||
Icon = FontAwesome.Solid.Trash;
|
|
||||||
DangerousAction = action;
|
DangerousAction = action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
private BeatmapInfo()
|
protected BeatmapInfo()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r =>
|
||||||
|
r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||||
@ -313,6 +314,23 @@ namespace osu.Game.Beatmaps
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetAllOffsets()
|
||||||
|
{
|
||||||
|
const string reset_complete_message = "All offsets have been reset!";
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
var items = r.All<BeatmapInfo>();
|
||||||
|
|
||||||
|
foreach (var beatmap in items)
|
||||||
|
{
|
||||||
|
if (beatmap.UserSettings.Offset != 0)
|
||||||
|
beatmap.UserSettings.Offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostNotification?.Invoke(new ProgressCompletionNotification { Text = reset_complete_message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
||||||
{
|
{
|
||||||
Realm.Run(r =>
|
Realm.Run(r =>
|
||||||
|
@ -183,7 +183,14 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
#region Beatmap
|
#region Beatmap
|
||||||
|
|
||||||
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
public virtual bool BeatmapLoaded
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (beatmapFetchLock)
|
||||||
|
return beatmapLoadTask?.IsCompleted ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IBeatmap Beatmap
|
public IBeatmap Beatmap
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@ using osu.Game.Overlays.Dialog;
|
|||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
public partial class DeleteCollectionDialog : DangerousActionDialog
|
public partial class DeleteCollectionDialog : DeletionDialog
|
||||||
{
|
{
|
||||||
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
||||||
{
|
{
|
||||||
|
@ -606,7 +606,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
// Importantly, also sleep if high performance session is active.
|
// Importantly, also sleep if high performance session is active.
|
||||||
// If we don't do this, memory usage can become runaway due to GC running in a more lenient mode.
|
// If we don't do this, memory usage can become runaway due to GC running in a more lenient mode.
|
||||||
while (localUserPlayInfo?.IsPlaying.Value == true || highPerformanceSessionManager?.IsSessionActive == true)
|
while (localUserPlayInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying || highPerformanceSessionManager?.IsSessionActive == true)
|
||||||
{
|
{
|
||||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
Logger.Log("Background processing sleeping due to active gameplay...");
|
||||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
{
|
{
|
||||||
if (detachedBeatmapSets.Count > 0 && sender.Count == 0)
|
if (sender is RealmResetEmptySet<BeatmapSetInfo>)
|
||||||
{
|
{
|
||||||
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
|
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
|
||||||
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
|
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
|
||||||
|
@ -93,8 +93,9 @@ namespace osu.Game.Database
|
|||||||
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
||||||
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
|
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
|
||||||
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
||||||
|
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 42;
|
private const int schema_version = 43;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
@ -375,10 +376,6 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
foreach (var beatmap in beatmapSet.Beatmaps)
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||||
{
|
{
|
||||||
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
|
|
||||||
foreach (var score in beatmap.Scores)
|
|
||||||
realm.Remove(score);
|
|
||||||
|
|
||||||
realm.Remove(beatmap.Metadata);
|
realm.Remove(beatmap.Metadata);
|
||||||
realm.Remove(beatmap);
|
realm.Remove(beatmap);
|
||||||
}
|
}
|
||||||
@ -568,7 +565,7 @@ namespace osu.Game.Database
|
|||||||
lock (notificationsResetMap)
|
lock (notificationsResetMap)
|
||||||
{
|
{
|
||||||
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
||||||
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null));
|
notificationsResetMap.Add(action, () => callback(new RealmResetEmptySet<T>(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
return RegisterCustomSubscription(action);
|
return RegisterCustomSubscription(action);
|
||||||
@ -1192,6 +1189,21 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 43:
|
||||||
|
{
|
||||||
|
// Clear default bindings for "Toggle FPS Display",
|
||||||
|
// as it conflicts with "Convert to Stream" in the editor.
|
||||||
|
// Only apply change if set to the conflicting bind
|
||||||
|
// i.e. has been manually rebound by the user.
|
||||||
|
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
||||||
|
|
||||||
|
var toggleFpsBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleFPSDisplay);
|
||||||
|
if (toggleFpsBind != null && toggleFpsBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.F }))
|
||||||
|
migration.NewRealm.Remove(toggleFpsBind);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user