mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 11:47:24 +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"
|
||||
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install maui-ios
|
||||
|
||||
- name: Select Xcode 16
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
|
||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||
|
||||
- name: Build
|
||||
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 }}
|
||||
|
||||
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:
|
||||
name: Check permissions
|
||||
runs-on: ubuntu-latest
|
||||
@ -121,7 +140,7 @@ jobs:
|
||||
|
||||
create-comment:
|
||||
name: Create PR comment
|
||||
needs: check-permissions
|
||||
needs: [ master-environment, check-permissions ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
@ -158,7 +177,7 @@ jobs:
|
||||
|
||||
environment:
|
||||
name: Setup environment
|
||||
needs: directory
|
||||
needs: [ master-environment, directory ]
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
VARS_JSON: ${{ toJSON(vars) }}
|
||||
@ -182,6 +201,10 @@ jobs:
|
||||
fi
|
||||
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
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
run: |
|
||||
@ -250,22 +273,23 @@ jobs:
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
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
|
||||
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 }}"
|
||||
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 "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
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
|
||||
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 }}"
|
||||
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 }}"
|
||||
|
||||
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-)
|
||||
|
||||
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
||||
@ -328,7 +356,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
docker-compose down -v
|
||||
docker compose down --volumes
|
||||
|
||||
output-cli:
|
||||
name: Output info
|
||||
@ -361,8 +389,7 @@ jobs:
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
create_if_not_exists: false
|
||||
mode: recreate
|
||||
message: |
|
||||
Target: ${{ needs.generator.outputs.TARGET }}
|
||||
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
||||
@ -372,8 +399,7 @@ jobs:
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
create_if_not_exists: false
|
||||
mode: recreate
|
||||
message: |
|
||||
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": [
|
||||
"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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.927.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- 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.Graphics;
|
||||
using osu.Game;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
public partial class GameplayScreenRotationLocker : Component
|
||||
{
|
||||
private Bindable<bool> localUserPlaying = null!;
|
||||
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameActivity gameActivity { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame game)
|
||||
private void load(ILocalUserPlayInfo localUserPlayInfo)
|
||||
{
|
||||
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
|
||||
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
|
||||
localUserPlaying.BindValueChanged(updateLock, true);
|
||||
}
|
||||
|
||||
private void updateLock(ValueChangedEvent<bool> userPlaying)
|
||||
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
|
||||
{
|
||||
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?
|
||||
// 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,
|
||||
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
|
||||
if (str.Length < 2)
|
||||
return str.PadRight(2, '\u200B');
|
||||
// Also, spaces don't count. Because reasons, clearly.
|
||||
// That all seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end. After making sure to trim whitespace.
|
||||
string trimmed = str.Trim();
|
||||
if (trimmed.Length < 2)
|
||||
return trimmed.PadRight(2, '\u200B');
|
||||
|
||||
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
||||
return str;
|
||||
|
@ -25,6 +25,8 @@ namespace osu.Desktop.Updater
|
||||
[Resolved]
|
||||
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
||||
|
||||
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
|
||||
|
||||
private UpdateInfo? pendingUpdate;
|
||||
|
||||
public VelopackUpdateManager()
|
||||
@ -43,7 +45,7 @@ namespace osu.Desktop.Updater
|
||||
|
||||
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).
|
||||
bool scheduleRecheck = false;
|
||||
@ -51,10 +53,10 @@ namespace osu.Desktop.Updater
|
||||
try
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (localUserInfo?.IsPlaying.Value == true)
|
||||
if (isInGameplay)
|
||||
{
|
||||
scheduleRecheck = true;
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
||||
@ -84,27 +86,22 @@ namespace osu.Desktop.Updater
|
||||
}
|
||||
|
||||
// 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 = () =>
|
||||
{
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
}
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
runOutsideOfGameplay(() => notificationOverlay.Post(notification));
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -131,6 +128,17 @@ namespace osu.Desktop.Updater
|
||||
return true;
|
||||
}
|
||||
|
||||
private void runOutsideOfGameplay(Action action)
|
||||
{
|
||||
if (isInGameplay)
|
||||
{
|
||||
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
action();
|
||||
}
|
||||
|
||||
private async Task restartToApplyUpdate()
|
||||
{
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Desktop.Windows
|
||||
public partial class GameplayWinKeyBlocker : Component
|
||||
{
|
||||
private Bindable<bool> disableWinKey = null!;
|
||||
private IBindable<bool> localUserPlaying = null!;
|
||||
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
||||
private IBindable<bool> isActive = null!;
|
||||
|
||||
[Resolved]
|
||||
@ -22,7 +22,7 @@ namespace osu.Desktop.Windows
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
|
||||
{
|
||||
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
|
||||
localUserPlaying = localUserInfo.PlayingState.GetBoundCopy();
|
||||
localUserPlaying.BindValueChanged(_ => updateBlocking());
|
||||
|
||||
isActive = host.IsActive.GetBoundCopy();
|
||||
@ -34,7 +34,7 @@ namespace osu.Desktop.Windows
|
||||
|
||||
private void updateBlocking()
|
||||
{
|
||||
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value;
|
||||
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value == LocalUserPlayingState.Playing;
|
||||
|
||||
if (shouldDisable)
|
||||
host.InputThread.Scheduler.Add(WindowsKey.Disable);
|
||||
|
@ -24,7 +24,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<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="Velopack" Version="0.0.630-g9c52e40" />
|
||||
</ItemGroup>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
@ -31,6 +32,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
@ -223,10 +225,28 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
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();
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
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, () =>
|
||||
{
|
||||
editablePath.AddVertex(rightMouseDownPosition);
|
||||
});
|
||||
})
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestKeyCountChange()
|
||||
{
|
||||
LabelledSliderBar<float> keyCount = null!;
|
||||
FormSliderBar<float> keyCount = null!;
|
||||
|
||||
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));
|
||||
AddStep("change key count to 8", () =>
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20230817;
|
||||
public override int Version => 20241007;
|
||||
|
||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
|
@ -19,12 +19,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private LabelledSwitchButton specialStyle { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private FormCheckBox specialStyle { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
@ -37,77 +37,81 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
keyCountSlider = new LabelledSliderBar<float>
|
||||
keyCountSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsCsMania,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = "The number of columns in the beatmap",
|
||||
Caption = BeatmapsetsStrings.ShowStatsCsMania,
|
||||
HintText = "The number of columns in the beatmap",
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
specialStyle = new LabelledSwitchButton
|
||||
specialStyle = new FormCheckBox
|
||||
{
|
||||
Label = "Use special (N+1) style",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
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.",
|
||||
Caption = "Use special (N+1) style",
|
||||
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.",
|
||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||
},
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -419,9 +419,12 @@ namespace osu.Game.Rulesets.Mania
|
||||
return new ManiaFilterCriteria();
|
||||
}
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new ManiaDifficultySection(),
|
||||
new ResourcesSection(),
|
||||
new DesignSection(),
|
||||
];
|
||||
|
||||
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.
|
||||
// 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.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 osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
@ -10,5 +21,66 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public partial class TestSceneOsuEditor : EditorTestScene
|
||||
{
|
||||
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.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Utils;
|
||||
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
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("move cursor to spacing + (1, 1)", () =>
|
||||
{
|
||||
@ -161,7 +163,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
return grid switch
|
||||
{
|
||||
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),
|
||||
_ => Vector2.Zero
|
||||
};
|
||||
@ -170,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
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());
|
||||
gridSizeIs(4);
|
||||
|
||||
@ -189,5 +192,97 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
private void gridSizeIs(int size)
|
||||
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -392,6 +393,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
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]
|
||||
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 addTouchMovementStep(Vector2 position) => AddStep($"move touch1 to {position}", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, InputManager.ToScreenSpace(position))));
|
||||
|
||||
private void addClickStep(MouseButton 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";
|
||||
|
||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14102693012101306d, 2, "nan-slider")]
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(8.9742952703071666d, 239, "diffcalc-test")]
|
||||
[TestCase(1.743180218215227d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55071082800473514d, 4, "very-fast-slider")]
|
||||
[TestCase(8.9825709931204205d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -10,8 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
public static class RhythmEvaluator
|
||||
{
|
||||
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
|
||||
private const double rhythm_multiplier = 0.75;
|
||||
private const int history_time_max = 5 * 1000; // 5 seconds
|
||||
private const int history_objects_max = 32;
|
||||
private const double rhythm_overall_multiplier = 0.95;
|
||||
private const double rhythm_ratio_multiplier = 12.0;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
return 0;
|
||||
|
||||
int previousIslandSize = 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
|
||||
|
||||
bool firstDeltaSwitch = false;
|
||||
|
||||
int historicalNoteCount = Math.Min(current.Index, 32);
|
||||
int historicalNoteCount = Math.Min(current.Index, history_objects_max);
|
||||
|
||||
int rhythmStart = 0;
|
||||
|
||||
@ -39,74 +50,177 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
||||
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--)
|
||||
{
|
||||
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 prevDelta = prevObj.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 (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
||||
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
||||
{
|
||||
if (islandSize < 7)
|
||||
islandSize++; // island is still progressing, count size.
|
||||
// island is still progressing
|
||||
island.AddDelta((int)currDelta);
|
||||
}
|
||||
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;
|
||||
|
||||
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||
effectiveRatio *= 0.25;
|
||||
// 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.3;
|
||||
|
||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||
effectiveRatio *= 0.25;
|
||||
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||
if (island.IsSimilarPolarity(previousIsland))
|
||||
effectiveRatio *= 0.5;
|
||||
|
||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
||||
effectiveRatio *= 0.50;
|
||||
|
||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||
// previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||
if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon)
|
||||
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;
|
||||
|
||||
previousIslandSize = islandSize; // log the last island size.
|
||||
previousIsland = island;
|
||||
|
||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||
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.
|
||||
|
||||
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.
|
||||
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;
|
||||
islandSize = 1;
|
||||
|
||||
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
||||
}
|
||||
|
||||
lastObj = prevObj;
|
||||
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 min_speed_bonus = 75; // ~200BPM
|
||||
private const double speed_balancing_factor = 40;
|
||||
private const double distance_multiplier = 0.94;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||
var osuNextObj = (OsuDifficultyHitObject?)current.Next(0);
|
||||
|
||||
double strainTime = osuCurrObj.StrainTime;
|
||||
double doubletapness = 1;
|
||||
|
||||
// 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);
|
||||
}
|
||||
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
||||
|
||||
// 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.
|
||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||
|
||||
// speedBonus will be 1.0 for BPM < 200
|
||||
double speedBonus = 1.0;
|
||||
// speedBonus will be 0.0 for BPM < 200
|
||||
double speedBonus = 0.0;
|
||||
|
||||
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||
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 distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||
@ -63,11 +52,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// Cap distance at single_spacing_threshold
|
||||
distance = Math.Min(distance, single_spacing_threshold);
|
||||
|
||||
// Max distance bonus is 2 at single_spacing_threshold
|
||||
double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5);
|
||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||
|
||||
// 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
|
||||
return difficulty * doubletapness;
|
||||
|
@ -46,6 +46,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("slider_factor")]
|
||||
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>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
@ -99,6 +105,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -113,8 +122,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
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];
|
||||
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
|
||||
public override int Version => 20220902;
|
||||
public override int Version => 20241007;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@ -48,6 +48,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
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))
|
||||
{
|
||||
aimRating = Math.Pow(aimRating, 0.8);
|
||||
@ -100,6 +103,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SpeedNoteCount = speedNotes,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
AimDifficultStrainCount = aimDifficultyStrainCount,
|
||||
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
|
@ -14,7 +14,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
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 int scoreMaxCombo;
|
||||
@ -23,6 +25,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
private int countMeh;
|
||||
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;
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
@ -34,13 +49,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
||||
|
||||
usingClassicSliderAccuracy = score.Mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);
|
||||
|
||||
accuracy = score.Accuracy;
|
||||
scoreMaxCombo = score.MaxCombo;
|
||||
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
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;
|
||||
|
||||
@ -93,11 +139,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
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)
|
||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
|
||||
|
||||
aimValue *= getComboScalingFactor(attributes);
|
||||
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
@ -123,8 +166,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
if (attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -146,11 +203,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
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)
|
||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
speedValue *= getComboScalingFactor(attributes);
|
||||
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
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);
|
||||
|
||||
// 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.
|
||||
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.
|
||||
double betterAccuracyPercentage;
|
||||
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
||||
if (!usingClassicSliderAccuracy)
|
||||
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||
|
||||
if (amountHitObjectsWithAccuracy > 0)
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
@ -247,25 +303,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
// Guess the number of misses + slider breaks from combo
|
||||
double comboBasedMissCount = 0.0;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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
|
||||
// to make it more punishing on maps with lower amount of hard sections.
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||
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 totalImperfectHits => countOk + countMeh + countMiss;
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
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 assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
||||
|
||||
@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
this.lastObject = (OsuHitObject)lastObject;
|
||||
|
||||
// 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)
|
||||
{
|
||||
@ -136,6 +137,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
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)
|
||||
{
|
||||
if (BaseObject is Slider currentSlider)
|
||||
@ -143,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
computeSliderCursorPosition(currentSlider);
|
||||
// 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);
|
||||
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
|
||||
@ -167,8 +186,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
if (lastObject is Slider lastSlider)
|
||||
{
|
||||
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
|
||||
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
|
||||
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 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.
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 24.963;
|
||||
private double skillMultiplier => 25.18;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
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 += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||
ObjectStrains.Add(currentStrain);
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
@ -23,6 +23,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
protected virtual double ReducedStrainBaseline => 0.75;
|
||||
|
||||
protected List<double> ObjectStrains = new List<double>();
|
||||
protected double Difficulty;
|
||||
|
||||
protected OsuStrainSkill(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
@ -30,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
double difficulty = 0;
|
||||
Difficulty = 0;
|
||||
double weight = 1;
|
||||
|
||||
// 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.
|
||||
foreach (double strain in strains.OrderDescending())
|
||||
{
|
||||
difficulty += strain * weight;
|
||||
Difficulty += strain * weight;
|
||||
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;
|
||||
|
@ -6,7 +6,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
|
||||
public Speed(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
@ -43,23 +40,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
|
||||
objectStrains.Add(totalStrain);
|
||||
ObjectStrains.Add(totalStrain);
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
|
||||
public double RelevantNoteCount()
|
||||
{
|
||||
if (objectStrains.Count == 0)
|
||||
if (ObjectStrains.Count == 0)
|
||||
return 0;
|
||||
|
||||
double maxStrain = objectStrains.Max();
|
||||
|
||||
double maxStrain = ObjectStrains.Max();
|
||||
if (maxStrain == 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)
|
||||
return;
|
||||
|
||||
var first = segment[0];
|
||||
PathControlPoint first = segment[0];
|
||||
|
||||
if (first.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private bool isSplittable(PathControlPointPiece<T> p) =>
|
||||
// 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)
|
||||
{
|
||||
@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (selectedPieces.Length != 1)
|
||||
return false;
|
||||
|
||||
var selectedPiece = selectedPieces.Single();
|
||||
var selectedPoint = selectedPiece.ControlPoint;
|
||||
PathControlPointPiece<T> selectedPiece = selectedPieces.Single();
|
||||
PathControlPoint selectedPoint = selectedPiece.ControlPoint;
|
||||
|
||||
var validTypes = path_types;
|
||||
PathType?[] validTypes = path_types;
|
||||
|
||||
if (selectedPoint == controlPoints[0])
|
||||
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))
|
||||
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
|
||||
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))
|
||||
{
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
List<PathControlPoint> pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
@ -412,14 +412,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public void DragInProgress(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = hitObject.Position;
|
||||
Vector2 oldPosition = hitObject.Position;
|
||||
double oldStartTime = hitObject.StartTime;
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
@ -428,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
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
|
||||
// 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
|
||||
@ -439,13 +439,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
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;
|
||||
|
||||
for (int i = 0; i < controlPoints.Count; ++i)
|
||||
{
|
||||
var controlPoint = controlPoints[i];
|
||||
PathControlPoint controlPoint = controlPoints[i];
|
||||
if (selectedControlPoints.Contains(controlPoint))
|
||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||
}
|
||||
@ -495,8 +495,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
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
|
||||
if (type == null)
|
||||
{
|
||||
@ -506,7 +508,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
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))
|
||||
@ -540,7 +542,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
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.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
@ -593,8 +594,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
changeHandler?.BeginChange();
|
||||
addControlPoint(lastRightClickPosition);
|
||||
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.
|
||||
|
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -37,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
MinValue = 0f,
|
||||
MaxValue = OsuPlayfield.BASE_SIZE.X,
|
||||
Precision = 1f
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
MinValue = 0f,
|
||||
MaxValue = OsuPlayfield.BASE_SIZE.Y,
|
||||
Precision = 1f
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
MinValue = 4f,
|
||||
MaxValue = 128f,
|
||||
Precision = 1f
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -67,14 +65,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
MinValue = -180f,
|
||||
MaxValue = 180f,
|
||||
Precision = 1f
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Read-only bindable representing the grid's origin.
|
||||
/// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code>
|
||||
/// </summary>
|
||||
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>();
|
||||
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>(OsuPlayfield.BASE_SIZE / 2);
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
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]
|
||||
private void load()
|
||||
{
|
||||
@ -160,22 +177,28 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
StartPositionX.BindValueChanged(x =>
|
||||
{
|
||||
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
|
||||
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}";
|
||||
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
|
||||
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}";
|
||||
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
||||
}, true);
|
||||
|
||||
StartPositionY.BindValueChanged(y =>
|
||||
{
|
||||
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}";
|
||||
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}";
|
||||
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
|
||||
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}";
|
||||
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||
}, true);
|
||||
|
||||
StartPosition.BindValueChanged(pos =>
|
||||
{
|
||||
StartPositionX.Value = pos.NewValue.X;
|
||||
StartPositionY.Value = pos.NewValue.Y;
|
||||
});
|
||||
|
||||
Spacing.BindValueChanged(spacing =>
|
||||
{
|
||||
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}";
|
||||
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}";
|
||||
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
|
||||
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
|
||||
SpacingVector.Value = new Vector2(spacing.NewValue);
|
||||
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
|
||||
}, true);
|
||||
@ -186,44 +209,50 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
||||
}, 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 =>
|
||||
{
|
||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||
|
||||
gridTypeButtons.Items[(int)v.NewValue].Select();
|
||||
|
||||
switch (v.NewValue)
|
||||
{
|
||||
case PositionSnapGridType.Square:
|
||||
GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45;
|
||||
GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 90);
|
||||
GridLinesRotation.MinValue = -45;
|
||||
GridLinesRotation.MaxValue = 45;
|
||||
break;
|
||||
|
||||
case PositionSnapGridType.Triangle:
|
||||
GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30;
|
||||
GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 60);
|
||||
GridLinesRotation.MinValue = -30;
|
||||
GridLinesRotation.MaxValue = 30;
|
||||
break;
|
||||
}
|
||||
}, 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)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorCycleGridDisplayMode:
|
||||
nextGridSize();
|
||||
case GlobalAction.EditorCycleGridSpacing:
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
new HitCircleCompositionTool(),
|
||||
new SliderCompositionTool(),
|
||||
new SpinnerCompositionTool()
|
||||
new SpinnerCompositionTool(),
|
||||
new GridFromPointsTool()
|
||||
};
|
||||
|
||||
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.
|
||||
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||
|
||||
LayerBelowRuleset.AddRange(new Drawable[]
|
||||
{
|
||||
LayerBelowRuleset.Add(
|
||||
distanceSnapGridContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
||||
selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();
|
||||
|
@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
points = originalConvexHull!;
|
||||
|
||||
foreach (var point in points)
|
||||
{
|
||||
scale = clampToBound(scale, point, Vector2.Zero);
|
||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
||||
}
|
||||
scale = clampToBounds(scale, point, Vector2.Zero, 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);
|
||||
|
||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
||||
// 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)
|
||||
{
|
||||
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 b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||
|
||||
float sLowerBound, sUpperBound;
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -136,8 +136,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
});
|
||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||
|
||||
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
||||
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
||||
xCheckBox.Current.BindValueChanged(_ =>
|
||||
{
|
||||
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);
|
||||
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()
|
||||
{
|
||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||
@ -175,12 +199,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
axisBindable.Disabled = !available;
|
||||
}
|
||||
|
||||
private void updateMaxScale()
|
||||
private void updateMinMaxScale()
|
||||
{
|
||||
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||
return;
|
||||
|
||||
const float min_scale = 0.5f;
|
||||
const float max_scale = 10;
|
||||
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||
|
||||
if (!scaleInfo.Value.XAxis)
|
||||
@ -189,12 +215,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
scale.Y = max_scale;
|
||||
|
||||
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)
|
||||
{
|
||||
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||
updateMaxScale();
|
||||
updateMinMaxScale();
|
||||
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 void setAxis(bool x, bool y)
|
||||
{
|
||||
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
||||
updateMaxScale();
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
scaleHandler.Begin();
|
||||
updateMaxScale();
|
||||
updateMinMaxScale();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
public partial class OsuDifficultySection : SetupSection
|
||||
{
|
||||
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> stackLeniency { get; set; } = null!;
|
||||
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> stackLeniency { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
@ -31,103 +31,110 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
circleSizeSlider = new LabelledSliderBar<float>
|
||||
circleSizeSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsCs,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.CircleSizeDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsCs,
|
||||
HintText = EditorSetupStrings.CircleSizeDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
approachRateSlider = new LabelledSliderBar<float>
|
||||
approachRateSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAr,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.ApproachRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAr,
|
||||
HintText = EditorSetupStrings.ApproachRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
stackLeniency = new LabelledSliderBar<float>
|
||||
stackLeniency = new FormSliderBar<float>
|
||||
{
|
||||
Label = "Stack Leniency",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
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.",
|
||||
Caption = "Stack Leniency",
|
||||
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.",
|
||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
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();
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
||||
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
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),
|
||||
StackHeight = StackHeight,
|
||||
Scale = Scale,
|
||||
PathProgress = e.PathProgress,
|
||||
});
|
||||
break;
|
||||
|
||||
@ -150,6 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
Scale = Scale,
|
||||
PathProgress = e.PathProgress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -204,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
SpanStartTime = e.SpanStartTime,
|
||||
StartTime = e.Time,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
PathProgress = e.PathProgress,
|
||||
StackHeight = StackHeight,
|
||||
});
|
||||
break;
|
||||
@ -236,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
PathProgress = e.PathProgress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -248,14 +250,27 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
endPositionCache.Invalidate();
|
||||
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.Position = Position;
|
||||
foreach (var nested in NestedHitObjects)
|
||||
{
|
||||
switch (nested)
|
||||
{
|
||||
case SliderHeadCircle headCircle:
|
||||
headCircle.Position = Position;
|
||||
break;
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
case SliderTailCircle tailCircle:
|
||||
tailCircle.Position = EndPosition;
|
||||
break;
|
||||
|
||||
if (LastRepeat != null)
|
||||
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
||||
case SliderRepeat repeat:
|
||||
repeat.Position = Position + Path.PositionAt(repeat.PathProgress);
|
||||
break;
|
||||
|
||||
case SliderTick tick:
|
||||
tick.Position = Position + Path.PositionAt(tick.PathProgress);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateNestedSamples()
|
||||
|
@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SliderRepeat : SliderEndCircle
|
||||
{
|
||||
public double PathProgress { get; set; }
|
||||
|
||||
public SliderRepeat(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public int SpanIndex { get; set; }
|
||||
public double SpanStartTime { get; set; }
|
||||
public double PathProgress { get; set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
@ -39,6 +40,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
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 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"/>
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@ -117,10 +116,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
if (osuObject is not Slider slider)
|
||||
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);
|
||||
|
||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
||||
modifySlider(slider, reflectControlPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -134,10 +132,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
if (osuObject is not Slider slider)
|
||||
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);
|
||||
|
||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
||||
modifySlider(slider, reflectControlPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -146,10 +143,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
/// <param name="slider">The slider to be flipped.</param>
|
||||
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);
|
||||
|
||||
modifySlider(slider, flipNestedObject, flipControlPoint);
|
||||
modifySlider(slider, flipControlPoint);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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);
|
||||
|
||||
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();
|
||||
foreach (var point in controlPoints)
|
||||
modifyControlPoint(point);
|
||||
|
@ -232,8 +232,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||
workingObject.EndPositionModified = slider.EndPosition;
|
||||
|
||||
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
|
||||
|
||||
return workingObject.PositionModified - previousPosition;
|
||||
}
|
||||
|
||||
@ -307,22 +305,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
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>
|
||||
/// Clamp a position to playfield, keeping a specified distance from the edges.
|
||||
/// </summary>
|
||||
@ -431,7 +413,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
private class WorkingObject
|
||||
{
|
||||
public float RotationOriginal { get; }
|
||||
public Vector2 PositionOriginal { get; }
|
||||
public Vector2 PositionModified { get; set; }
|
||||
public Vector2 EndPositionModified { get; set; }
|
||||
|
||||
@ -442,7 +423,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
{
|
||||
PositionInfo = positionInfo;
|
||||
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
||||
PositionModified = PositionOriginal = HitObject.Position;
|
||||
PositionModified = HitObject.Position;
|
||||
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">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("great_hit_window")]
|
||||
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()
|
||||
{
|
||||
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_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||
}
|
||||
|
||||
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];
|
||||
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 stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
|
||||
public override int Version => 20221107;
|
||||
public override int Version => 20241007;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
[JsonProperty("estimated_unstable_rate")]
|
||||
public double? EstimatedUnstableRate { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> 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.Taiko.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
private int countOk;
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
private double accuracy;
|
||||
private double? estimatedUnstableRate;
|
||||
|
||||
private double effectiveMissCount;
|
||||
|
||||
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
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.
|
||||
if (totalSuccessfulHits > 0)
|
||||
@ -65,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
Difficulty = difficultyValue,
|
||||
Accuracy = accuracyValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
EstimatedUnstableRate = estimatedUnstableRate,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
@ -85,35 +87,94 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
difficultyValue *= 1.025;
|
||||
|
||||
if (score.Mods.Any(m => m is ModHardRock))
|
||||
difficultyValue *= 1.050;
|
||||
difficultyValue *= 1.10;
|
||||
|
||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||
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)
|
||||
{
|
||||
if (attributes.GreatHitWindow <= 0)
|
||||
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
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));
|
||||
accuracyValue *= lengthBonus;
|
||||
|
||||
// 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)
|
||||
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);
|
||||
accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus);
|
||||
|
||||
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 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
|
||||
{
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
@ -28,64 +28,68 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
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();
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
||||
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -86,10 +87,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
{
|
||||
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))
|
||||
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))
|
||||
yield return item;
|
||||
|
@ -190,9 +190,12 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new TaikoDifficultySection(),
|
||||
new ResourcesSection(),
|
||||
new DesignSection(),
|
||||
];
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
||||
|
@ -22,9 +22,9 @@ namespace osu.Game.Tests.Database
|
||||
[HeadlessTest]
|
||||
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!;
|
||||
|
||||
@ -37,7 +37,7 @@ namespace osu.Game.Tests.Database
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Set not playing", () => isPlaying.Value = false);
|
||||
AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
|
||||
}
|
||||
|
||||
[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", () =>
|
||||
{
|
||||
@ -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", () =>
|
||||
{
|
||||
|
@ -716,7 +716,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
foreach (var entry in zip.Entries.ToArray())
|
||||
{
|
||||
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
|
||||
if (entry.Key!.EndsWith(".osu", StringComparison.InvariantCulture))
|
||||
zip.RemoveEntry(entry);
|
||||
}
|
||||
|
||||
|
@ -267,23 +267,26 @@ namespace osu.Game.Tests.Editing
|
||||
[Test]
|
||||
public void TestUseCurrentSnap()
|
||||
{
|
||||
ExpandableButton getCurrentSnapButton() => composer.ChildrenOfType<EditorToolboxGroup>().Single(g => g.Name == "snapping")
|
||||
.ChildrenOfType<ExpandableButton>().Single();
|
||||
|
||||
AddStep("add objects to beatmap", () =>
|
||||
{
|
||||
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
||||
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
||||
});
|
||||
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
|
||||
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(getCurrentSnapButton()));
|
||||
AddUntilStep("use current snap expanded", () => getCurrentSnapButton().Expanded.Value, () => Is.True);
|
||||
|
||||
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));
|
||||
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));
|
||||
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)
|
||||
|
@ -3,11 +3,13 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Input
|
||||
@ -15,9 +17,20 @@ namespace osu.Game.Tests.Input
|
||||
[HeadlessTest]
|
||||
public partial class ConfineMouseTrackerTest : OsuGameTestScene
|
||||
{
|
||||
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
|
||||
|
||||
[Resolved]
|
||||
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.Borderless)]
|
||||
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));
|
||||
|
||||
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)
|
||||
=> 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);
|
||||
}
|
||||
|
||||
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 =
|
||||
{
|
||||
new object[] { "0", DateTimeOffset.MinValue, true },
|
||||
|
@ -6,10 +6,12 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Resources;
|
||||
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)));
|
||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||
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.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
@ -82,7 +83,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNudgeSelection()
|
||||
public void TestNudgeSelectionTime()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
@ -103,6 +104,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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]
|
||||
public void TestRotateHotkeys()
|
||||
{
|
||||
|
@ -7,12 +7,14 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
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;
|
||||
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private TestDesignSection designSection;
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
{
|
||||
@ -42,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
(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
|
||||
{
|
||||
public new LabelledSwitchButton EnableCountdown => base.EnableCountdown;
|
||||
public new FormCheckBox EnableCountdown => base.EnableCountdown;
|
||||
|
||||
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
|
||||
public new LabelledEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
||||
public new LabelledNumberBox CountdownOffset => base.CountdownOffset;
|
||||
public new FormEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
||||
public new FormTextBox CountdownOffset => base.CountdownOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK.Input;
|
||||
@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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)
|
||||
{
|
||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
EditorBeatmap.EndChange();
|
||||
});
|
||||
|
||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
||||
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||
|
||||
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||
|
@ -1,11 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -135,9 +137,42 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,13 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
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;
|
||||
@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Cached]
|
||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
||||
{
|
||||
@ -201,7 +206,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
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)
|
||||
=> 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
|
||||
{
|
||||
public new LabelledTextBox ArtistTextBox => base.ArtistTextBox;
|
||||
public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
||||
public new FormTextBox ArtistTextBox => base.ArtistTextBox;
|
||||
public new FormTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
||||
|
||||
public new LabelledTextBox TitleTextBox => base.TitleTextBox;
|
||||
public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
||||
public new FormTextBox TitleTextBox => base.TitleTextBox;
|
||||
public new FormTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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)
|
||||
{
|
||||
AddStep($"{step} {timestamp}", () =>
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Overlays;
|
||||
@ -12,7 +10,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneOverlayActivation : OsuPlayerTestScene
|
||||
{
|
||||
protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer;
|
||||
protected new OverlayTestPlayer Player => (OverlayTestPlayer)base.Player;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
|
@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
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.
|
||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||
|
||||
@ -77,5 +79,114 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
|
||||
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))]
|
||||
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();
|
||||
|
||||
public TestSceneGameplayChatDisplay()
|
||||
{
|
||||
var mockLocalUserInfo = new Mock<ILocalUserPlayInfo>();
|
||||
mockLocalUserInfo.SetupGet(i => i.IsPlaying).Returns(localUserPlaying);
|
||||
mockLocalUserInfo.SetupGet(i => i.PlayingState).Returns(playingState);
|
||||
|
||||
localUserInfo = mockLocalUserInfo.Object;
|
||||
}
|
||||
@ -124,6 +124,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
|
||||
|
||||
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);
|
||||
|
||||
AddStep("join other user (ready)", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID });
|
||||
MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.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);
|
||||
AddStep("other user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||
|
@ -8,11 +8,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
@ -90,6 +92,48 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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]
|
||||
public void TestUserBest()
|
||||
{
|
||||
@ -103,6 +147,18 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||
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)", () =>
|
||||
{
|
||||
var allScores = createScores();
|
||||
@ -287,13 +343,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
const int initial_great_count = 2000;
|
||||
const int initial_tick_count = 100;
|
||||
const int initial_slider_end_count = 500;
|
||||
|
||||
int greatCount = initial_great_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.LargeTickHit, tickCount },
|
||||
@ -301,10 +361,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{ HitResult.Meh, RNG.Next(100) },
|
||||
{ HitResult.Miss, initial_great_count - greatCount },
|
||||
{ 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;
|
||||
tickCount -= RNG.Next(1, 5);
|
||||
sliderEndCount -= 20;
|
||||
}
|
||||
|
||||
return scores;
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("select EZ mod", () =>
|
||||
{
|
||||
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));
|
||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("select HR mod", () =>
|
||||
{
|
||||
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));
|
||||
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
||||
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
||||
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||
});
|
||||
|
||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
||||
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
|
||||
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));
|
||||
|
@ -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.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osuTK;
|
||||
@ -28,9 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private TestCancellableCommentEditor cancellableCommentEditor = null!;
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
Add(new FillFlowContainer
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create content", () => Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -43,7 +45,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
commentEditor = new TestCommentEditor(),
|
||||
cancellableCommentEditor = new TestCancellableCommentEditor()
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommitViaKeyboard()
|
||||
@ -133,6 +136,34 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
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]
|
||||
public void TestCancelAction()
|
||||
{
|
||||
@ -167,8 +198,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"Commit" : "You're logged out!";
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it.";
|
||||
protected override LocalisableString GetPlaceholderText() => @"This text box is empty";
|
||||
}
|
||||
|
||||
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 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>
|
||||
{
|
||||
Caption = "Instantaneous slider",
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
@ -82,19 +82,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
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>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
@ -105,6 +92,17 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Caption = "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">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.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="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
|
@ -4,7 +4,7 @@
|
||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -2,18 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Tournament.Models;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.Editors.Components
|
||||
{
|
||||
public partial class DeleteRoundDialog : DangerousActionDialog
|
||||
public partial class DeleteRoundDialog : DeletionDialog
|
||||
{
|
||||
public DeleteRoundDialog(TournamentRound round, Action action)
|
||||
{
|
||||
HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?";
|
||||
Icon = FontAwesome.Solid.Trash;
|
||||
DangerousAction = action;
|
||||
}
|
||||
}
|
||||
|
@ -2,20 +2,18 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Tournament.Models;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.Editors.Components
|
||||
{
|
||||
public partial class DeleteTeamDialog : DangerousActionDialog
|
||||
public partial class DeleteTeamDialog : DeletionDialog
|
||||
{
|
||||
public DeleteTeamDialog(TournamentTeam team, Action action)
|
||||
{
|
||||
HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" :
|
||||
team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" :
|
||||
@"Delete unnamed team?";
|
||||
Icon = FontAwesome.Solid.Trash;
|
||||
DangerousAction = action;
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private BeatmapInfo()
|
||||
protected BeatmapInfo()
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <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>
|
||||
/// 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)
|
||||
{
|
||||
Realm.Run(r =>
|
||||
|
@ -183,7 +183,14 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
#region Beatmap
|
||||
|
||||
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
||||
public virtual bool BeatmapLoaded
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (beatmapFetchLock)
|
||||
return beatmapLoadTask?.IsCompleted ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public IBeatmap Beatmap
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
public partial class DeleteCollectionDialog : DangerousActionDialog
|
||||
public partial class DeleteCollectionDialog : DeletionDialog
|
||||
{
|
||||
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
||||
{
|
||||
|
@ -606,7 +606,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
// 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.
|
||||
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...");
|
||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
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.
|
||||
// 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.
|
||||
/// 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
|
||||
/// 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>
|
||||
private const int schema_version = 42;
|
||||
private const int schema_version = 43;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
@ -568,7 +565,7 @@ namespace osu.Game.Database
|
||||
lock (notificationsResetMap)
|
||||
{
|
||||
// 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);
|
||||
@ -1192,6 +1189,21 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
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");
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user