mirror of
https://github.com/ppy/osu.git
synced 2025-02-03 17:22:57 +08:00
Merge branch 'master' into barrel_roll_fix
This commit is contained in:
commit
8349226d9f
23
.github/workflows/diffcalc.yml
vendored
23
.github/workflows/diffcalc.yml
vendored
@ -273,22 +273,23 @@ jobs:
|
|||||||
|
|
||||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Restore cache
|
- name: Restore cache
|
||||||
id: restore-cache
|
id: restore-cache
|
||||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||||
|
|
||||||
- name: Download
|
- name: Download
|
||||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||||
|
|
||||||
- name: Extract
|
- name: Extract
|
||||||
run: |
|
run: |
|
||||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
@ -304,22 +305,23 @@ jobs:
|
|||||||
|
|
||||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||||
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Restore cache
|
- name: Restore cache
|
||||||
id: restore-cache
|
id: restore-cache
|
||||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||||
|
|
||||||
- name: Download
|
- name: Download
|
||||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||||
|
|
||||||
- name: Extract
|
- name: Extract
|
||||||
run: |
|
run: |
|
||||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
@ -339,9 +341,12 @@ jobs:
|
|||||||
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
|
||||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||||
docker-compose up --build generator
|
|
||||||
|
|
||||||
link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
docker compose up --build --detach
|
||||||
|
docker compose logs --follow &
|
||||||
|
docker compose wait generator
|
||||||
|
|
||||||
|
link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
||||||
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
||||||
|
|
||||||
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
||||||
@ -351,7 +356,7 @@ jobs:
|
|||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||||
docker-compose down -v
|
docker compose down --volumes
|
||||||
|
|
||||||
output-cli:
|
output-cli:
|
||||||
name: Output info
|
name: Output info
|
||||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"ms-dotnettools.csharp"
|
"editorconfig.editorconfig",
|
||||||
|
"ms-dotnettools.csdevkit"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ Please make sure you have the following prerequisites:
|
|||||||
|
|
||||||
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
|
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||||
|
|
||||||
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
|
||||||
|
|
||||||
### Downloading the source code
|
### Downloading the source code
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1007.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -279,10 +279,12 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
||||||
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
||||||
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
// Also, spaces don't count. Because reasons, clearly.
|
||||||
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
|
// That all seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||||
if (str.Length < 2)
|
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end. After making sure to trim whitespace.
|
||||||
return str.PadRight(2, '\u200B');
|
string trimmed = str.Trim();
|
||||||
|
if (trimmed.Length < 2)
|
||||||
|
return trimmed.PadRight(2, '\u200B');
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
||||||
return str;
|
return str;
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
||||||
{
|
{
|
||||||
|
AddAssert($"grid type is {typeof(T).Name}", () => this.ChildrenOfType<T>().Any());
|
||||||
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
||||||
AddStep("move cursor to spacing + (1, 1)", () =>
|
AddStep("move cursor to spacing + (1, 1)", () =>
|
||||||
{
|
{
|
||||||
@ -161,7 +163,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
return grid switch
|
return grid switch
|
||||||
{
|
{
|
||||||
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
||||||
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
|
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(
|
||||||
|
new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
|
||||||
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
|
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
|
||||||
_ => Vector2.Zero
|
_ => Vector2.Zero
|
||||||
};
|
};
|
||||||
@ -170,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGridSizeToggling()
|
public void TestGridSizeToggling()
|
||||||
{
|
{
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
gridSizeIs(4);
|
gridSizeIs(4);
|
||||||
|
|
||||||
@ -189,5 +192,97 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
private void gridSizeIs(int size)
|
private void gridSizeIs(int size)
|
||||||
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
||||||
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridTypeToggling()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
|
nextGridTypeIs<TriangularPositionSnapGrid>();
|
||||||
|
nextGridTypeIs<CircularPositionSnapGrid>();
|
||||||
|
nextGridTypeIs<RectangularPositionSnapGrid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextGridTypeIs<T>() where T : PositionSnapGrid
|
||||||
|
{
|
||||||
|
AddStep("toggle to next grid type", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
|
InputManager.Key(Key.G);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
|
gridActive<T>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridPlacementTool()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
|
||||||
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
|
AddStep("move cursor to slider head + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).Position + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddStep("move cursor to slider tail + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
AddAssert("grid position at slider head", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).Position, composer.StartPosition.Value);
|
||||||
|
});
|
||||||
|
AddAssert("grid spacing is distance to slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||||
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
|
||||||
|
});
|
||||||
|
AddAssert("grid rotation points to slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
|
AddStep("move cursor to slider tail + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("double click", () =>
|
||||||
|
{
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("move cursor to (0, 0)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(Vector2.Zero));
|
||||||
|
});
|
||||||
|
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
AddAssert("grid position at slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).EndPosition, composer.StartPosition.Value);
|
||||||
|
});
|
||||||
|
AddAssert("grid spacing and rotation unchanged", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||||
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
|
||||||
|
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
private bool isSplittable(PathControlPointPiece<T> p) =>
|
private bool isSplittable(PathControlPointPiece<T> p) =>
|
||||||
// A hit object can only be split on control points which connect two different path segments.
|
// A hit object can only be split on control points which connect two different path segments.
|
||||||
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
|
p.ControlPoint.Type.HasValue && p.ControlPoint != controlPoints.FirstOrDefault() && p.ControlPoint != controlPoints.LastOrDefault();
|
||||||
|
|
||||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
@ -213,6 +213,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||||
|
|
||||||
|
gridTypeButtons.Items[(int)v.NewValue].Select();
|
||||||
|
|
||||||
switch (v.NewValue)
|
switch (v.NewValue)
|
||||||
{
|
{
|
||||||
case PositionSnapGridType.Square:
|
case PositionSnapGridType.Square:
|
||||||
@ -241,17 +243,16 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f;
|
return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void nextGridSize()
|
|
||||||
{
|
|
||||||
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.EditorCycleGridDisplayMode:
|
case GlobalAction.EditorCycleGridSpacing:
|
||||||
nextGridSize();
|
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case GlobalAction.EditorCycleGridType:
|
||||||
|
GridType.Value = (PositionSnapGridType)(((int)GridType.Value + 1) % Enum.GetValues<PositionSnapGridType>().Length);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
points = originalConvexHull!;
|
points = originalConvexHull!;
|
||||||
|
|
||||||
foreach (var point in points)
|
foreach (var point in points)
|
||||||
{
|
scale = clampToBounds(scale, point, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||||
scale = clampToBound(scale, point, Vector2.Zero);
|
|
||||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
return scale;
|
||||||
|
|
||||||
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
// Clamps the scale vector s such that the point p scaled by s is within the rectangle defined by lowerBounds and upperBounds
|
||||||
|
Vector2 clampToBounds(Vector2 s, Vector2 p, Vector2 lowerBounds, Vector2 upperBounds)
|
||||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
|
||||||
{
|
{
|
||||||
p -= actualOrigin;
|
p -= actualOrigin;
|
||||||
bound -= actualOrigin;
|
lowerBounds -= actualOrigin;
|
||||||
|
upperBounds -= actualOrigin;
|
||||||
|
// a.X is the rotated X component of p with respect to the X bounds
|
||||||
|
// a.Y is the rotated X component of p with respect to the Y bounds
|
||||||
|
// b.X is the rotated Y component of p with respect to the X bounds
|
||||||
|
// b.Y is the rotated Y component of p with respect to the Y bounds
|
||||||
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||||
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||||
|
|
||||||
|
float sLowerBound, sUpperBound;
|
||||||
|
|
||||||
switch (adjustAxis)
|
switch (adjustAxis)
|
||||||
{
|
{
|
||||||
case Axes.X:
|
case Axes.X:
|
||||||
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
|
||||||
|
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Axes.Y:
|
case Axes.Y:
|
||||||
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
|
||||||
|
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Axes.Both:
|
case Axes.Both:
|
||||||
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
// Here we compute the bounds for the magnitude multiplier of the scale vector
|
||||||
|
// Therefore the ratio s.X / s.Y will be maintained
|
||||||
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
|
||||||
|
s.X = s.X < 0
|
||||||
|
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||||
|
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||||
|
s.Y = s.Y < 0
|
||||||
|
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||||
|
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Computes the bounds for the magnitude of the scaled point p with respect to the bounds lowerBounds and upperBounds
|
||||||
|
(float, float) computeBounds(Vector2 lowerBounds, Vector2 upperBounds, Vector2 p)
|
||||||
|
{
|
||||||
|
var sLowerBounds = Vector2.Divide(lowerBounds, p);
|
||||||
|
var sUpperBounds = Vector2.Divide(upperBounds, p);
|
||||||
|
|
||||||
|
// If the point is negative, then the bounds are flipped
|
||||||
|
if (p.X < 0)
|
||||||
|
(sLowerBounds.X, sUpperBounds.X) = (sUpperBounds.X, sLowerBounds.X);
|
||||||
|
if (p.Y < 0)
|
||||||
|
(sLowerBounds.Y, sUpperBounds.Y) = (sUpperBounds.Y, sLowerBounds.Y);
|
||||||
|
|
||||||
|
// If the point is at zero, then any scale will have no effect on the point so the bounds are infinite
|
||||||
|
// The float division would already give us infinity for the bounds, but the sign is not consistent so we have to manually set it
|
||||||
|
if (Precision.AlmostEquals(p.X, 0))
|
||||||
|
(sLowerBounds.X, sUpperBounds.X) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||||
|
if (Precision.AlmostEquals(p.Y, 0))
|
||||||
|
(sLowerBounds.Y, sUpperBounds.Y) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||||
|
|
||||||
|
return (MathF.Max(sLowerBounds.X, sLowerBounds.Y), MathF.Min(sUpperBounds.X, sUpperBounds.Y));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
|
@ -136,8 +136,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
});
|
});
|
||||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
xCheckBox.Current.BindValueChanged(_ =>
|
||||||
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
{
|
||||||
|
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
|
||||||
|
{
|
||||||
|
yCheckBox.Current.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAxes();
|
||||||
|
});
|
||||||
|
yCheckBox.Current.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
|
||||||
|
{
|
||||||
|
xCheckBox.Current.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAxes();
|
||||||
|
});
|
||||||
|
|
||||||
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
@ -152,6 +170,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateAxes()
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { XAxis = xCheckBox.Current.Value, YAxis = yCheckBox.Current.Value };
|
||||||
|
updateMinMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateAxisCheckBoxesEnabled()
|
private void updateAxisCheckBoxesEnabled()
|
||||||
{
|
{
|
||||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||||
@ -175,12 +199,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
axisBindable.Disabled = !available;
|
axisBindable.Disabled = !available;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMaxScale()
|
private void updateMinMaxScale()
|
||||||
{
|
{
|
||||||
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const float min_scale = 0.5f;
|
||||||
const float max_scale = 10;
|
const float max_scale = 10;
|
||||||
|
|
||||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
if (!scaleInfo.Value.XAxis)
|
if (!scaleInfo.Value.XAxis)
|
||||||
@ -189,12 +215,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
scale.Y = max_scale;
|
scale.Y = max_scale;
|
||||||
|
|
||||||
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||||
|
|
||||||
|
scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(min_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
|
if (!scaleInfo.Value.XAxis)
|
||||||
|
scale.X = min_scale;
|
||||||
|
if (!scaleInfo.Value.YAxis)
|
||||||
|
scale.Y = min_scale;
|
||||||
|
|
||||||
|
scaleInputBindable.MinValue = MathF.Min(1, MathF.Max(scale.X, scale.Y));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOrigin(ScaleOrigin origin)
|
private void setOrigin(ScaleOrigin origin)
|
||||||
{
|
{
|
||||||
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||||
updateMaxScale();
|
updateMinMaxScale();
|
||||||
updateAxisCheckBoxesEnabled();
|
updateAxisCheckBoxesEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,21 +254,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
private Axes getAdjustAxis(PreciseScaleInfo scale)
|
||||||
|
{
|
||||||
|
var result = Axes.None;
|
||||||
|
|
||||||
|
if (scale.XAxis)
|
||||||
|
result |= Axes.X;
|
||||||
|
|
||||||
|
if (scale.YAxis)
|
||||||
|
result |= Axes.Y;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||||
|
|
||||||
private void setAxis(bool x, bool y)
|
|
||||||
{
|
|
||||||
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
|
||||||
updateMaxScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
base.PopIn();
|
base.PopIn();
|
||||||
scaleHandler.Begin();
|
scaleHandler.Begin();
|
||||||
updateMaxScale();
|
updateMinMaxScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
|
@ -120,6 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -150,6 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
@ -117,10 +116,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (osuObject is not Slider slider)
|
if (osuObject is not Slider slider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
|
|
||||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
modifySlider(slider, reflectControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -134,10 +132,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (osuObject is not Slider slider)
|
if (osuObject is not Slider slider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
|
|
||||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
modifySlider(slider, reflectControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -146,10 +143,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// <param name="slider">The slider to be flipped.</param>
|
/// <param name="slider">The slider to be flipped.</param>
|
||||||
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
||||||
{
|
{
|
||||||
void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
|
|
||||||
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, flipNestedObject, flipControlPoint);
|
modifySlider(slider, flipControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -159,18 +155,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
||||||
public static void RotateSlider(Slider slider, float rotation)
|
public static void RotateSlider(Slider slider, float rotation)
|
||||||
{
|
{
|
||||||
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
|
||||||
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
||||||
|
|
||||||
modifySlider(slider, rotateNestedObject, rotateControlPoint);
|
modifySlider(slider, rotateControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void modifySlider(Slider slider, Action<OsuHitObject> modifyNestedObject, Action<PathControlPoint> modifyControlPoint)
|
private static void modifySlider(Slider slider, Action<PathControlPoint> modifyControlPoint)
|
||||||
{
|
{
|
||||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
|
||||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(modifyNestedObject);
|
|
||||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(modifyNestedObject);
|
|
||||||
|
|
||||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||||
foreach (var point in controlPoints)
|
foreach (var point in controlPoints)
|
||||||
modifyControlPoint(point);
|
modifyControlPoint(point);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -716,7 +716,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
foreach (var entry in zip.Entries.ToArray())
|
foreach (var entry in zip.Entries.ToArray())
|
||||||
{
|
{
|
||||||
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
|
if (entry.Key!.EndsWith(".osu", StringComparison.InvariantCulture))
|
||||||
zip.RemoveEntry(entry);
|
zip.RemoveEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,6 +627,87 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) =>
|
||||||
|
new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
private static readonly object[] ranked_date_valid_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
|
||||||
|
new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
|
||||||
|
new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
|
||||||
|
new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
|
||||||
|
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(ranked_date_valid_test_cases))]
|
||||||
|
public void TestValidRankedDateQueries(string query, DateTimeOffset expected, Func<FilterCriteria, DateTimeOffset?> f)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(true, filterCriteria.DateRanked.HasFilter);
|
||||||
|
Assert.AreEqual(expected, f(filterCriteria));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[] ranked_date_invalid_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "ranked<0" },
|
||||||
|
new object[] { "ranked=99999" },
|
||||||
|
new object[] { "ranked>=2012-03-05-04" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(ranked_date_invalid_test_cases))]
|
||||||
|
public void TestInvalidRankedDateQueries(string query)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(false, filterCriteria.DateRanked.HasFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[] submitted_date_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "submitted<2012", true },
|
||||||
|
new object[] { "submitted<2012.03", true },
|
||||||
|
new object[] { "submitted<2012/03/05", true },
|
||||||
|
new object[] { "submitted<2012-3-5", true },
|
||||||
|
|
||||||
|
new object[] { "submitted<0", false },
|
||||||
|
new object[] { "submitted=99999", false },
|
||||||
|
new object[] { "submitted>=2012-03-05-04", false },
|
||||||
|
new object[] { "submitted>=2012/03.05-04", false },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(submitted_date_test_cases))]
|
||||||
|
public void TestInvalidRankedDateQueries(string query, bool expected)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(expected, filterCriteria.DateSubmitted.HasFilter);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly object[] played_query_tests =
|
private static readonly object[] played_query_tests =
|
||||||
{
|
{
|
||||||
new object[] { "0", DateTimeOffset.MinValue, true },
|
new object[] { "0", DateTimeOffset.MinValue, true },
|
||||||
|
@ -6,10 +6,12 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Metadata;
|
using osu.Game.Online.Metadata;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual.Metadata;
|
using osu.Game.Tests.Visual.Metadata;
|
||||||
@ -81,6 +83,38 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
|||||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||||
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
||||||
|
AddAssert("notification posted", () => notificationOverlay.AllNotifications.OfType<SimpleNotification>().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestConclusionNotificationDoesNotFireOnDisconnect()
|
||||||
|
{
|
||||||
|
var room = new Room
|
||||||
|
{
|
||||||
|
RoomID = { Value = 1234 },
|
||||||
|
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||||
|
{
|
||||||
|
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||||
|
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
|
||||||
|
Category = { Value = RoomCategory.DailyChallenge }
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||||
|
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||||
|
|
||||||
|
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||||
|
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||||
|
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||||
|
AddStep("disconnect from metadata server", () => metadataClient.Disconnect());
|
||||||
|
AddUntilStep("wait for disconnection", () => metadataClient.DailyChallengeInfo.Value, () => Is.Null);
|
||||||
|
AddAssert("no notification posted", () => notificationOverlay.AllNotifications, () => Is.Empty);
|
||||||
|
AddStep("reconnect to metadata server", () => metadataClient.Reconnect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,5 +79,114 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
|
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
|
||||||
AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShuffleBackwards()
|
||||||
|
{
|
||||||
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||||
|
|
||||||
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||||
|
{
|
||||||
|
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||||
|
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||||
|
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||||
|
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||||
|
beatmap.Length = 60_000;
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("bind to track change", () =>
|
||||||
|
{
|
||||||
|
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||||
|
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000));
|
||||||
|
AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddAssert("no track change", () => trackChangeQueue.Count == 0);
|
||||||
|
AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 2);
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () =>
|
||||||
|
trackChangeQueue.Count == 3 && !trackChangeQueue.ElementAt(1).working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShuffleForwards()
|
||||||
|
{
|
||||||
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||||
|
|
||||||
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||||
|
{
|
||||||
|
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||||
|
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||||
|
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||||
|
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||||
|
beatmap.Length = 60_000;
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("bind to track change", () =>
|
||||||
|
{
|
||||||
|
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||||
|
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 2);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () =>
|
||||||
|
trackChangeQueue.Count == 3 && !trackChangeQueue.ElementAt(1).working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShuffleBackAndForth()
|
||||||
|
{
|
||||||
|
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||||
|
|
||||||
|
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||||
|
|
||||||
|
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||||
|
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||||
|
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||||
|
{
|
||||||
|
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||||
|
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||||
|
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||||
|
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||||
|
beatmap.Length = 60_000;
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("bind to track change", () =>
|
||||||
|
{
|
||||||
|
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||||
|
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||||
|
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||||
|
|
||||||
|
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||||
|
AddUntilStep("track changed", () =>
|
||||||
|
trackChangeQueue.Count == 2 && !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -90,6 +92,48 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
|
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitResultsWithSameNameAreGrouped()
|
||||||
|
{
|
||||||
|
AddStep("Load scores without user best", () =>
|
||||||
|
{
|
||||||
|
var allScores = createScores();
|
||||||
|
allScores.UserScore = null;
|
||||||
|
scoresContainer.Scores = allScores;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
|
AddAssert("only one column for slider end", () =>
|
||||||
|
{
|
||||||
|
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
|
||||||
|
return scoreTable.Columns.Count(c => c.Header.Equals("slider end")) == 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all rows show non-zero slider ends", () =>
|
||||||
|
{
|
||||||
|
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
|
||||||
|
int sliderEndColumnIndex = Array.FindIndex(scoreTable.Columns, c => c != null && c.Header.Equals("slider end"));
|
||||||
|
bool sliderEndFilledInEachRow = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < scoreTable.Content?.GetLength(0); i++)
|
||||||
|
{
|
||||||
|
switch (scoreTable.Content[i, sliderEndColumnIndex])
|
||||||
|
{
|
||||||
|
case OsuSpriteText text:
|
||||||
|
if (text.Text.Equals(0.0d.ToLocalisableString(@"N0")))
|
||||||
|
sliderEndFilledInEachRow = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sliderEndFilledInEachRow = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sliderEndFilledInEachRow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestUserBest()
|
public void TestUserBest()
|
||||||
{
|
{
|
||||||
@ -287,13 +331,17 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
const int initial_great_count = 2000;
|
const int initial_great_count = 2000;
|
||||||
const int initial_tick_count = 100;
|
const int initial_tick_count = 100;
|
||||||
|
const int initial_slider_end_count = 500;
|
||||||
|
|
||||||
int greatCount = initial_great_count;
|
int greatCount = initial_great_count;
|
||||||
int tickCount = initial_tick_count;
|
int tickCount = initial_tick_count;
|
||||||
|
int sliderEndCount = initial_slider_end_count;
|
||||||
|
|
||||||
foreach (var s in scores.Scores)
|
foreach (var (score, index) in scores.Scores.Select((s, i) => (s, i)))
|
||||||
{
|
{
|
||||||
s.Statistics = new Dictionary<HitResult, int>
|
HitResult sliderEndResult = index % 2 == 0 ? HitResult.SliderTailHit : HitResult.SmallTickHit;
|
||||||
|
|
||||||
|
score.Statistics = new Dictionary<HitResult, int>
|
||||||
{
|
{
|
||||||
{ HitResult.Great, greatCount },
|
{ HitResult.Great, greatCount },
|
||||||
{ HitResult.LargeTickHit, tickCount },
|
{ HitResult.LargeTickHit, tickCount },
|
||||||
@ -301,10 +349,19 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{ HitResult.Meh, RNG.Next(100) },
|
{ HitResult.Meh, RNG.Next(100) },
|
||||||
{ HitResult.Miss, initial_great_count - greatCount },
|
{ HitResult.Miss, initial_great_count - greatCount },
|
||||||
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
|
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
|
||||||
|
{ sliderEndResult, sliderEndCount },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some hit results, including SliderTailHit and SmallTickHit, are only displayed
|
||||||
|
// when the maximum number is known
|
||||||
|
score.MaximumStatistics = new Dictionary<HitResult, int>
|
||||||
|
{
|
||||||
|
{ sliderEndResult, initial_slider_end_count },
|
||||||
};
|
};
|
||||||
|
|
||||||
greatCount -= 100;
|
greatCount -= 100;
|
||||||
tickCount -= RNG.Next(1, 5);
|
tickCount -= RNG.Next(1, 5);
|
||||||
|
sliderEndCount -= 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
return scores;
|
return scores;
|
||||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("select EZ mod", () =>
|
AddStep("select EZ mod", () =>
|
||||||
{
|
{
|
||||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||||
SelectedMods.Value = new[] { ruleset.CreateMod<ModEasy>() };
|
advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModEasy>() };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
|
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
|
||||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("select HR mod", () =>
|
AddStep("select HR mod", () =>
|
||||||
{
|
{
|
||||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||||
SelectedMods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
|
advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
|
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
|
||||||
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||||
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
||||||
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
||||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||||
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
||||||
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
|
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
|
||||||
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
|
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
|
||||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Skinning.Components;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public partial class TestSceneBeatmapAttributeText : OsuTestScene
|
||||||
|
{
|
||||||
|
private readonly BeatmapAttributeText text;
|
||||||
|
|
||||||
|
public TestSceneBeatmapAttributeText()
|
||||||
|
{
|
||||||
|
Child = text = new BeatmapAttributeText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
BPM = 100,
|
||||||
|
DifficultyName = "_Difficulty",
|
||||||
|
Status = BeatmapOnlineStatus.Loved,
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = "_Title",
|
||||||
|
TitleUnicode = "_Title",
|
||||||
|
Artist = "_Artist",
|
||||||
|
ArtistUnicode = "_Artist",
|
||||||
|
Author = new RealmUser { Username = "_Creator" },
|
||||||
|
Source = "_Source",
|
||||||
|
},
|
||||||
|
Difficulty =
|
||||||
|
{
|
||||||
|
CircleSize = 1,
|
||||||
|
DrainRate = 2,
|
||||||
|
OverallDifficulty = 3,
|
||||||
|
ApproachRate = 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[TestCase(BeatmapAttribute.CircleSize, "Circle Size: 1.00")]
|
||||||
|
[TestCase(BeatmapAttribute.HPDrain, "HP Drain: 2.00")]
|
||||||
|
[TestCase(BeatmapAttribute.Accuracy, "Accuracy: 3.00")]
|
||||||
|
[TestCase(BeatmapAttribute.ApproachRate, "Approach Rate: 4.00")]
|
||||||
|
[TestCase(BeatmapAttribute.Title, "Title: _Title")]
|
||||||
|
[TestCase(BeatmapAttribute.Artist, "Artist: _Artist")]
|
||||||
|
[TestCase(BeatmapAttribute.Creator, "Creator: _Creator")]
|
||||||
|
[TestCase(BeatmapAttribute.DifficultyName, "Difficulty: _Difficulty")]
|
||||||
|
[TestCase(BeatmapAttribute.Source, "Source: _Source")]
|
||||||
|
[TestCase(BeatmapAttribute.RankedStatus, "Beatmap Status: Loved")]
|
||||||
|
public void TestAttributeDisplay(BeatmapAttribute attribute, string expectedText)
|
||||||
|
{
|
||||||
|
AddStep($"set attribute: {attribute}", () => text.Attribute.Value = attribute);
|
||||||
|
AddAssert("check correct text", getText, () => Is.EqualTo(expectedText));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeBeatmap()
|
||||||
|
{
|
||||||
|
AddStep("set title attribute", () => text.Attribute.Value = BeatmapAttribute.Title);
|
||||||
|
AddAssert("check initial title", getText, () => Is.EqualTo("Title: _Title"));
|
||||||
|
|
||||||
|
AddStep("change to beatmap with another title", () => Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = "Another"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddAssert("check new title", getText, () => Is.EqualTo("Title: Another"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
|
||||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
private BeatmapInfo()
|
protected BeatmapInfo()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r =>
|
||||||
|
r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||||
@ -313,6 +314,23 @@ namespace osu.Game.Beatmaps
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetAllOffsets()
|
||||||
|
{
|
||||||
|
const string reset_complete_message = "All offsets have been reset!";
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
var items = r.All<BeatmapInfo>();
|
||||||
|
|
||||||
|
foreach (var beatmap in items)
|
||||||
|
{
|
||||||
|
if (beatmap.UserSettings.Offset != 0)
|
||||||
|
beatmap.UserSettings.Offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostNotification?.Invoke(new ProgressCompletionNotification { Text = reset_complete_message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
||||||
{
|
{
|
||||||
Realm.Run(r =>
|
Realm.Run(r =>
|
||||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
{
|
{
|
||||||
if (detachedBeatmapSets.Count > 0 && sender.Count == 0)
|
if (sender is RealmResetEmptySet<BeatmapSetInfo>)
|
||||||
{
|
{
|
||||||
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
|
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
|
||||||
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
|
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
|
||||||
|
@ -93,8 +93,9 @@ namespace osu.Game.Database
|
|||||||
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
||||||
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
|
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
|
||||||
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
||||||
|
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 42;
|
private const int schema_version = 43;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
@ -568,7 +569,7 @@ namespace osu.Game.Database
|
|||||||
lock (notificationsResetMap)
|
lock (notificationsResetMap)
|
||||||
{
|
{
|
||||||
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
||||||
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null));
|
notificationsResetMap.Add(action, () => callback(new RealmResetEmptySet<T>(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
return RegisterCustomSubscription(action);
|
return RegisterCustomSubscription(action);
|
||||||
@ -1192,6 +1193,21 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 43:
|
||||||
|
{
|
||||||
|
// Clear default bindings for "Toggle FPS Display",
|
||||||
|
// as it conflicts with "Convert to Stream" in the editor.
|
||||||
|
// Only apply change if set to the conflicting bind
|
||||||
|
// i.e. has been manually rebound by the user.
|
||||||
|
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
||||||
|
|
||||||
|
var toggleFpsBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleFPSDisplay);
|
||||||
|
if (toggleFpsBind != null && toggleFpsBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.F }))
|
||||||
|
migration.NewRealm.Remove(toggleFpsBind);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
@ -12,7 +12,13 @@ using Realms.Schema;
|
|||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
public class EmptyRealmSet<T> : IRealmCollection<T>
|
/// <summary>
|
||||||
|
/// This can arrive in <see cref="RealmAccess.RegisterForNotifications{T}"/> callbacks to imply that realm access has been reset.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Usually implies that the original database may return soon and the callback can usually be silently ignored.
|
||||||
|
///</remarks>
|
||||||
|
public class RealmResetEmptySet<T> : IRealmCollection<T>
|
||||||
{
|
{
|
||||||
private IList<T> emptySet => Array.Empty<T>();
|
private IList<T> emptySet => Array.Empty<T>();
|
||||||
|
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
@ -20,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
/// <param name="nextStateFunction">A function to inform what the next state should be when this item is clicked.</param>
|
/// <param name="nextStateFunction">A function to inform what the next state should be when this item is clicked.</param>
|
||||||
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
||||||
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
||||||
protected TernaryStateMenuItem(string text, Func<TernaryState, TernaryState> nextStateFunction, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
protected TernaryStateMenuItem(LocalisableString text, Func<TernaryState, TernaryState> nextStateFunction, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
||||||
: base(text, nextStateFunction, type, action)
|
: base(text, nextStateFunction, type, action)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
@ -18,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
/// <param name="text">The text to display.</param>
|
/// <param name="text">The text to display.</param>
|
||||||
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
||||||
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
||||||
public TernaryStateRadioMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
public TernaryStateRadioMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
||||||
: base(text, getNextState, type, action)
|
: base(text, getNextState, type, action)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
|
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
|
||||||
|
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
|
new KeyBinding(InputKey.None, GlobalAction.ToggleFPSDisplay),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
|
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
|
||||||
|
|
||||||
@ -134,7 +134,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
|
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
|
||||||
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
||||||
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
||||||
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
|
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridSpacing),
|
||||||
|
new KeyBinding(new[] { InputKey.Shift, InputKey.G }, GlobalAction.EditorCycleGridType),
|
||||||
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
|
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
|
||||||
new KeyBinding(new[] { InputKey.T }, GlobalAction.EditorTapForBPM),
|
new KeyBinding(new[] { InputKey.T }, GlobalAction.EditorTapForBPM),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
|
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
|
||||||
@ -368,8 +369,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))]
|
||||||
ToggleChatFocus,
|
ToggleChatFocus,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridSpacing))]
|
||||||
EditorCycleGridDisplayMode,
|
EditorCycleGridSpacing,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))]
|
||||||
EditorTestGameplay,
|
EditorTestGameplay,
|
||||||
@ -472,6 +473,9 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))]
|
||||||
EditorSeekToNextSamplePoint,
|
EditorSeekToNextSamplePoint,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridType))]
|
||||||
|
EditorCycleGridType,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GlobalActionCategory
|
public enum GlobalActionCategory
|
||||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"Are you sure you want to delete all beatmaps videos? This cannot be undone!");
|
public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"Are you sure you want to delete all beatmaps videos? This cannot be undone!");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Are you sure you want to reset all local beatmap offsets? This cannot be undone!"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Offsets => new TranslatableString(getKey(@"offsets"), @"Are you sure you want to reset all local beatmap offsets? This cannot be undone!");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Are you sure you want to delete all skins? This cannot be undone!"
|
/// "Are you sure you want to delete all skins? This cannot be undone!"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -190,9 +190,14 @@ namespace osu.Game.Localisation
|
|||||||
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
|
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Cycle grid display mode"
|
/// "Cycle grid spacing"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString EditorCycleGridDisplayMode => new TranslatableString(getKey(@"editor_cycle_grid_display_mode"), @"Cycle grid display mode");
|
public static LocalisableString EditorCycleGridSpacing => new TranslatableString(getKey(@"editor_cycle_grid_spacing"), @"Cycle grid spacing");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Cycle grid type"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorCycleGridType => new TranslatableString(getKey(@"editor_cycle_grid_type"), @"Cycle grid type");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Test gameplay"
|
/// "Test gameplay"
|
||||||
|
@ -59,6 +59,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
|
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Reset ALL beatmap offsets"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ResetAllOffsets => new TranslatableString(getKey(@"reset_all_offsets"), @"Reset ALL beatmap offsets");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Delete ALL scores"
|
/// "Delete ALL scores"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -79,6 +79,11 @@ namespace osu.Game.Localisation.SkinComponents
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text.");
|
public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text.");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Use relative size"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString UseRelativeSize => new TranslatableString(getKey(@"use_relative_size"), @"Use relative size");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,51 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString RevertToDefaultDescription => new TranslatableString(getKey(@"revert_to_default_description"), @"All layout elements for layers in the current screen will be reset to defaults.");
|
public static LocalisableString RevertToDefaultDescription => new TranslatableString(getKey(@"revert_to_default_description"), @"All layout elements for layers in the current screen will be reset to defaults.");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Closest"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Closest => new TranslatableString(getKey(@"closest"), @"Closest");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Anchor"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Anchor => new TranslatableString(getKey(@"anchor"), @"Anchor");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Origin"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Origin => new TranslatableString(getKey(@"origin"), @"Origin");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Reset position"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ResetPosition => new TranslatableString(getKey(@"reset_position"), @"Reset position");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Reset rotation"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ResetRotation => new TranslatableString(getKey(@"reset_rotation"), @"Reset rotation");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Reset scale"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ResetScale => new TranslatableString(getKey(@"reset_scale"), @"Reset scale");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Bring to front"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString BringToFront => new TranslatableString(getKey(@"bring_to_front"), @"Bring to front");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Send to back"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString SendToBack => new TranslatableString(getKey(@"send_to_back"), @"Send to back");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Current working layer"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString CurrentWorkingLayer => new TranslatableString(getKey(@"current_working_layer"), @"Current working layer");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -10,13 +9,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class InvalidPasswordException : HubException
|
public class InvalidPasswordException : HubException
|
||||||
{
|
{
|
||||||
public InvalidPasswordException()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected InvalidPasswordException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
: base($"Cannot change from {oldState} to {newState}")
|
: base($"Cannot change from {oldState} to {newState}")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InvalidStateChangeException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InvalidStateException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
: base("User is attempting to perform a host level operation while not the host")
|
: base("User is attempting to perform a host level operation while not the host")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected NotHostException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
: base("This user has not yet joined a multiplayer room.")
|
: base("This user has not yet joined a multiplayer room.")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected NotJoinedRoomException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
: base(MESSAGE)
|
: base(MESSAGE)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UserBlockedException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
: base(MESSAGE)
|
: base(MESSAGE)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UserBlocksPMsException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -541,7 +541,10 @@ namespace osu.Game
|
|||||||
realmBlocker = realm.BlockAllOperations("migration");
|
realmBlocker = realm.BlockAllOperations("migration");
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
catch { }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log($"Attempting to block all operations failed: {ex}", LoggingTarget.Database);
|
||||||
|
}
|
||||||
|
|
||||||
readyToRun.Set();
|
readyToRun.Set();
|
||||||
}, false);
|
}, false);
|
||||||
|
@ -9,7 +9,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.EnumExtensions;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -58,9 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The statistics that appear in the table, in order of appearance.
|
/// The names of the statistics that appear in the table. If multiple HitResults have the same
|
||||||
|
/// DisplayName (for example, "slider end" is the name for both <see cref="HitResult.SliderTailHit"/> and <see cref="HitResult.SmallTickHit"/>
|
||||||
|
/// in osu!) the name will only be listed once.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>();
|
private readonly List<LocalisableString> statisticResultNames = new List<LocalisableString>();
|
||||||
|
|
||||||
private bool showPerformancePoints;
|
private bool showPerformancePoints;
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
showPerformancePoints = showPerformanceColumn;
|
showPerformancePoints = showPerformanceColumn;
|
||||||
statisticResultTypes.Clear();
|
statisticResultNames.Clear();
|
||||||
|
|
||||||
for (int i = 0; i < scores.Count; i++)
|
for (int i = 0; i < scores.Count; i++)
|
||||||
backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height));
|
backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height));
|
||||||
@ -105,20 +106,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
|
|
||||||
var ruleset = scores.First().Ruleset.CreateInstance();
|
var ruleset = scores.First().Ruleset.CreateInstance();
|
||||||
|
|
||||||
foreach (var result in EnumExtensions.GetValuesInOrder<HitResult>())
|
foreach (var resultGroup in ruleset.GetHitResults().GroupBy(r => r.displayName))
|
||||||
{
|
{
|
||||||
if (!allScoreStatistics.Contains(result))
|
if (!resultGroup.Any(r => allScoreStatistics.Contains(r.result)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// for the time being ignore bonus result types.
|
// for the time being ignore bonus result types.
|
||||||
// this is not being sent from the API and will be empty in all cases.
|
// this is not being sent from the API and will be empty in all cases.
|
||||||
if (result.IsBonus())
|
if (resultGroup.All(r => r.result.IsBonus()))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var displayName = ruleset.GetDisplayNameForHitResult(result);
|
columns.Add(new TableColumn(resultGroup.Key, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
|
||||||
|
statisticResultNames.Add(resultGroup.Key);
|
||||||
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
|
|
||||||
statisticResultTypes.Add((result, displayName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPerformancePoints)
|
if (showPerformancePoints)
|
||||||
@ -167,14 +166,25 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
#pragma warning restore 618
|
#pragma warning restore 618
|
||||||
};
|
};
|
||||||
|
|
||||||
var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result);
|
var availableStatistics = score.GetStatisticsForDisplay().ToLookup(tuple => tuple.DisplayName);
|
||||||
|
|
||||||
foreach (var result in statisticResultTypes)
|
foreach (var columnName in statisticResultNames)
|
||||||
{
|
{
|
||||||
if (!availableStatistics.TryGetValue(result.result, out var stat))
|
int count = 0;
|
||||||
stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName);
|
int? maxCount = null;
|
||||||
|
|
||||||
content.Add(new StatisticText(stat.Count, stat.MaxCount, @"N0") { Colour = stat.Count == 0 ? Color4.Gray : Color4.White });
|
if (availableStatistics.Contains(columnName))
|
||||||
|
{
|
||||||
|
maxCount = 0;
|
||||||
|
|
||||||
|
foreach (var s in availableStatistics[columnName])
|
||||||
|
{
|
||||||
|
count += s.Count;
|
||||||
|
maxCount += s.MaxCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.Add(new StatisticText(count, maxCount, @"N0") { Colour = count == 0 ? Color4.Gray : Color4.White });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPerformancePoints)
|
if (showPerformancePoints)
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -16,8 +14,6 @@ using osu.Game.Online.API.Requests.Responses;
|
|||||||
using osu.Game.Overlays.BeatmapSet;
|
using osu.Game.Overlays.BeatmapSet;
|
||||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||||
using osu.Game.Overlays.Comments;
|
using osu.Game.Overlays.Comments;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Screens.Select.Details;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -37,14 +33,6 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
private (BeatmapSetLookupType type, int id)? lastLookup;
|
private (BeatmapSetLookupType type, int id)? lastLookup;
|
||||||
|
|
||||||
/// <remarks>
|
|
||||||
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
|
|
||||||
/// to avoid affecting the beatmap details section (i.e. <see cref="AdvancedStats.StatisticRow"/>).
|
|
||||||
/// </remarks>
|
|
||||||
[Cached]
|
|
||||||
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
|
|
||||||
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
|
||||||
|
|
||||||
public BeatmapSetOverlay()
|
public BeatmapSetOverlay()
|
||||||
: base(OverlayColourScheme.Blue)
|
: base(OverlayColourScheme.Blue)
|
||||||
{
|
{
|
||||||
|
@ -72,8 +72,9 @@ namespace osu.Game.Overlays
|
|||||||
private AudioFilter audioDuckFilter = null!;
|
private AudioFilter audioDuckFilter = null!;
|
||||||
|
|
||||||
private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
|
private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
|
||||||
private readonly List<BeatmapSetInfo> previousRandomSets = new List<BeatmapSetInfo>();
|
private readonly List<Live<BeatmapSetInfo>> previousRandomSets = new List<Live<BeatmapSetInfo>>();
|
||||||
private int randomHistoryDirection;
|
private int randomHistoryDirection;
|
||||||
|
private int lastRandomTrackDirection;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(AudioManager audio, OsuConfigManager configManager)
|
private void load(AudioManager audio, OsuConfigManager configManager)
|
||||||
@ -249,19 +250,19 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
queuedDirection = TrackChangeDirection.Prev;
|
queuedDirection = TrackChangeDirection.Prev;
|
||||||
|
|
||||||
BeatmapSetInfo? playableSet;
|
Live<BeatmapSetInfo>? playableSet;
|
||||||
|
|
||||||
if (Shuffle.Value)
|
if (Shuffle.Value)
|
||||||
playableSet = getNextRandom(-1, allowProtectedTracks);
|
playableSet = getNextRandom(-1, allowProtectedTracks);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
|
playableSet = getBeatmapSets().TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks)
|
||||||
?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks);
|
?? getBeatmapSets().LastOrDefault(s => !s.Value.Protected || allowProtectedTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playableSet != null)
|
if (playableSet != null)
|
||||||
{
|
{
|
||||||
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Beatmaps.First()));
|
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Value.Beatmaps.First()));
|
||||||
restartTrack();
|
restartTrack();
|
||||||
return PreviousTrackResult.Previous;
|
return PreviousTrackResult.Previous;
|
||||||
}
|
}
|
||||||
@ -345,19 +346,19 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
queuedDirection = TrackChangeDirection.Next;
|
queuedDirection = TrackChangeDirection.Next;
|
||||||
|
|
||||||
BeatmapSetInfo? playableSet;
|
Live<BeatmapSetInfo>? playableSet;
|
||||||
|
|
||||||
if (Shuffle.Value)
|
if (Shuffle.Value)
|
||||||
playableSet = getNextRandom(1, allowProtectedTracks);
|
playableSet = getNextRandom(1, allowProtectedTracks);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo))
|
playableSet = getBeatmapSets().SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo))
|
||||||
.Where(i => !i.Protected || allowProtectedTracks)
|
.Where(i => !i.Value.Protected || allowProtectedTracks)
|
||||||
.ElementAtOrDefault(1)
|
.ElementAtOrDefault(1)
|
||||||
?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks);
|
?? getBeatmapSets().FirstOrDefault(i => !i.Value.Protected || allowProtectedTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
|
var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault();
|
||||||
|
|
||||||
if (playableBeatmap != null)
|
if (playableBeatmap != null)
|
||||||
{
|
{
|
||||||
@ -369,56 +370,82 @@ namespace osu.Game.Overlays
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks)
|
private Live<BeatmapSetInfo>? getNextRandom(int direction, bool allowProtectedTracks)
|
||||||
{
|
{
|
||||||
BeatmapSetInfo result;
|
try
|
||||||
|
|
||||||
var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray();
|
|
||||||
|
|
||||||
if (possibleSets.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
|
|
||||||
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
|
|
||||||
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
|
|
||||||
// in both cases, it means that we have a history of previous random selections that we can rewind.
|
|
||||||
if (randomHistoryDirection * direction < 0)
|
|
||||||
{
|
{
|
||||||
Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count);
|
Live<BeatmapSetInfo> result;
|
||||||
result = previousRandomSets[^1];
|
|
||||||
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
|
||||||
randomHistoryDirection += direction;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the early-return above didn't cover it, it means that we have no history to fall back on
|
var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList();
|
||||||
// and need to actually choose something random.
|
|
||||||
switch (randomSelectAlgorithm.Value)
|
|
||||||
{
|
|
||||||
case RandomSelectAlgorithm.Random:
|
|
||||||
result = possibleSets[RNG.Next(possibleSets.Length)];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RandomSelectAlgorithm.RandomPermutation:
|
if (possibleSets.Count == 0)
|
||||||
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray();
|
return null;
|
||||||
|
|
||||||
if (notYetPlayedSets.Length == 0)
|
// if there is only one possible set left, play it, even if it is the same as the current track.
|
||||||
|
// looping is preferable over playing nothing.
|
||||||
|
if (possibleSets.Count == 1)
|
||||||
|
return possibleSets.Single();
|
||||||
|
|
||||||
|
// now that we actually know there is a choice, do not allow the current track to be played again.
|
||||||
|
possibleSets.RemoveAll(s => s.Value.Equals(current?.BeatmapSetInfo));
|
||||||
|
|
||||||
|
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
|
||||||
|
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
|
||||||
|
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
|
||||||
|
// in both cases, it means that we have a history of previous random selections that we can rewind.
|
||||||
|
if (randomHistoryDirection * direction < 0)
|
||||||
|
{
|
||||||
|
Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count);
|
||||||
|
|
||||||
|
// if the user has been shuffling backwards and now going forwards (or vice versa),
|
||||||
|
// the topmost item from history needs to be discarded because it's the *current* track.
|
||||||
|
if (direction * lastRandomTrackDirection < 0)
|
||||||
{
|
{
|
||||||
notYetPlayedSets = possibleSets;
|
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
||||||
previousRandomSets.Clear();
|
randomHistoryDirection += direction;
|
||||||
randomHistoryDirection = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)];
|
if (previousRandomSets.Count > 0)
|
||||||
break;
|
{
|
||||||
|
result = previousRandomSets[^1];
|
||||||
|
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
// if the early-return above didn't cover it, it means that we have no history to fall back on
|
||||||
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
|
// and need to actually choose something random.
|
||||||
|
switch (randomSelectAlgorithm.Value)
|
||||||
|
{
|
||||||
|
case RandomSelectAlgorithm.Random:
|
||||||
|
result = possibleSets[RNG.Next(possibleSets.Count)];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RandomSelectAlgorithm.RandomPermutation:
|
||||||
|
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToList();
|
||||||
|
|
||||||
|
if (notYetPlayedSets.Count == 0)
|
||||||
|
{
|
||||||
|
notYetPlayedSets = possibleSets;
|
||||||
|
previousRandomSets.Clear();
|
||||||
|
randomHistoryDirection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
|
||||||
|
}
|
||||||
|
|
||||||
|
previousRandomSets.Add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
randomHistoryDirection += direction;
|
||||||
|
lastRandomTrackDirection = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
previousRandomSets.Add(result);
|
|
||||||
randomHistoryDirection += direction;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restartTrack()
|
private void restartTrack()
|
||||||
@ -432,7 +459,9 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
private TrackChangeDirection? queuedDirection;
|
private TrackChangeDirection? queuedDirection;
|
||||||
|
|
||||||
private IQueryable<BeatmapSetInfo> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending);
|
private IEnumerable<Live<BeatmapSetInfo>> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(s => new RealmLive<BeatmapSetInfo>(s, realm));
|
||||||
|
|
||||||
private void changeBeatmap(WorkingBeatmap newWorking)
|
private void changeBeatmap(WorkingBeatmap newWorking)
|
||||||
{
|
{
|
||||||
@ -459,8 +488,8 @@ namespace osu.Game.Overlays
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// figure out the best direction based on order in playlist.
|
// figure out the best direction based on order in playlist.
|
||||||
int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count();
|
int last = getBeatmapSets().TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count();
|
||||||
int next = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count();
|
int next = getBeatmapSets().TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count();
|
||||||
|
|
||||||
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
|
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
|
|
||||||
private SettingsButton deleteBeatmapsButton = null!;
|
private SettingsButton deleteBeatmapsButton = null!;
|
||||||
private SettingsButton deleteBeatmapVideosButton = null!;
|
private SettingsButton deleteBeatmapVideosButton = null!;
|
||||||
|
private SettingsButton resetOffsetsButton = null!;
|
||||||
private SettingsButton restoreButton = null!;
|
private SettingsButton restoreButton = null!;
|
||||||
private SettingsButton undeleteButton = null!;
|
private SettingsButton undeleteButton = null!;
|
||||||
|
|
||||||
@ -47,6 +48,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
}, DeleteConfirmationContentStrings.BeatmapVideos));
|
}, DeleteConfirmationContentStrings.BeatmapVideos));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Add(resetOffsetsButton = new DangerousSettingsButton
|
||||||
|
{
|
||||||
|
Text = MaintenanceSettingsStrings.ResetAllOffsets,
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
|
||||||
|
{
|
||||||
|
resetOffsetsButton.Enabled.Value = false;
|
||||||
|
Task.Run(beatmaps.ResetAllOffsets).ContinueWith(_ => Schedule(() => resetOffsetsButton.Enabled.Value = true));
|
||||||
|
}, DeleteConfirmationContentStrings.Offsets));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
restoreButton = new SettingsButton
|
restoreButton = new SettingsButton
|
||||||
|
@ -361,7 +361,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
componentsSidebar.Children = new[]
|
componentsSidebar.Children = new[]
|
||||||
{
|
{
|
||||||
new EditorSidebarSection("Current working layer")
|
new EditorSidebarSection(SkinEditorStrings.CurrentWorkingLayer)
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Extensions;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -101,19 +102,19 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
|
||||||
{
|
{
|
||||||
var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors())
|
var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors())
|
||||||
{
|
{
|
||||||
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
|
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new OsuMenuItem("Anchor")
|
yield return new OsuMenuItem(SkinEditorStrings.Anchor)
|
||||||
{
|
{
|
||||||
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
|
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
|
||||||
.Prepend(closestItem)
|
.Prepend(closestItem)
|
||||||
.ToArray()
|
.ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return originMenu = new OsuMenuItem("Origin");
|
yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin);
|
||||||
|
|
||||||
closestItem.State.BindValueChanged(s =>
|
closestItem.State.BindValueChanged(s =>
|
||||||
{
|
{
|
||||||
@ -125,19 +126,19 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
yield return new OsuMenuItemSpacer();
|
yield return new OsuMenuItemSpacer();
|
||||||
|
|
||||||
yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () =>
|
yield return new OsuMenuItem(SkinEditorStrings.ResetPosition, MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
foreach (var blueprint in SelectedBlueprints)
|
foreach (var blueprint in SelectedBlueprints)
|
||||||
((Drawable)blueprint.Item).Position = Vector2.Zero;
|
((Drawable)blueprint.Item).Position = Vector2.Zero;
|
||||||
});
|
});
|
||||||
|
|
||||||
yield return new OsuMenuItem("Reset rotation", MenuItemType.Standard, () =>
|
yield return new OsuMenuItem(SkinEditorStrings.ResetRotation, MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
foreach (var blueprint in SelectedBlueprints)
|
foreach (var blueprint in SelectedBlueprints)
|
||||||
((Drawable)blueprint.Item).Rotation = 0;
|
((Drawable)blueprint.Item).Rotation = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
yield return new OsuMenuItem("Reset scale", MenuItemType.Standard, () =>
|
yield return new OsuMenuItem(SkinEditorStrings.ResetScale, MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
foreach (var blueprint in SelectedBlueprints)
|
foreach (var blueprint in SelectedBlueprints)
|
||||||
{
|
{
|
||||||
@ -153,9 +154,9 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
yield return new OsuMenuItemSpacer();
|
yield return new OsuMenuItemSpacer();
|
||||||
|
|
||||||
yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront());
|
yield return new OsuMenuItem(SkinEditorStrings.BringToFront, MenuItemType.Standard, () => skinEditor.BringSelectionToFront());
|
||||||
|
|
||||||
yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack());
|
yield return new OsuMenuItem(SkinEditorStrings.SendToBack, MenuItemType.Standard, () => skinEditor.SendSelectionToBack());
|
||||||
|
|
||||||
yield return new OsuMenuItemSpacer();
|
yield return new OsuMenuItemSpacer();
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ namespace osu.Game.Scoring
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UsedImplicitly] // Realm
|
[UsedImplicitly] // Realm
|
||||||
private ScoreInfo()
|
protected ScoreInfo()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,13 +150,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
{
|
{
|
||||||
case Key.G:
|
case Key.G:
|
||||||
return CanReverse && reverseButton?.TriggerClick() == true;
|
if (!CanReverse || reverseButton == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
reverseButton.TriggerAction();
|
||||||
|
return true;
|
||||||
|
|
||||||
case Key.Comma:
|
case Key.Comma:
|
||||||
return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true;
|
if (!canRotate.Value || rotateCounterClockwiseButton == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
rotateCounterClockwiseButton.TriggerAction();
|
||||||
|
return true;
|
||||||
|
|
||||||
case Key.Period:
|
case Key.Period:
|
||||||
return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true;
|
if (!canRotate.Value || rotateClockwiseButton == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
rotateClockwiseButton.TriggerAction();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.OnKeyDown(e);
|
return base.OnKeyDown(e);
|
||||||
@ -285,7 +297,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Action = action
|
Action = action
|
||||||
};
|
};
|
||||||
|
|
||||||
button.OperationStarted += freezeButtonPosition;
|
button.Clicked += freezeButtonPosition;
|
||||||
button.HoverLost += unfreezeButtonPosition;
|
button.HoverLost += unfreezeButtonPosition;
|
||||||
|
|
||||||
button.OperationStarted += operationStarted;
|
button.OperationStarted += operationStarted;
|
||||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
public Action? Action;
|
public Action? Action;
|
||||||
|
|
||||||
|
public event Action? Clicked;
|
||||||
|
|
||||||
public event Action? HoverLost;
|
public event Action? HoverLost;
|
||||||
|
|
||||||
public SelectionBoxButton(IconUsage iconUsage, string tooltip)
|
public SelectionBoxButton(IconUsage iconUsage, string tooltip)
|
||||||
@ -49,11 +51,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
Circle.FlashColour(Colours.GrayF, 300);
|
Clicked?.Invoke();
|
||||||
|
|
||||||
|
TriggerAction();
|
||||||
|
|
||||||
TriggerOperationStarted();
|
|
||||||
Action?.Invoke();
|
|
||||||
TriggerOperationEnded();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,5 +72,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LocalisableString TooltipText { get; }
|
public LocalisableString TooltipText { get; }
|
||||||
|
|
||||||
|
public void TriggerAction()
|
||||||
|
{
|
||||||
|
Circle.FlashColour(Colours.GrayF, 300);
|
||||||
|
|
||||||
|
TriggerOperationStarted();
|
||||||
|
Action?.Invoke();
|
||||||
|
TriggerOperationEnded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,7 +410,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
|||||||
|
|
||||||
private void dailyChallengeChanged(ValueChangedEvent<DailyChallengeInfo?> change)
|
private void dailyChallengeChanged(ValueChangedEvent<DailyChallengeInfo?> change)
|
||||||
{
|
{
|
||||||
if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null)
|
if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null && metadataClient.IsConnected.Value)
|
||||||
{
|
{
|
||||||
notificationOverlay?.Post(new SimpleNotification { Text = DailyChallengeStrings.ChallengeEndedNotification });
|
notificationOverlay?.Post(new SimpleNotification { Text = DailyChallengeStrings.ChallengeEndedNotification });
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Layout;
|
|||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Localisation.SkinComponents;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts;
|
using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -33,7 +34,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
Precision = 1
|
Precision = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
[SettingSource("Use relative size")]
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))]
|
||||||
public BindableBool UseRelativeSize { get; } = new BindableBool(true);
|
public BindableBool UseRelativeSize { get; } = new BindableBool(true);
|
||||||
|
|
||||||
private ArgonHealthDisplayBar mainBar = null!;
|
private ArgonHealthDisplayBar mainBar = null!;
|
||||||
|
@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
|
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
|
||||||
public Bindable<bool> ShowTime { get; } = new BindableBool(true);
|
public Bindable<bool> ShowTime { get; } = new BindableBool(true);
|
||||||
|
|
||||||
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))]
|
||||||
|
public BindableBool UseRelativeSize { get; } = new BindableBool(true);
|
||||||
|
|
||||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||||
|
|
||||||
@ -99,6 +102,11 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
|
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
|
||||||
ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true);
|
ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true);
|
||||||
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
|
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
|
||||||
|
|
||||||
|
// see comment in ArgonHealthDisplay.cs regarding RelativeSizeAxes
|
||||||
|
float previousWidth = Width;
|
||||||
|
UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true);
|
||||||
|
Width = previousWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateObjects(IEnumerable<HitObject> objects)
|
protected override void UpdateObjects(IEnumerable<HitObject> objects)
|
||||||
|
@ -41,6 +41,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
if (!Interactive)
|
||||||
|
return default;
|
||||||
|
|
||||||
double progress = Math.Clamp(lastMouseX, 0, DrawWidth) / DrawWidth;
|
double progress = Math.Clamp(lastMouseX, 0, DrawWidth) / DrawWidth;
|
||||||
|
|
||||||
TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress));
|
TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress));
|
||||||
|
@ -37,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
|
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
|
||||||
public Bindable<bool> ShowTime { get; } = new BindableBool(true);
|
public Bindable<bool> ShowTime { get; } = new BindableBool(true);
|
||||||
|
|
||||||
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))]
|
||||||
|
public BindableBool UseRelativeSize { get; } = new BindableBool(true);
|
||||||
|
|
||||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||||
|
|
||||||
@ -83,6 +86,11 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
graph.FillColour = bar.FillColour = colours.BlueLighter;
|
graph.FillColour = bar.FillColour = colours.BlueLighter;
|
||||||
|
|
||||||
|
// see comment in ArgonHealthDisplay.cs regarding RelativeSizeAxes
|
||||||
|
float previousWidth = Width;
|
||||||
|
UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true);
|
||||||
|
Width = previousWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -299,7 +299,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
case GlobalAction.Back:
|
case GlobalAction.Back:
|
||||||
if (!pendingAnimation)
|
if (!pendingAnimation)
|
||||||
BeginConfirm();
|
Confirm();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.PauseGameplay:
|
case GlobalAction.PauseGameplay:
|
||||||
@ -307,7 +307,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
if (ReplayLoaded.Value) return false;
|
if (ReplayLoaded.Value) return false;
|
||||||
|
|
||||||
if (!pendingAnimation)
|
if (!pendingAnimation)
|
||||||
BeginConfirm();
|
Confirm();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,7 +455,19 @@ namespace osu.Game.Screens.Play
|
|||||||
MetadataInfo.Loading = true;
|
MetadataInfo.Loading = true;
|
||||||
|
|
||||||
content.FadeInFromZero(500, Easing.OutQuint);
|
content.FadeInFromZero(500, Easing.OutQuint);
|
||||||
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
|
|
||||||
|
if (quickRestart)
|
||||||
|
{
|
||||||
|
prepareNewPlayer();
|
||||||
|
content.ScaleTo(1, 650, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
content
|
||||||
|
.ScaleTo(1, 650, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.Schedule(prepareNewPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
using (BeginDelayedSequence(delayBeforeSideDisplays))
|
using (BeginDelayedSequence(delayBeforeSideDisplays))
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Screens.Select
|
|||||||
public BeatmapDeleteDialog(BeatmapSetInfo beatmapSet)
|
public BeatmapDeleteDialog(BeatmapSetInfo beatmapSet)
|
||||||
{
|
{
|
||||||
this.beatmapSet = beatmapSet;
|
this.beatmapSet = beatmapSet;
|
||||||
BodyText = $@"{beatmapSet.Metadata.Artist} - {beatmapSet.Metadata.Title}";
|
BodyText = beatmapSet.Metadata.GetDisplayTitleRomanisable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -66,6 +66,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
|
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
|
||||||
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length);
|
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length);
|
||||||
match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue);
|
match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue);
|
||||||
|
match &= !criteria.DateRanked.HasFilter || (BeatmapInfo.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet.DateRanked.Value));
|
||||||
|
match &= !criteria.DateSubmitted.HasFilter || (BeatmapInfo.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet.DateSubmitted.Value));
|
||||||
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);
|
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);
|
||||||
|
|
||||||
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);
|
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
@ -36,9 +37,6 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
|
||||||
|
|
||||||
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
||||||
private readonly StatisticRow starDifficulty;
|
private readonly StatisticRow starDifficulty;
|
||||||
|
|
||||||
@ -69,6 +67,14 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public Bindable<RulesetInfo> Ruleset { get; } = new Bindable<RulesetInfo>();
|
public Bindable<RulesetInfo> Ruleset { get; } = new Bindable<RulesetInfo>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mods to be used for certain elements of display.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// No checks are done as to whether the mods specified are valid for the current <see cref="Ruleset"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public Bindable<IReadOnlyList<Mod>> Mods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||||
|
|
||||||
public AdvancedStats(int columns = 1)
|
public AdvancedStats(int columns = 1)
|
||||||
{
|
{
|
||||||
switch (columns)
|
switch (columns)
|
||||||
@ -143,8 +149,7 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Ruleset.BindValueChanged(_ => updateStatistics());
|
Ruleset.BindValueChanged(_ => updateStatistics());
|
||||||
|
Mods.BindValueChanged(modsChanged, true);
|
||||||
mods.BindValueChanged(modsChanged, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ModSettingChangeTracker modSettingChangeTracker;
|
private ModSettingChangeTracker modSettingChangeTracker;
|
||||||
@ -173,14 +178,14 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
{
|
{
|
||||||
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty);
|
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty);
|
||||||
|
|
||||||
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
|
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
|
||||||
mod.ApplyToDifficulty(originalDifficulty);
|
mod.ApplyToDifficulty(originalDifficulty);
|
||||||
|
|
||||||
adjustedDifficulty = originalDifficulty;
|
adjustedDifficulty = originalDifficulty;
|
||||||
|
|
||||||
if (Ruleset.Value != null)
|
if (Ruleset.Value != null)
|
||||||
{
|
{
|
||||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
double rate = ModUtils.CalculateRateWithMods(Mods.Value);
|
||||||
|
|
||||||
adjustedDifficulty = Ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
|
adjustedDifficulty = Ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
|
||||||
|
|
||||||
@ -198,7 +203,7 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
// For the time being, the key count is static no matter what, because:
|
// For the time being, the key count is static no matter what, because:
|
||||||
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
|
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
|
||||||
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
|
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
|
||||||
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo, mods.Value);
|
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo, Mods.Value);
|
||||||
|
|
||||||
FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania;
|
FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania;
|
||||||
FirstValue.Value = (keyCount, keyCount);
|
FirstValue.Value = (keyCount, keyCount);
|
||||||
@ -236,7 +241,7 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, null, starDifficultyCancellationSource.Token);
|
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, null, starDifficultyCancellationSource.Token);
|
||||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, Mods.Value, starDifficultyCancellationSource.Token);
|
||||||
|
|
||||||
Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() =>
|
Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() =>
|
||||||
{
|
{
|
||||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Screens.Select
|
|||||||
public OptionalRange<int> BeatDivisor;
|
public OptionalRange<int> BeatDivisor;
|
||||||
public OptionalSet<BeatmapOnlineStatus> OnlineStatus = new OptionalSet<BeatmapOnlineStatus>();
|
public OptionalSet<BeatmapOnlineStatus> OnlineStatus = new OptionalSet<BeatmapOnlineStatus>();
|
||||||
public OptionalRange<DateTimeOffset> LastPlayed;
|
public OptionalRange<DateTimeOffset> LastPlayed;
|
||||||
|
public OptionalRange<DateTimeOffset> DateRanked;
|
||||||
|
public OptionalRange<DateTimeOffset> DateSubmitted;
|
||||||
public OptionalTextFilter Creator;
|
public OptionalTextFilter Creator;
|
||||||
public OptionalTextFilter Artist;
|
public OptionalTextFilter Artist;
|
||||||
public OptionalTextFilter Title;
|
public OptionalTextFilter Title;
|
||||||
|
@ -65,6 +65,12 @@ namespace osu.Game.Screens.Select
|
|||||||
case "lastplayed":
|
case "lastplayed":
|
||||||
return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value);
|
return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value);
|
||||||
|
|
||||||
|
case "ranked":
|
||||||
|
return tryUpdateRankedDateRange(ref criteria.DateRanked, op, value);
|
||||||
|
|
||||||
|
case "submitted":
|
||||||
|
return tryUpdateRankedDateRange(ref criteria.DateSubmitted, op, value);
|
||||||
|
|
||||||
case "played":
|
case "played":
|
||||||
if (!tryParseBool(value, out bool played))
|
if (!tryParseBool(value, out bool played))
|
||||||
return false;
|
return false;
|
||||||
@ -592,5 +598,163 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset.Value);
|
return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper function for building a UTC date from only the year, month and day.
|
||||||
|
/// UTC is used to keep consistent search results with osu!web.
|
||||||
|
/// </summary>
|
||||||
|
private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) =>
|
||||||
|
new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a string containing a ranked or submitted date filter.
|
||||||
|
/// Returns a boolean depending on whether parsing was successful or not.
|
||||||
|
/// Accepted dates are in the formats `yyyy`, `yyyy-mm` and `yyyy-mm-dd`.
|
||||||
|
/// Leading zeros are accepted. Numbers can be separated by `-`, `/`, or `.`
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateRange">The <see cref="FilterCriteria.OptionalRange{DateTimeOffset}"/> to store the parsed data into, if successful.</param>
|
||||||
|
/// <param name="op">The operator of the filtering query</param>
|
||||||
|
/// <param name="val">The string value to attempt parsing for.</param>
|
||||||
|
private static bool tryUpdateRankedDateRange(ref FilterCriteria.OptionalRange<DateTimeOffset> dateRange, Operator op, string val)
|
||||||
|
{
|
||||||
|
GroupCollection? match = tryMatchRegex(val, @"^(?<year>\d+)([-/.](?<month>\d+)([-/.](?<day>\d+))?)?$");
|
||||||
|
|
||||||
|
if (match == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int? year = null;
|
||||||
|
int? month = null;
|
||||||
|
int? day = null;
|
||||||
|
|
||||||
|
List<string> keys = new List<string> { @"year", @"month", @"day" };
|
||||||
|
|
||||||
|
foreach (string key in keys)
|
||||||
|
{
|
||||||
|
if (!match.TryGetValue(key, out var group) || !group.Success)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (group.Success)
|
||||||
|
{
|
||||||
|
if (!tryParseDoubleWithPoint(group.Value, out double value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case @"year":
|
||||||
|
year = (int)value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case @"month":
|
||||||
|
month = (int)value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case @"day":
|
||||||
|
day = (int)value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DateTimeOffset dateTimeOffset;
|
||||||
|
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case Operator.Less:
|
||||||
|
month ??= 1;
|
||||||
|
day ??= 1;
|
||||||
|
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset);
|
||||||
|
|
||||||
|
case Operator.LessOrEqual:
|
||||||
|
if (month == null)
|
||||||
|
{
|
||||||
|
month = 1;
|
||||||
|
day = 1;
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day == null)
|
||||||
|
{
|
||||||
|
day = 1;
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset);
|
||||||
|
|
||||||
|
case Operator.GreaterOrEqual:
|
||||||
|
month ??= 1;
|
||||||
|
day ??= 1;
|
||||||
|
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset);
|
||||||
|
|
||||||
|
case Operator.Greater:
|
||||||
|
if (month == null)
|
||||||
|
{
|
||||||
|
month = 1;
|
||||||
|
day = 1;
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day == null)
|
||||||
|
{
|
||||||
|
day = 1;
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset);
|
||||||
|
|
||||||
|
case Operator.Equal:
|
||||||
|
|
||||||
|
DateTimeOffset minDateTimeOffset;
|
||||||
|
DateTimeOffset maxDateTimeOffset;
|
||||||
|
|
||||||
|
if (month == null)
|
||||||
|
{
|
||||||
|
month = 1;
|
||||||
|
day = 1;
|
||||||
|
minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value);
|
||||||
|
maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset)
|
||||||
|
&& tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day == null)
|
||||||
|
{
|
||||||
|
day = 1;
|
||||||
|
minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value);
|
||||||
|
maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset)
|
||||||
|
&& tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value);
|
||||||
|
maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1);
|
||||||
|
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset)
|
||||||
|
&& tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -610,11 +610,6 @@ namespace osu.Game.Screens.Select
|
|||||||
beatmapInfoPrevious = beatmap;
|
beatmapInfoPrevious = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we can't run this in the debounced run due to the selected mods bindable not being debounced,
|
|
||||||
// since mods could be updated to the new ruleset instances while the decoupled bindable is held behind,
|
|
||||||
// therefore resulting in performing difficulty calculation with invalid states.
|
|
||||||
advancedStats.Ruleset.Value = ruleset;
|
|
||||||
|
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
// clear pending task immediately to track any potential nested debounce operation.
|
// clear pending task immediately to track any potential nested debounce operation.
|
||||||
@ -878,6 +873,8 @@ namespace osu.Game.Screens.Select
|
|||||||
ModSelect.Beatmap.Value = beatmap;
|
ModSelect.Beatmap.Value = beatmap;
|
||||||
|
|
||||||
advancedStats.BeatmapInfo = beatmap.BeatmapInfo;
|
advancedStats.BeatmapInfo = beatmap.BeatmapInfo;
|
||||||
|
advancedStats.Mods.Value = selectedMods.Value;
|
||||||
|
advancedStats.Ruleset.Value = Ruleset.Value;
|
||||||
|
|
||||||
bool beatmapSelected = beatmap is not DummyWorkingBeatmap;
|
bool beatmapSelected = beatmap is not DummyWorkingBeatmap;
|
||||||
|
|
||||||
@ -990,6 +987,12 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
Beatmap.BindValueChanged(updateCarouselSelection);
|
Beatmap.BindValueChanged(updateCarouselSelection);
|
||||||
|
|
||||||
|
selectedMods.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (decoupledRuleset.Value.Equals(rulesetNoDebounce))
|
||||||
|
advancedStats.Mods.Value = selectedMods.Value;
|
||||||
|
}, true);
|
||||||
|
|
||||||
boundLocalBindables = true;
|
boundLocalBindables = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -35,25 +33,6 @@ namespace osu.Game.Skinning.Components
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||||
|
|
||||||
private readonly Dictionary<BeatmapAttribute, LocalisableString> valueDictionary = new Dictionary<BeatmapAttribute, LocalisableString>();
|
|
||||||
|
|
||||||
private static readonly ImmutableDictionary<BeatmapAttribute, LocalisableString> label_dictionary = new Dictionary<BeatmapAttribute, LocalisableString>
|
|
||||||
{
|
|
||||||
[BeatmapAttribute.CircleSize] = BeatmapsetsStrings.ShowStatsCs,
|
|
||||||
[BeatmapAttribute.Accuracy] = BeatmapsetsStrings.ShowStatsAccuracy,
|
|
||||||
[BeatmapAttribute.HPDrain] = BeatmapsetsStrings.ShowStatsDrain,
|
|
||||||
[BeatmapAttribute.ApproachRate] = BeatmapsetsStrings.ShowStatsAr,
|
|
||||||
[BeatmapAttribute.StarRating] = BeatmapsetsStrings.ShowStatsStars,
|
|
||||||
[BeatmapAttribute.Title] = EditorSetupStrings.Title,
|
|
||||||
[BeatmapAttribute.Artist] = EditorSetupStrings.Artist,
|
|
||||||
[BeatmapAttribute.DifficultyName] = EditorSetupStrings.DifficultyHeader,
|
|
||||||
[BeatmapAttribute.Creator] = EditorSetupStrings.Creator,
|
|
||||||
[BeatmapAttribute.Source] = EditorSetupStrings.Source,
|
|
||||||
[BeatmapAttribute.Length] = ArtistStrings.TracklistLength.ToTitle(),
|
|
||||||
[BeatmapAttribute.RankedStatus] = BeatmapDiscussionsStrings.IndexFormBeatmapsetStatusDefault,
|
|
||||||
[BeatmapAttribute.BPM] = BeatmapsetsStrings.ShowStatsBpm,
|
|
||||||
}.ToImmutableDictionary();
|
|
||||||
|
|
||||||
private readonly OsuSpriteText text;
|
private readonly OsuSpriteText text;
|
||||||
|
|
||||||
public BeatmapAttributeText()
|
public BeatmapAttributeText()
|
||||||
@ -74,52 +53,135 @@ namespace osu.Game.Skinning.Components
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Attribute.BindValueChanged(_ => updateLabel());
|
Attribute.BindValueChanged(_ => updateText());
|
||||||
Template.BindValueChanged(_ => updateLabel());
|
Template.BindValueChanged(_ => updateText());
|
||||||
beatmap.BindValueChanged(b =>
|
beatmap.BindValueChanged(_ => updateText());
|
||||||
{
|
|
||||||
updateBeatmapContent(b.NewValue);
|
updateText();
|
||||||
updateLabel();
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateBeatmapContent(WorkingBeatmap workingBeatmap)
|
private void updateText()
|
||||||
{
|
|
||||||
valueDictionary[BeatmapAttribute.Title] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.TitleUnicode, workingBeatmap.BeatmapInfo.Metadata.Title);
|
|
||||||
valueDictionary[BeatmapAttribute.Artist] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.ArtistUnicode, workingBeatmap.BeatmapInfo.Metadata.Artist);
|
|
||||||
valueDictionary[BeatmapAttribute.DifficultyName] = workingBeatmap.BeatmapInfo.DifficultyName;
|
|
||||||
valueDictionary[BeatmapAttribute.Creator] = workingBeatmap.BeatmapInfo.Metadata.Author.Username;
|
|
||||||
valueDictionary[BeatmapAttribute.Source] = workingBeatmap.BeatmapInfo.Metadata.Source;
|
|
||||||
valueDictionary[BeatmapAttribute.Length] = TimeSpan.FromMilliseconds(workingBeatmap.BeatmapInfo.Length).ToFormattedDuration();
|
|
||||||
valueDictionary[BeatmapAttribute.RankedStatus] = workingBeatmap.BeatmapInfo.Status.GetLocalisableDescription();
|
|
||||||
valueDictionary[BeatmapAttribute.BPM] = workingBeatmap.BeatmapInfo.BPM.ToLocalisableString(@"F2");
|
|
||||||
valueDictionary[BeatmapAttribute.CircleSize] = ((double)workingBeatmap.BeatmapInfo.Difficulty.CircleSize).ToLocalisableString(@"F2");
|
|
||||||
valueDictionary[BeatmapAttribute.HPDrain] = ((double)workingBeatmap.BeatmapInfo.Difficulty.DrainRate).ToLocalisableString(@"F2");
|
|
||||||
valueDictionary[BeatmapAttribute.Accuracy] = ((double)workingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty).ToLocalisableString(@"F2");
|
|
||||||
valueDictionary[BeatmapAttribute.ApproachRate] = ((double)workingBeatmap.BeatmapInfo.Difficulty.ApproachRate).ToLocalisableString(@"F2");
|
|
||||||
valueDictionary[BeatmapAttribute.StarRating] = workingBeatmap.BeatmapInfo.StarRating.ToLocalisableString(@"F2");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLabel()
|
|
||||||
{
|
{
|
||||||
string numberedTemplate = Template.Value
|
string numberedTemplate = Template.Value
|
||||||
.Replace("{", "{{")
|
.Replace("{", "{{")
|
||||||
.Replace("}", "}}")
|
.Replace("}", "}}")
|
||||||
.Replace(@"{{Label}}", "{0}")
|
.Replace(@"{{Label}}", "{0}")
|
||||||
.Replace(@"{{Value}}", $"{{{1 + (int)Attribute.Value}}}");
|
.Replace(@"{{Value}}", "{1}");
|
||||||
|
|
||||||
object?[] args = valueDictionary.OrderBy(pair => pair.Key)
|
List<object?> values = new List<object?>
|
||||||
.Select(pair => pair.Value)
|
{
|
||||||
.Prepend(label_dictionary[Attribute.Value])
|
getLabelString(Attribute.Value),
|
||||||
.Cast<object?>()
|
getValueString(Attribute.Value)
|
||||||
.ToArray();
|
};
|
||||||
|
|
||||||
foreach (var type in Enum.GetValues<BeatmapAttribute>())
|
foreach (var type in Enum.GetValues<BeatmapAttribute>())
|
||||||
{
|
{
|
||||||
numberedTemplate = numberedTemplate.Replace($"{{{{{type}}}}}", $"{{{1 + (int)type}}}");
|
string replaced = numberedTemplate.Replace($@"{{{{{type}}}}}", $@"{{{values.Count}}}");
|
||||||
|
|
||||||
|
if (numberedTemplate != replaced)
|
||||||
|
{
|
||||||
|
numberedTemplate = replaced;
|
||||||
|
values.Add(getValueString(type));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text.Text = LocalisableString.Format(numberedTemplate, args);
|
text.Text = LocalisableString.Format(numberedTemplate, values.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalisableString getLabelString(BeatmapAttribute attribute)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case BeatmapAttribute.CircleSize:
|
||||||
|
return BeatmapsetsStrings.ShowStatsCs;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Accuracy:
|
||||||
|
return BeatmapsetsStrings.ShowStatsAccuracy;
|
||||||
|
|
||||||
|
case BeatmapAttribute.HPDrain:
|
||||||
|
return BeatmapsetsStrings.ShowStatsDrain;
|
||||||
|
|
||||||
|
case BeatmapAttribute.ApproachRate:
|
||||||
|
return BeatmapsetsStrings.ShowStatsAr;
|
||||||
|
|
||||||
|
case BeatmapAttribute.StarRating:
|
||||||
|
return BeatmapsetsStrings.ShowStatsStars;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Title:
|
||||||
|
return EditorSetupStrings.Title;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Artist:
|
||||||
|
return EditorSetupStrings.Artist;
|
||||||
|
|
||||||
|
case BeatmapAttribute.DifficultyName:
|
||||||
|
return EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Creator:
|
||||||
|
return EditorSetupStrings.Creator;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Source:
|
||||||
|
return EditorSetupStrings.Source;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Length:
|
||||||
|
return ArtistStrings.TracklistLength.ToTitle();
|
||||||
|
|
||||||
|
case BeatmapAttribute.RankedStatus:
|
||||||
|
return BeatmapDiscussionsStrings.IndexFormBeatmapsetStatusDefault;
|
||||||
|
|
||||||
|
case BeatmapAttribute.BPM:
|
||||||
|
return BeatmapsetsStrings.ShowStatsBpm;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalisableString getValueString(BeatmapAttribute attribute)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case BeatmapAttribute.Title:
|
||||||
|
return new RomanisableString(beatmap.Value.BeatmapInfo.Metadata.TitleUnicode, beatmap.Value.BeatmapInfo.Metadata.Title);
|
||||||
|
|
||||||
|
case BeatmapAttribute.Artist:
|
||||||
|
return new RomanisableString(beatmap.Value.BeatmapInfo.Metadata.ArtistUnicode, beatmap.Value.BeatmapInfo.Metadata.Artist);
|
||||||
|
|
||||||
|
case BeatmapAttribute.DifficultyName:
|
||||||
|
return beatmap.Value.BeatmapInfo.DifficultyName;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Creator:
|
||||||
|
return beatmap.Value.BeatmapInfo.Metadata.Author.Username;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Source:
|
||||||
|
return beatmap.Value.BeatmapInfo.Metadata.Source;
|
||||||
|
|
||||||
|
case BeatmapAttribute.Length:
|
||||||
|
return TimeSpan.FromMilliseconds(beatmap.Value.BeatmapInfo.Length).ToFormattedDuration();
|
||||||
|
|
||||||
|
case BeatmapAttribute.RankedStatus:
|
||||||
|
return beatmap.Value.BeatmapInfo.Status.GetLocalisableDescription();
|
||||||
|
|
||||||
|
case BeatmapAttribute.BPM:
|
||||||
|
return beatmap.Value.BeatmapInfo.BPM.ToLocalisableString(@"F2");
|
||||||
|
|
||||||
|
case BeatmapAttribute.CircleSize:
|
||||||
|
return ((double)beatmap.Value.BeatmapInfo.Difficulty.CircleSize).ToLocalisableString(@"F2");
|
||||||
|
|
||||||
|
case BeatmapAttribute.HPDrain:
|
||||||
|
return ((double)beatmap.Value.BeatmapInfo.Difficulty.DrainRate).ToLocalisableString(@"F2");
|
||||||
|
|
||||||
|
case BeatmapAttribute.Accuracy:
|
||||||
|
return ((double)beatmap.Value.BeatmapInfo.Difficulty.OverallDifficulty).ToLocalisableString(@"F2");
|
||||||
|
|
||||||
|
case BeatmapAttribute.ApproachRate:
|
||||||
|
return ((double)beatmap.Value.BeatmapInfo.Difficulty.ApproachRate).ToLocalisableString(@"F2");
|
||||||
|
|
||||||
|
case BeatmapAttribute.StarRating:
|
||||||
|
return beatmap.Value.BeatmapInfo.StarRating.ToLocalisableString(@"F2");
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
||||||
|
@ -13,7 +13,8 @@ namespace osu.Game.Tests.Visual.Metadata
|
|||||||
{
|
{
|
||||||
public partial class TestMetadataClient : MetadataClient
|
public partial class TestMetadataClient : MetadataClient
|
||||||
{
|
{
|
||||||
public override IBindable<bool> IsConnected => new BindableBool(true);
|
public override IBindable<bool> IsConnected => isConnected;
|
||||||
|
private readonly BindableBool isConnected = new BindableBool(true);
|
||||||
|
|
||||||
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
|
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
|
||||||
private readonly BindableBool isWatchingUserPresence = new BindableBool();
|
private readonly BindableBool isWatchingUserPresence = new BindableBool();
|
||||||
@ -98,5 +99,16 @@ namespace osu.Game.Tests.Visual.Metadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask;
|
public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
isConnected.Value = false;
|
||||||
|
dailyChallengeInfo.Value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reconnect()
|
||||||
|
{
|
||||||
|
isConnected.Value = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,16 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.2" />
|
<PackageReference Include="DiffPlex" Version="1.7.2" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
||||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||||
<PackageReference Include="MessagePack" Version="2.5.140" />
|
<PackageReference Include="MessagePack" Version="2.5.187" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.15" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.15" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="7.0.15" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.12" />
|
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2024.802.0">
|
<PackageReference Include="ppy.LocalisationAnalyser" Version="2024.802.0">
|
||||||
@ -35,13 +35,13 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.5.0" />
|
<PackageReference Include="Realm" Version="11.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1007.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2024.1009.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1003.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1003.0" />
|
||||||
<PackageReference Include="Sentry" Version="4.3.0" />
|
<PackageReference Include="Sentry" Version="4.12.1" />
|
||||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.8" />
|
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
|
|
||||||
|
@ -17,6 +17,6 @@
|
|||||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1007.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1009.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user