1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-20 20:33:21 +08:00

Merge branch 'master' into editor-slider-touch-support-2

This commit is contained in:
Salman Alshamrani 2024-10-23 17:02:38 -04:00 committed by GitHub
commit 6e9ff49e59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
107 changed files with 2187 additions and 418 deletions

View File

@ -341,9 +341,12 @@ jobs:
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
docker-compose up --build generator
link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
docker compose up --build --detach
docker compose logs --follow &
docker compose wait generator
link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
@ -353,7 +356,7 @@ jobs:
if: ${{ always() }}
run: |
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
docker-compose down -v
docker compose down --volumes
output-cli:
name: Output info

View File

@ -9,7 +9,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -9,7 +9,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -9,7 +9,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -9,7 +9,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -118,5 +118,45 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
}
[Test]
public void TestOffScreenObjectsRemainSelectedOnHorizontalFlip()
{
AddStep("create objects", () =>
{
for (int i = 0; i < 20; ++i)
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = i % 4 });
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("flip", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.H);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
}
[Test]
public void TestOffScreenObjectsRemainSelectedOnVerticalFlip()
{
AddStep("create objects", () =>
{
for (int i = 0; i < 20; ++i)
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = i % 4 });
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("flip", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.J);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects.Reverse()));
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -54,9 +54,8 @@ namespace osu.Game.Rulesets.Mania.Edit
int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
EditorBeatmap.PerformOnSelection(hitObject =>
performOnSelection(maniaObject =>
{
var maniaObject = (ManiaHitObject)hitObject;
maniaPlayfield.Remove(maniaObject);
maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
maniaPlayfield.Add(maniaObject);
@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Edit
double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
EditorBeatmap.PerformOnSelection(hitObject =>
performOnSelection(hitObject =>
{
hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
});
@ -117,14 +116,21 @@ namespace osu.Game.Rulesets.Mania.Edit
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
EditorBeatmap.PerformOnSelection(h =>
performOnSelection(h =>
{
maniaPlayfield.Remove(h);
((ManiaHitObject)h).Column += columnDelta;
h.Column += columnDelta;
maniaPlayfield.Add(h);
});
}
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
private void performOnSelection(Action<ManiaHitObject> action)
{
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
EditorBeatmap.PerformOnSelection(h => action.Invoke((ManiaHitObject)h));
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with mania's usage patterns,
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
// (check blame for detailed explanation).
// thus, ensure that selection is preserved manually.

View File

@ -9,6 +9,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osu.Game.Utils;
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void gridActive<T>(bool active) where T : PositionSnapGrid
{
AddAssert($"grid type is {typeof(T).Name}", () => this.ChildrenOfType<T>().Any());
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
AddStep("move cursor to spacing + (1, 1)", () =>
{
@ -161,7 +163,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return grid switch
{
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(
new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
_ => Vector2.Zero
};
@ -170,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestGridSizeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
gridSizeIs(4);
@ -189,5 +192,97 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size);
[Test]
public void TestGridTypeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(true);
nextGridTypeIs<TriangularPositionSnapGrid>();
nextGridTypeIs<CircularPositionSnapGrid>();
nextGridTypeIs<RectangularPositionSnapGrid>();
}
private void nextGridTypeIs<T>() where T : PositionSnapGrid
{
AddStep("toggle to next grid type", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.ShiftLeft);
});
gridActive<T>(true);
}
[Test]
public void TestGridPlacementTool()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
AddStep("move cursor to slider head + (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).Position + new Vector2(1, 1)));
});
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddStep("move cursor to slider tail + (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
});
AddStep("left click", () => InputManager.Click(MouseButton.Left));
gridActive<RectangularPositionSnapGrid>(true);
AddAssert("grid position at slider head", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).Position, composer.StartPosition.Value);
});
AddAssert("grid spacing is distance to slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
});
AddAssert("grid rotation points to slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
});
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
AddStep("move cursor to slider tail + (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
});
AddStep("double click", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddStep("move cursor to (0, 0)", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(Vector2.Zero));
});
gridActive<RectangularPositionSnapGrid>(true);
AddAssert("grid position at slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).EndPosition, composer.StartPosition.Value);
});
AddAssert("grid spacing and rotation unchanged", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
});
}
}
}

View File

@ -0,0 +1,55 @@
// 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.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneToolSwitching : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestSliderAnchorMoveOperationEndsOnSwitchingTool()
{
var initialPosition = Vector2.Zero;
AddStep("store original anchor position", () => initialPosition = EditorBeatmap.HitObjects.OfType<Slider>().First().Path.ControlPoints.ElementAt(1).Position);
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<Slider>().First()));
AddStep("move to second anchor", () => InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1)));
AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
AddStep("drag away", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
AddStep("switch tool", () => InputManager.PressButton(MouseButton.Button1));
AddStep("undo", () => Editor.Undo());
AddAssert("anchor back at original position",
() => EditorBeatmap.HitObjects.OfType<Slider>().First().Path.ControlPoints.ElementAt(1).Position,
() => Is.EqualTo(initialPosition));
}
[Test]
public void TestSliderAnchorCreationOperationEndsOnSwitchingTool()
{
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<Slider>().First()));
AddStep("move to second anchor", () => InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1), new Vector2(-50, 0)));
AddStep("quick-create anchor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("drag away", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
AddStep("switch tool", () => InputManager.PressKey(Key.Number3));
AddStep("drag away further", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<Slider>().First()));
AddStep("undo", () => Editor.Undo());
AddAssert("slider has three anchors again", () => EditorBeatmap.HitObjects.OfType<Slider>().First().Path.ControlPoints, () => Has.Count.EqualTo(3));
}
}
}

View 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));
}
});
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />

View File

@ -25,6 +25,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
/// <summary>
/// Missed slider ticks that includes missed reverse arrows. Will only be correct on non-classic scores
/// </summary>
private int countSliderTickMiss;
/// <summary>
/// Amount of missed slider tails that don't break combo. Will only be correct on non-classic scores
/// </summary>
private int countSliderEndsDropped;
/// <summary>
/// Estimated total amount of combo breaks
/// </summary>
private double effectiveMissCount;
public OsuPerformanceCalculator()
@ -44,7 +57,36 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
if (osuAttributes.SliderCount > 0)
{
if (usingClassicSliderAccuracy)
{
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold)
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
// In classic scores there can't be more misses than a sum of all non-perfect judgements
effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
}
else
{
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
if (scoreMaxCombo < fullComboThreshold)
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
// Combine regular misses with tick misses since tick misses break combo as well
effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
}
}
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
@ -124,8 +166,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.SliderCount > 0)
{
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
double estimateImproperlyFollowedDifficultSliders;
if (usingClassicSliderAccuracy)
{
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
int maximumPossibleDroppedSliders = totalImperfectHits;
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
}
else
{
// We add tick misses here since they too mean that the player didn't follow the slider properly
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
}
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
@ -247,29 +303,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
{
// Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0;
if (attributes.SliderCount > 0)
{
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
// Clamp miss count to maximum amount of possible breaks
comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
return Math.Max(countMiss, comboBasedMissCount);
}
// Miss penalty assumes that a player will miss on the hardest parts of a map,
// so we use the amount of relatively difficult sections to adjust miss penalty
// to make it more punishing on maps with lower amount of hard sections.
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalImperfectHits => countOk + countMeh + countMiss;
}
}

View File

@ -333,6 +333,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
base.Dispose(isDisposing);
foreach (var p in Pieces)
p.ControlPoint.Changed -= controlPointChanged;
if (draggedControlPointIndex >= 0)
DragEnded();
}
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
@ -392,7 +395,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private Vector2[] dragStartPositions;
private PathType?[] dragPathTypes;
private int draggedControlPointIndex;
private int draggedControlPointIndex = -1;
private HashSet<PathControlPoint> selectedControlPoints;
private List<MenuItem> curveTypeItems;
@ -473,7 +476,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
EnsureValidPathTypes();
}
public void DragEnded() => changeHandler?.EndChange();
public void DragEnded()
{
changeHandler?.EndChange();
draggedControlPointIndex = -1;
}
#endregion

View File

@ -178,6 +178,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.OnDeselected();
if (placementControlPoint != null)
endControlPointPlacement();
updateVisualDefinition();
BodyPiece.RecyclePath();
}
@ -377,13 +380,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnMouseUp(MouseUpEvent e)
{
if (placementControlPoint != null)
{
if (IsDragged)
ControlPointVisualiser?.DragEnded();
endControlPointPlacement();
}
placementControlPoint = null;
changeHandler?.EndChange();
}
private void endControlPointPlacement()
{
if (IsDragged)
ControlPointVisualiser?.DragEnded();
placementControlPoint = null;
changeHandler?.EndChange();
}
protected override bool OnKeyDown(KeyDownEvent e)

View File

@ -213,6 +213,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
gridTypeButtons.Items[(int)v.NewValue].Select();
switch (v.NewValue)
{
case PositionSnapGridType.Square:
@ -241,17 +243,16 @@ namespace osu.Game.Rulesets.Osu.Edit
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)
{
switch (e.Action)
{
case GlobalAction.EditorCycleGridDisplayMode:
nextGridSize();
case GlobalAction.EditorCycleGridSpacing:
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
return true;
case GlobalAction.EditorCycleGridType:
GridType.Value = (PositionSnapGridType)(((int)GridType.Value + 1) % Enum.GetValues<PositionSnapGridType>().Length);
return true;
}

View File

@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit
points = originalConvexHull!;
foreach (var point in points)
{
scale = clampToBound(scale, point, Vector2.Zero);
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
}
scale = clampToBounds(scale, point, Vector2.Zero, OsuPlayfield.BASE_SIZE);
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
return scale;
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
// Clamps the scale vector s such that the point p scaled by s is within the rectangle defined by lowerBounds and upperBounds
Vector2 clampToBounds(Vector2 s, Vector2 p, Vector2 lowerBounds, Vector2 upperBounds)
{
p -= actualOrigin;
bound -= actualOrigin;
lowerBounds -= actualOrigin;
upperBounds -= actualOrigin;
// a.X is the rotated X component of p with respect to the X bounds
// a.Y is the rotated X component of p with respect to the Y bounds
// b.X is the rotated Y component of p with respect to the X bounds
// b.Y is the rotated Y component of p with respect to the Y bounds
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
float sLowerBound, sUpperBound;
switch (adjustAxis)
{
case Axes.X:
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
break;
case Axes.Y:
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
break;
case Axes.Both:
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
// Here we compute the bounds for the magnitude multiplier of the scale vector
// Therefore the ratio s.X / s.Y will be maintained
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
s.X = s.X < 0
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
s.Y = s.Y < 0
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
break;
}
return s;
}
// Computes the bounds for the magnitude of the scaled point p with respect to the bounds lowerBounds and upperBounds
(float, float) computeBounds(Vector2 lowerBounds, Vector2 upperBounds, Vector2 p)
{
var sLowerBounds = Vector2.Divide(lowerBounds, p);
var sUpperBounds = Vector2.Divide(upperBounds, p);
// If the point is negative, then the bounds are flipped
if (p.X < 0)
(sLowerBounds.X, sUpperBounds.X) = (sUpperBounds.X, sLowerBounds.X);
if (p.Y < 0)
(sLowerBounds.Y, sUpperBounds.Y) = (sUpperBounds.Y, sLowerBounds.Y);
// If the point is at zero, then any scale will have no effect on the point so the bounds are infinite
// The float division would already give us infinity for the bounds, but the sign is not consistent so we have to manually set it
if (Precision.AlmostEquals(p.X, 0))
(sLowerBounds.X, sUpperBounds.X) = (float.NegativeInfinity, float.PositiveInfinity);
if (Precision.AlmostEquals(p.Y, 0))
(sLowerBounds.Y, sUpperBounds.Y) = (float.NegativeInfinity, float.PositiveInfinity);
return (MathF.Max(sLowerBounds.X, sLowerBounds.Y), MathF.Min(sUpperBounds.X, sUpperBounds.Y));
}
}
private void moveSelectionInBounds()

View File

@ -5,10 +5,13 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
@ -55,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit
MaxValue = 360,
Precision = 1
},
KeyboardStep = 1f,
Instantaneous = true
},
rotationOrigin = new EditorRadioButtonCollection
@ -126,6 +130,17 @@ namespace osu.Game.Rulesets.Osu.Edit
if (IsLoaded)
rotationHandler.Commit();
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Select && !e.Repeat)
{
this.HidePopover();
return true;
}
return base.OnPressed(e);
}
}
public enum RotationOrigin

View File

@ -5,11 +5,14 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
@ -70,6 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Value = 1,
Default = 1,
},
KeyboardStep = 0.01f,
Instantaneous = true
},
scaleOrigin = new EditorRadioButtonCollection
@ -136,8 +140,26 @@ namespace osu.Game.Rulesets.Osu.Edit
});
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
xCheckBox.Current.BindValueChanged(_ =>
{
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
{
yCheckBox.Current.Value = true;
return;
}
updateAxes();
});
yCheckBox.Current.BindValueChanged(_ =>
{
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
{
xCheckBox.Current.Value = true;
return;
}
updateAxes();
});
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
@ -152,6 +174,12 @@ namespace osu.Game.Rulesets.Osu.Edit
});
}
private void updateAxes()
{
scaleInfo.Value = scaleInfo.Value with { XAxis = xCheckBox.Current.Value, YAxis = yCheckBox.Current.Value };
updateMinMaxScale();
}
private void updateAxisCheckBoxesEnabled()
{
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
@ -175,12 +203,14 @@ namespace osu.Game.Rulesets.Osu.Edit
axisBindable.Disabled = !available;
}
private void updateMaxScale()
private void updateMinMaxScale()
{
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
return;
const float min_scale = 0.5f;
const float max_scale = 10;
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
if (!scaleInfo.Value.XAxis)
@ -189,12 +219,21 @@ namespace osu.Game.Rulesets.Osu.Edit
scale.Y = max_scale;
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(min_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
if (!scaleInfo.Value.XAxis)
scale.X = min_scale;
if (!scaleInfo.Value.YAxis)
scale.Y = min_scale;
scaleInputBindable.MinValue = MathF.Min(1, MathF.Max(scale.X, scale.Y));
}
private void setOrigin(ScaleOrigin origin)
{
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
updateMaxScale();
updateMinMaxScale();
updateAxisCheckBoxesEnabled();
}
@ -219,21 +258,26 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
private Axes getAdjustAxis(PreciseScaleInfo scale)
{
var result = Axes.None;
if (scale.XAxis)
result |= Axes.X;
if (scale.YAxis)
result |= Axes.Y;
return result;
}
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
private void setAxis(bool x, bool y)
{
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
updateMaxScale();
}
protected override void PopIn()
{
base.PopIn();
scaleHandler.Begin();
updateMaxScale();
updateMinMaxScale();
}
protected override void PopOut()
@ -242,6 +286,17 @@ namespace osu.Game.Rulesets.Osu.Edit
if (IsLoaded) scaleHandler.Commit();
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Select && !e.Repeat)
{
this.HidePopover();
return true;
}
return base.OnPressed(e);
}
}
public enum ScaleOrigin

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -25,5 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
}
};
}
public override void Update(Playfield playfield)
{
base.Update(playfield);
OsuPlayfield osuPlayfield = (OsuPlayfield)playfield;
Debug.Assert(osuPlayfield.Cursor != null);
osuPlayfield.Cursor.ActiveCursor.Rotation = -CurrentRotation;
}
}
}

View File

@ -120,6 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
PathProgress = e.PathProgress,
});
break;
@ -150,6 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
PathProgress = e.PathProgress,
});
break;
}

View File

@ -3,7 +3,6 @@
using System;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
@ -117,10 +116,9 @@ namespace osu.Game.Rulesets.Osu.Utils
if (osuObject is not Slider slider)
return;
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
modifySlider(slider, reflectNestedObject, reflectControlPoint);
modifySlider(slider, reflectControlPoint);
}
/// <summary>
@ -134,10 +132,9 @@ namespace osu.Game.Rulesets.Osu.Utils
if (osuObject is not Slider slider)
return;
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
modifySlider(slider, reflectNestedObject, reflectControlPoint);
modifySlider(slider, reflectControlPoint);
}
/// <summary>
@ -146,10 +143,9 @@ namespace osu.Game.Rulesets.Osu.Utils
/// <param name="slider">The slider to be flipped.</param>
public static void FlipSliderInPlaceHorizontally(Slider slider)
{
void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
modifySlider(slider, flipNestedObject, flipControlPoint);
modifySlider(slider, flipControlPoint);
}
/// <summary>
@ -159,18 +155,13 @@ namespace osu.Game.Rulesets.Osu.Utils
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
public static void RotateSlider(Slider slider, float rotation)
{
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
modifySlider(slider, rotateNestedObject, rotateControlPoint);
modifySlider(slider, rotateControlPoint);
}
private static void modifySlider(Slider slider, Action<OsuHitObject> modifyNestedObject, Action<PathControlPoint> modifyControlPoint)
private static void modifySlider(Slider slider, Action<PathControlPoint> modifyControlPoint)
{
// No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType<SliderTick>().ForEach(modifyNestedObject);
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(modifyNestedObject);
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
modifyControlPoint(point);

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public partial class TestSceneEditorPlacement : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
[Test]
public void TestPlacementBlueprintDoesNotCauseCrashes()
{
AddStep("clear objects", () => EditorBeatmap.Clear());
AddStep("add two objects", () =>
{
EditorBeatmap.Add(new Hit { StartTime = 1818 });
EditorBeatmap.Add(new Hit { StartTime = 1584 });
});
AddStep("seek back", () => EditorClock.Seek(1584));
AddStep("choose hit placement tool", () => InputManager.Key(Key.Number2));
AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(1)));
AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(0)));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddUntilStep("context menu open", () => Editor.ChildrenOfType<OsuContextMenu>().Any(menu => menu.State == MenuState.Open));
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -716,7 +716,7 @@ namespace osu.Game.Tests.Database
{
foreach (var entry in zip.Entries.ToArray())
{
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
if (entry.Key!.EndsWith(".osu", StringComparison.InvariantCulture))
zip.RemoveEntry(entry);
}

View File

@ -627,6 +627,87 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
}
private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) =>
new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero);
private static readonly object[] ranked_date_valid_test_cases =
{
new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
};
[Test]
[TestCaseSource(nameof(ranked_date_valid_test_cases))]
public void TestValidRankedDateQueries(string query, DateTimeOffset expected, Func<FilterCriteria, DateTimeOffset?> f)
{
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(true, filterCriteria.DateRanked.HasFilter);
Assert.AreEqual(expected, f(filterCriteria));
}
private static readonly object[] ranked_date_invalid_test_cases =
{
new object[] { "ranked<0" },
new object[] { "ranked=99999" },
new object[] { "ranked>=2012-03-05-04" },
};
[Test]
[TestCaseSource(nameof(ranked_date_invalid_test_cases))]
public void TestInvalidRankedDateQueries(string query)
{
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(false, filterCriteria.DateRanked.HasFilter);
}
private static readonly object[] submitted_date_test_cases =
{
new object[] { "submitted<2012", true },
new object[] { "submitted<2012.03", true },
new object[] { "submitted<2012/03/05", true },
new object[] { "submitted<2012-3-5", true },
new object[] { "submitted<0", false },
new object[] { "submitted=99999", false },
new object[] { "submitted>=2012-03-05-04", false },
new object[] { "submitted>=2012/03.05-04", false },
};
[Test]
[TestCaseSource(nameof(submitted_date_test_cases))]
public void TestInvalidRankedDateQueries(string query, bool expected)
{
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(expected, filterCriteria.DateSubmitted.HasFilter);
}
private static readonly object[] played_query_tests =
{
new object[] { "0", DateTimeOffset.MinValue, true },

View File

@ -96,6 +96,7 @@ namespace osu.Game.Tests.NonVisual
public override IAdjustableAudioComponent Audio { get; }
public override Playfield Playfield { get; }
public override PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; }
public override Container Overlays { get; }
public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; }

View File

@ -362,6 +362,12 @@ namespace osu.Game.Tests.Visual.Editing
}
});
AddStep("add whistle addition", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT));
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
@ -374,8 +380,10 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
AddStep("Press drum bank shortcut", () =>
{
@ -384,8 +392,10 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
AddStep("Press auto bank shortcut", () =>
{
@ -395,8 +405,47 @@ namespace osu.Game.Tests.Visual.Editing
});
// Should be a noop.
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
AddStep("Press addition normal bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.AltLeft);
});
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_NORMAL);
AddStep("Press addition drum bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.AltLeft);
});
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_DRUM);
AddStep("Press auto bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.AltLeft);
});
// Should be a noop.
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_DRUM);
}
[Test]
@ -414,7 +463,21 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
AddStep("Press soft addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.E);
InputManager.ReleaseKey(Key.AltLeft);
});
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
AddStep("Press finish sample shortcut", () =>
{
InputManager.Key(Key.E);
});
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_SOFT);
AddStep("Press drum bank shortcut", () =>
{
@ -423,7 +486,18 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_DRUM);
checkPlacementSampleBank(HitSampleInfo.BANK_DRUM);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_SOFT);
AddStep("Press drum addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.AltLeft);
});
checkPlacementSampleBank(HitSampleInfo.BANK_DRUM);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_DRUM);
AddStep("Press auto bank shortcut", () =>
{
@ -432,15 +506,29 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_DRUM);
AddStep("Press auto addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.AltLeft);
});
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
AddStep("Move after second object", () => EditorClock.Seek(750));
checkPlacementSample(HitSampleInfo.BANK_SOFT);
checkPlacementSampleBank(HitSampleInfo.BANK_SOFT);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_SOFT);
AddStep("Move to first object", () => EditorClock.Seek(0));
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
}
[Test]
@ -585,7 +673,29 @@ namespace osu.Game.Tests.Visual.Editing
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
AddStep("set normal addition bank", () =>
{
InputManager.PressKey(Key.LAlt);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.LAlt);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_NORMAL);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
}
@ -629,20 +739,37 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseKey(Key.LShift);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleNormalBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
AddStep("unify whistle addition", () => InputManager.Key(Key.W));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleNormalBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
AddStep("set drum addition bank", () =>
{
InputManager.PressKey(Key.LAlt);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LAlt);
});
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleNormalBank(0, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(0, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(0, 1, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
}

View File

@ -165,7 +165,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("enable automatic bank assignment", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.LAlt);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.LAlt);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
@ -228,7 +230,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("select drum bank", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.LAlt);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.LAlt);
InputManager.ReleaseKey(Key.LShift);
});
AddStep("enable clap addition", () => InputManager.Key(Key.R));

View File

@ -284,6 +284,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IAdjustableAudioComponent Audio { get; }
public override Playfield Playfield { get; }
public override PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; }
public override Container Overlays { get; }
public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; }

View File

@ -165,11 +165,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room join", () => RoomJoined);
AddStep("join other user (ready)", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID });
MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready);
});
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
AddUntilStep("wait for user populated", () => MultiplayerClient.ClientRoom!.Users.Single(u => u.UserID == PLAYER_1_ID).User, () => Is.Not.Null);
AddStep("other user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
ClickButtonWhenEnabled<MultiplayerSpectateButton>();

View File

@ -648,6 +648,34 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
}
[Test]
public void TestFiltering()
{
AddStep("Show overlay", () => chatOverlay.Show());
joinTestChannel(1);
joinTestChannel(3);
joinTestChannel(5);
joinChannel(new Channel(new APIUser { Id = 2001, Username = "alice" }));
joinChannel(new Channel(new APIUser { Id = 2002, Username = "bob" }));
joinChannel(new Channel(new APIUser { Id = 2003, Username = "charley the plant" }));
AddStep("filter to \"c\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "c");
AddUntilStep("bob filtered out", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(5));
AddStep("filter to \"channel\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "channel");
AddUntilStep("only public channels left", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(3));
AddStep("commit textbox", () =>
{
chatOverlay.ChildrenOfType<SearchTextBox>().Single().TakeFocus();
Schedule(() => InputManager.PressKey(Key.Enter));
});
AddUntilStep("#channel-2 active", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo("#channel-2"));
AddStep("filter to \"channel-3\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "channel-3");
AddUntilStep("no channels left", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(0));
}
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));

View File

@ -8,11 +8,13 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@ -90,6 +92,48 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
}
[Test]
public void TestHitResultsWithSameNameAreGrouped()
{
AddStep("Load scores without user best", () =>
{
var allScores = createScores();
allScores.UserScore = null;
scoresContainer.Scores = allScores;
});
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
AddAssert("only one column for slider end", () =>
{
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
return scoreTable.Columns.Count(c => c.Header.Equals("slider end")) == 1;
});
AddAssert("all rows show non-zero slider ends", () =>
{
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
int sliderEndColumnIndex = Array.FindIndex(scoreTable.Columns, c => c != null && c.Header.Equals("slider end"));
bool sliderEndFilledInEachRow = true;
for (int i = 0; i < scoreTable.Content?.GetLength(0); i++)
{
switch (scoreTable.Content[i, sliderEndColumnIndex])
{
case OsuSpriteText text:
if (text.Text.Equals(0.0d.ToLocalisableString(@"N0")))
sliderEndFilledInEachRow = false;
break;
default:
sliderEndFilledInEachRow = false;
break;
}
}
return sliderEndFilledInEachRow;
});
}
[Test]
public void TestUserBest()
{
@ -103,6 +147,18 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
AddStep("Load scores with personal best FC", () =>
{
var allScores = createScores();
allScores.UserScore = createUserBest();
allScores.UserScore.Score.Accuracy = 1;
scoresContainer.Beatmap.Value.MaxCombo = allScores.UserScore.Score.MaxCombo = 1337;
scoresContainer.Scores = allScores;
});
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
AddStep("Load scores with personal best (null position)", () =>
{
var allScores = createScores();
@ -287,13 +343,17 @@ namespace osu.Game.Tests.Visual.Online
const int initial_great_count = 2000;
const int initial_tick_count = 100;
const int initial_slider_end_count = 500;
int greatCount = initial_great_count;
int tickCount = initial_tick_count;
int sliderEndCount = initial_slider_end_count;
foreach (var s in scores.Scores)
foreach (var (score, index) in scores.Scores.Select((s, i) => (s, i)))
{
s.Statistics = new Dictionary<HitResult, int>
HitResult sliderEndResult = index % 2 == 0 ? HitResult.SliderTailHit : HitResult.SmallTickHit;
score.Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, greatCount },
{ HitResult.LargeTickHit, tickCount },
@ -301,10 +361,19 @@ namespace osu.Game.Tests.Visual.Online
{ HitResult.Meh, RNG.Next(100) },
{ HitResult.Miss, initial_great_count - greatCount },
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
{ sliderEndResult, sliderEndCount },
};
// Some hit results, including SliderTailHit and SmallTickHit, are only displayed
// when the maximum number is known
score.MaximumStatistics = new Dictionary<HitResult, int>
{
{ sliderEndResult, initial_slider_end_count },
};
greatCount -= 100;
tickCount -= RNG.Next(1, 5);
sliderEndCount -= 20;
}
return scores;

View File

@ -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();
}
}

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="DeepEqual" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />

View File

@ -4,7 +4,7 @@
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps
}
[UsedImplicitly]
private BeatmapInfo()
protected BeatmapInfo()
{
}

View File

@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r =>
r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
@ -313,6 +314,23 @@ namespace osu.Game.Beatmaps
});
}
public void ResetAllOffsets()
{
const string reset_complete_message = "All offsets have been reset!";
Realm.Write(r =>
{
var items = r.All<BeatmapInfo>();
foreach (var beatmap in items)
{
if (beatmap.UserSettings.Offset != 0)
beatmap.UserSettings.Offset = 0;
}
PostNotification?.Invoke(new ProgressCompletionNotification { Text = reset_complete_message });
});
}
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
{
Realm.Run(r =>

View File

@ -93,8 +93,9 @@ namespace osu.Game.Database
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
/// </summary>
private const int schema_version = 42;
private const int schema_version = 43;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -375,10 +376,6 @@ namespace osu.Game.Database
{
foreach (var beatmap in beatmapSet.Beatmaps)
{
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
foreach (var score in beatmap.Scores)
realm.Remove(score);
realm.Remove(beatmap.Metadata);
realm.Remove(beatmap);
}
@ -1192,6 +1189,21 @@ namespace osu.Game.Database
}
break;
case 43:
{
// Clear default bindings for "Toggle FPS Display",
// as it conflicts with "Convert to Stream" in the editor.
// Only apply change if set to the conflicting bind
// i.e. has been manually rebound by the user.
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var toggleFpsBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleFPSDisplay);
if (toggleFpsBind != null && toggleFpsBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.F }))
migration.NewRealm.Remove(toggleFpsBind);
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -10,7 +10,7 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Database
{
public partial class UserLookupCache : OnlineLookupCache<int, APIUser, GetUsersRequest>
public partial class UserLookupCache : OnlineLookupCache<int, APIUser, LookupUsersRequest>
{
/// <summary>
/// Perform an API lookup on the specified user, populating a <see cref="APIUser"/> model.
@ -28,8 +28,8 @@ namespace osu.Game.Database
/// <returns>The populated users. May include null results for failed retrievals.</returns>
public Task<APIUser?[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray());
protected override LookupUsersRequest CreateRequest(IEnumerable<int> ids) => new LookupUsersRequest(ids.ToArray());
protected override IEnumerable<APIUser>? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
protected override IEnumerable<APIUser>? RetrieveResults(LookupUsersRequest request) => request.Response?.Users;
}
}

View File

@ -5,6 +5,7 @@
using System;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
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="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>
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)
{
}

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface
{
@ -18,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface
/// <param name="text">The text to display.</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>
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)
{
}

View File

@ -101,7 +101,7 @@ namespace osu.Game.Input.Bindings
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.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.J }, GlobalAction.EditorNudgeLeft),
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.T }, GlobalAction.EditorTapForBPM),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
@ -368,8 +369,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))]
ToggleChatFocus,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))]
EditorCycleGridDisplayMode,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridSpacing))]
EditorCycleGridSpacing,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))]
EditorTestGameplay,
@ -472,6 +473,9 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))]
EditorSeekToNextSamplePoint,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridType))]
EditorCycleGridType,
}
public enum GlobalActionCategory

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Are you sure you want to delete all skins? This cannot be undone!"
/// </summary>

View File

@ -190,9 +190,14 @@ namespace osu.Game.Localisation
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
/// <summary>
/// "Cycle grid display mode"
/// "Cycle grid spacing"
/// </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>
/// "Test gameplay"

View File

@ -59,6 +59,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Delete ALL scores"
/// </summary>

View File

@ -79,6 +79,11 @@ namespace osu.Game.Localisation.SkinComponents
/// </summary>
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}";
}
}

View File

@ -49,6 +49,51 @@ namespace osu.Game.Localisation
/// </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.");
/// <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}";
}
}

View File

@ -2,9 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
/// <summary>
/// Looks up users with the given <see cref="UserIds"/>.
/// In comparison to <see cref="LookupUsersRequest"/>, the response here contains <see cref="APIUser.RulesetsStatistics"/>,
/// but in exchange is subject to more stringent rate limiting.
/// </summary>
public class GetUsersRequest : APIRequest<GetUsersResponse>
{
public readonly int[] UserIds;

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
/// <summary>
/// Looks up users with the given <see cref="UserIds"/>.
/// In comparison to <see cref="GetUsersRequest"/>, the response here does not contain <see cref="APIUser.RulesetsStatistics"/>,
/// but in exchange is subject to less stringent rate limiting, making it suitable for mass user listings.
/// </summary>
public class LookupUsersRequest : APIRequest<GetUsersResponse>
{
public readonly int[] UserIds;
private const int max_ids_per_request = 50;
public LookupUsersRequest(int[] userIds)
{
if (userIds.Length > max_ids_per_request)
throw new ArgumentException($"{nameof(LookupUsersRequest)} calls only support up to {max_ids_per_request} IDs at once");
UserIds = userIds;
}
protected override string Target => @"users/lookup/?ids[]=" + string.Join(@"&ids[]=", UserIds);
}
}

View File

@ -261,7 +261,7 @@ namespace osu.Game.Online.API.Requests.Responses
public APIUserHistoryCount[] ReplaysWatchedCounts;
/// <summary>
/// All user statistics per ruleset's short name (in the case of a <see cref="GetUsersRequest"/> response).
/// All user statistics per ruleset's short name (in the case of a <see cref="GetUsersRequest"/> or <see cref="GetMeRequest"/> response).
/// Otherwise empty. Can be altered for testing purposes.
/// </summary>
// todo: this should likely be moved to a separate UserCompact class at some point.

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
@ -10,13 +9,5 @@ namespace osu.Game.Online.Multiplayer
[Serializable]
public class InvalidPasswordException : HubException
{
public InvalidPasswordException()
{
}
protected InvalidPasswordException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
: base($"Cannot change from {oldState} to {newState}")
{
}
protected InvalidStateChangeException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
: base(message)
{
}
protected InvalidStateException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics;
using osu.Game.Database;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
@ -188,7 +189,7 @@ namespace osu.Game.Online.Multiplayer
// Populate users.
Debug.Assert(joinedRoom.Users != null);
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
await PopulateUsers(joinedRoom.Users).ConfigureAwait(false);
// Update the stored room (must be done on update thread for thread-safety).
await runOnUpdateThreadAsync(() =>
@ -416,7 +417,7 @@ namespace osu.Game.Online.Multiplayer
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
{
await PopulateUser(user).ConfigureAwait(false);
await PopulateUsers([user]).ConfigureAwait(false);
Scheduler.Add(() =>
{
@ -803,10 +804,26 @@ namespace osu.Game.Online.Multiplayer
}
/// <summary>
/// Populates the <see cref="APIUser"/> for a given <see cref="MultiplayerRoomUser"/>.
/// Populates the <see cref="APIUser"/> for a given collection of <see cref="MultiplayerRoomUser"/>s.
/// </summary>
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false);
/// <param name="multiplayerUsers">The <see cref="MultiplayerRoomUser"/>s to populate.</param>
protected async Task PopulateUsers(IEnumerable<MultiplayerRoomUser> multiplayerUsers)
{
var request = new GetUsersRequest(multiplayerUsers.Select(u => u.UserID).Distinct().ToArray());
await API.PerformAsync(request).ConfigureAwait(false);
if (request.Response == null)
return;
Dictionary<int, APIUser> users = request.Response.Users.ToDictionary(user => user.Id);
foreach (var multiplayerUser in multiplayerUsers)
{
if (users.TryGetValue(multiplayerUser.UserID, out var user))
multiplayerUser.User = user;
}
}
/// <summary>
/// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
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")
{
}
protected NotHostException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer
: base("This user has not yet joined a multiplayer room.")
{
}
protected NotJoinedRoomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer
: base(MESSAGE)
{
}
protected UserBlockedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer
: base(MESSAGE)
{
}
protected UserBlocksPMsException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -17,19 +17,21 @@ namespace osu.Game.Online.Placeholders
public ClickablePlaceholder(LocalisableString actionMessage, IconUsage icon)
{
OsuAnimatedButton button;
OsuTextFlowContainer textFlow;
AddArbitraryDrawable(new OsuAnimatedButton
AddArbitraryDrawable(button = new OsuAnimatedButton
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Child = textFlow = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE))
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Margin = new Framework.Graphics.MarginPadding(5)
},
Action = () => Action?.Invoke()
});
button.Add(textFlow = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE))
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Margin = new Framework.Graphics.MarginPadding(5)
});
textFlow.AddIcon(icon, i =>
{
i.Padding = new Framework.Graphics.MarginPadding { Right = 10 };

View File

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Online.API;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -56,6 +58,17 @@ namespace osu.Game.Online.Spectator
[Key(6)]
public DateTimeOffset ReceivedTime { get; set; }
/// <summary>
/// The set of mods currently active.
/// </summary>
/// <remarks>
/// Nullable for backwards compatibility with older clients
/// (these structures are also used server-side, and <see langword="null"/> will be used as marker that the data isn't there).
/// can be made non-nullable 20250407
/// </remarks>
[Key(7)]
public APIMod[]? Mods { get; set; }
/// <summary>
/// Construct header summary information from a point-in-time reference to a score which is actively being played.
/// </summary>
@ -69,6 +82,7 @@ namespace osu.Game.Online.Spectator
MaxCombo = score.MaxCombo;
// copy for safety
Statistics = new Dictionary<HitResult, int>(score.Statistics);
Mods = score.APIMods.ToArray();
ScoreProcessorStatistics = statistics;
}

View File

@ -17,9 +17,9 @@ namespace osu.Game.Overlays.BeatmapSet
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
{
if (SearchAction != null)
loaded.AddLink(metadata, () => SearchAction(metadata));
loaded.AddLink(metadata, () => SearchAction($@"source=""""{metadata}"""""));
else
loaded.AddLink(metadata, LinkAction.SearchBeatmapSet, metadata);
loaded.AddLink(metadata, LinkAction.SearchBeatmapSet, $@"source=""""{metadata}""""");
}
}
}

View File

@ -9,7 +9,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -58,9 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
/// <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>
private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>();
private readonly List<LocalisableString> statisticResultNames = new List<LocalisableString>();
private bool showPerformancePoints;
@ -72,7 +73,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
showPerformancePoints = showPerformanceColumn;
statisticResultTypes.Clear();
statisticResultNames.Clear();
for (int i = 0; i < scores.Count; i++)
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();
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;
// for the time being ignore bonus result types.
// 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;
var displayName = ruleset.GetDisplayNameForHitResult(result);
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
columns.Add(new TableColumn(resultGroup.Key, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
statisticResultNames.Add(resultGroup.Key);
}
if (showPerformancePoints)
@ -167,14 +166,25 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
#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))
stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName);
int count = 0;
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)

View File

@ -96,10 +96,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
if (score != null)
{
totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(score);
if (score.Accuracy == 1.0) accuracyColumn.TextColour = colours.GreenLight;
#pragma warning disable CS0618
if (score.MaxCombo == score.BeatmapInfo!.MaxCombo) maxComboColumn.TextColour = colours.GreenLight;
#pragma warning restore CS0618
}
}
private ScoreInfo score;
@ -228,6 +235,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
set => text.Text = value;
}
public Colour4 TextColour
{
set => text.Colour = value;
}
public Drawable Drawable
{
set

View File

@ -9,13 +9,17 @@ using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat.Listing;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Overlays.Chat.ChannelList
{
@ -34,11 +38,12 @@ namespace osu.Game.Overlays.Chat.ChannelList
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private OsuScrollContainer scroll = null!;
private FillFlowContainer groupFlow = null!;
private SearchContainer groupFlow = null!;
private ChannelGroup announceChannelGroup = null!;
private ChannelGroup publicChannelGroup = null!;
private ChannelGroup privateChannelGroup = null!;
private ChannelListItem selector = null!;
private TextBox searchTextBox = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
@ -55,13 +60,23 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.Both,
ScrollbarAnchor = Anchor.TopRight,
ScrollDistance = 35f,
Child = groupFlow = new FillFlowContainer
Child = groupFlow = new SearchContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Top = 8 },
Child = searchTextBox = new ChannelSearchTextBox
{
RelativeSizeAxes = Axes.X,
}
},
announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()),
publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()),
selector = new ChannelListItem(ChannelListingChannel),
@ -71,6 +86,19 @@ namespace osu.Game.Overlays.Chat.ChannelList
},
};
searchTextBox.Current.BindValueChanged(_ => groupFlow.SearchTerm = searchTextBox.Current.Value, true);
searchTextBox.OnCommit += (_, _) =>
{
if (string.IsNullOrEmpty(searchTextBox.Current.Value))
return;
var firstMatchingItem = this.ChildrenOfType<ChannelListItem>().FirstOrDefault(item => item.MatchingFilter);
if (firstMatchingItem == null)
return;
OnRequestSelect?.Invoke(firstMatchingItem.Channel);
};
selector.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
}
@ -168,5 +196,17 @@ namespace osu.Game.Overlays.Chat.ChannelList
};
}
}
private partial class ChannelSearchTextBox : BasicSearchTextBox
{
protected override bool AllowCommit => true;
public ChannelSearchTextBox()
{
const float scale_factor = 0.8f;
Scale = new Vector2(scale_factor);
Width = 1 / scale_factor;
}
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -9,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Overlays.Chat.ChannelList
{
public partial class ChannelListItem : OsuClickableContainer
public partial class ChannelListItem : OsuClickableContainer, IFilterable
{
public event Action<Channel>? OnRequestSelect;
public event Action<Channel>? OnRequestLeave;
@ -186,5 +188,28 @@ namespace osu.Game.Overlays.Chat.ChannelList
}
private bool isSelector => Channel is ChannelListing.ChannelListingChannel;
#region Filtering support
public IEnumerable<LocalisableString> FilterTerms => isSelector ? Enumerable.Empty<LocalisableString>() : [Channel.Name];
private bool matchingFilter = true;
public bool MatchingFilter
{
get => matchingFilter;
set
{
if (matchingFilter == value)
return;
matchingFilter = value;
Alpha = matchingFilter ? 1 : 0;
}
}
public bool FilteringActive { get; set; }
#endregion
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
@ -58,6 +59,21 @@ namespace osu.Game.Overlays.Mods
modState.ValidForSelection.BindValueChanged(_ => updateFilterState());
modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true);
modState.Preselected.BindValueChanged(b =>
{
if (b.NewValue)
{
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = AccentColour,
Hollow = true,
Radius = 2,
};
}
else
Content.EdgeEffect = default;
}, true);
}
protected override void Select()

View File

@ -243,6 +243,9 @@ namespace osu.Game.Overlays.Mods
{
foreach (var column in columnFlow.Columns)
column.SearchTerm = query.NewValue;
if (SearchTextBox.HasFocus)
preselectMod();
}, true);
// Start scrolling from the end, to give the user a sense that
@ -254,6 +257,26 @@ namespace osu.Game.Overlays.Mods
});
}
private void preselectMod()
{
var visibleMods = columnFlow.Columns.OfType<ModColumn>().Where(c => c.IsPresent).SelectMany(c => c.AvailableMods.Where(m => m.Visible));
// Search for an exact acronym or name match, or otherwise default to the first visible mod.
ModState? matchingMod =
visibleMods.FirstOrDefault(m => m.Mod.Acronym.Equals(SearchTerm, StringComparison.OrdinalIgnoreCase) || m.Mod.Name.Equals(SearchTerm, StringComparison.OrdinalIgnoreCase))
?? visibleMods.FirstOrDefault();
var preselectedMod = matchingMod;
foreach (var mod in AllAvailableMods)
mod.Preselected.Value = mod == preselectedMod && SearchTextBox.Current.Value.Length > 0;
}
private void clearPreselection()
{
foreach (var mod in AllAvailableMods)
mod.Preselected.Value = false;
}
public new ModSelectFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as ModSelectFooterContent;
public override VisibilityContainer CreateFooterContent() => new ModSelectFooterContent(this)
@ -383,7 +406,7 @@ namespace osu.Game.Overlays.Mods
{
columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
SearchTextBox.KillFocus();
setTextBoxFocus(false);
}
else
{
@ -590,11 +613,11 @@ namespace osu.Game.Overlays.Mods
return true;
}
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);
var matchingMod = AllAvailableMods.SingleOrDefault(m => m.Preselected.Value);
if (firstMod is not null)
if (matchingMod is not null)
{
firstMod.Active.Value = !firstMod.Active.Value;
matchingMod.Active.Value = !matchingMod.Active.Value;
SearchTextBox.SelectAll();
}
@ -648,9 +671,15 @@ namespace osu.Game.Overlays.Mods
private void setTextBoxFocus(bool focus)
{
if (focus)
{
SearchTextBox.TakeFocus();
preselectMod();
}
else
{
SearchTextBox.KillFocus();
clearPreselection();
}
}
#endregion

View File

@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Mods
/// </summary>
public BindableBool Active { get; } = new BindableBool();
public BindableBool Preselected { get; } = new BindableBool();
/// <summary>
/// Whether the mod requires further customisation.
/// This flag is read by the <see cref="ModSelectOverlay"/> to determine if the customisation panel should be opened after a mod change

View File

@ -376,11 +376,19 @@ namespace osu.Game.Overlays
{
Live<BeatmapSetInfo> result;
var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToArray();
var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList();
if (possibleSets.Length == 0)
if (possibleSets.Count == 0)
return null;
// 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.
@ -410,20 +418,20 @@ namespace osu.Game.Overlays
switch (randomSelectAlgorithm.Value)
{
case RandomSelectAlgorithm.Random:
result = possibleSets[RNG.Next(possibleSets.Length)];
result = possibleSets[RNG.Next(possibleSets.Count)];
break;
case RandomSelectAlgorithm.RandomPermutation:
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray();
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToList();
if (notYetPlayedSets.Length == 0)
if (notYetPlayedSets.Count == 0)
{
notYetPlayedSets = possibleSets;
previousRandomSets.Clear();
randomHistoryDirection = 0;
}
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)];
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)];
break;
default:

View File

@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private SettingsButton deleteBeatmapsButton = null!;
private SettingsButton deleteBeatmapVideosButton = null!;
private SettingsButton resetOffsetsButton = null!;
private SettingsButton restoreButton = null!;
private SettingsButton undeleteButton = null!;
@ -47,6 +48,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}, 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[]
{
restoreButton = new SettingsButton

View File

@ -361,7 +361,7 @@ namespace osu.Game.Overlays.SkinEditor
componentsSidebar.Children = new[]
{
new EditorSidebarSection("Current working layer")
new EditorSidebarSection(SkinEditorStrings.CurrentWorkingLayer)
{
Children = new Drawable[]
{

View File

@ -13,6 +13,7 @@ using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Localisation;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
@ -101,19 +102,19 @@ namespace osu.Game.Overlays.SkinEditor
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) }
};
yield return new OsuMenuItem("Anchor")
yield return new OsuMenuItem(SkinEditorStrings.Anchor)
{
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
.Prepend(closestItem)
.ToArray()
};
yield return originMenu = new OsuMenuItem("Origin");
yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin);
closestItem.State.BindValueChanged(s =>
{
@ -125,19 +126,19 @@ namespace osu.Game.Overlays.SkinEditor
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)
((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)
((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)
{
@ -153,9 +154,9 @@ namespace osu.Game.Overlays.SkinEditor
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();

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Edit
{
internal partial class ExpandableSpriteText : OsuSpriteText, IExpandable
{
public BindableBool Expanded { get; } = new BindableBool();
[Resolved(canBeNull: true)]
private IExpandingContainer? expandingContainer { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
expandingContainer?.Expanded.BindValueChanged(containerExpanded =>
{
Expanded.Value = containerExpanded.NewValue;
}, true);
Expanded.BindValueChanged(expanded =>
{
this.FadeTo(expanded.NewValue ? 1 : 0, 150, Easing.OutQuint);
}, true);
}
}
}

View File

@ -18,6 +18,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
@ -178,16 +179,56 @@ namespace osu.Game.Rulesets.Edit
Spacing = new Vector2(0, 5),
},
},
new EditorToolboxGroup("bank (Shift-Q~R)")
new EditorToolboxGroup("bank (Shift/Alt-Q~R)")
{
Child = sampleBankTogglesCollection = new FillFlowContainer
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new ExpandableSpriteText
{
Text = "Normal",
AlwaysPresent = true,
AllowMultiline = false,
RelativePositionAxes = Axes.X,
X = 0.25f,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 17),
},
new ExpandableSpriteText
{
Text = "Addition",
AlwaysPresent = true,
AllowMultiline = false,
RelativePositionAxes = Axes.X,
X = 0.75f,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 17),
},
}
},
sampleBankTogglesCollection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
}
},
}
},
}
@ -231,7 +272,7 @@ namespace osu.Game.Rulesets.Edit
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b)));
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates).Select(b => new SampleBankTernaryButton(b.First, b.Second)));
SetSelectTool();
@ -362,7 +403,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
if (e.ControlPressed || e.SuperPressed)
return false;
if (checkToolboxMappingFromKey(e.Key, out int leftIndex))
@ -379,14 +420,26 @@ namespace osu.Game.Rulesets.Edit
if (checkToggleMappingFromKey(e.Key, out int rightIndex))
{
var item = e.ShiftPressed
? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex)
: togglesCollection.ElementAtOrDefault(rightIndex);
if (item is DrawableTernaryButton button)
if (e.ShiftPressed || e.AltPressed)
{
button.Button.Toggle();
return true;
if (sampleBankTogglesCollection.ElementAtOrDefault(rightIndex) is SampleBankTernaryButton sampleBankTernaryButton)
{
if (e.ShiftPressed)
sampleBankTernaryButton.NormalButton.Toggle();
if (e.AltPressed)
sampleBankTernaryButton.AdditionsButton.Toggle();
return true;
}
}
else
{
if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
{
button.Button.Toggle();
return true;
}
}
}

View File

@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// Whether the sample addition bank should be taken from the previous hit objects.
/// </summary>
public bool AutomaticAdditionBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
@ -82,7 +87,7 @@ namespace osu.Game.Rulesets.Edit
}
/// <summary>
/// Updates the time and position of this <see cref="HitObjectPlacementBlueprint"/> based on the provided snap information.
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public override void UpdateTimeAndPosition(SnapResult result)
@ -96,26 +101,26 @@ namespace osu.Game.Rulesets.Edit
}
var lastHitObject = getPreviousHitObject();
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (AutomaticBankAssignment)
if (AutomaticAdditionBankAssignment)
{
// Create samples based on the sample settings of the previous hit object
if (lastHitObject != null)
{
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name);
}
// Inherit the addition bank from the previous hit object
// If there is no previous addition, inherit from the normal sample
var lastAddition = lastHitObject?.Samples?.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL) ?? lastHitNormal;
if (lastAddition != null)
HitObject.Samples = HitObject.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: lastAddition.Bank) : s).ToList();
}
else
{
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
{
// Only inherit the volume from the previous hit object
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
}
if (lastHitNormal != null)
{
if (AutomaticBankAssignment)
// Inherit the bank from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: lastHitNormal.Bank) : s).ToList();
// Inherit the volume from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.With(newVolume: lastHitNormal.Volume)).ToList();
}
if (HitObject is IHasRepeats hasRepeats)

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods
private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!;
public void Update(Playfield playfield)
public virtual void Update(Playfield playfield)
{
playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
}

View File

@ -65,22 +65,20 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public override Playfield Playfield => playfield.Value;
public override PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => playfieldAdjustmentContainer;
public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IAdjustableAudioComponent Audio => audioContainer;
private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both };
/// <summary>
/// A container which encapsulates the <see cref="Playfield"/> and provides any adjustments to
/// ensure correct scale and position.
/// </summary>
public virtual PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; private set; }
public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IFrameStableClock FrameStableClock => frameStabilityContainer;
private readonly PlayfieldAdjustmentContainer playfieldAdjustmentContainer;
private bool allowBackwardsSeeks;
public override bool AllowBackwardsSeeks
@ -146,6 +144,7 @@ namespace osu.Game.Rulesets.UI
RelativeSizeAxes = Axes.Both;
KeyBindingInputManager = CreateInputManager();
playfieldAdjustmentContainer = CreatePlayfieldAdjustmentContainer();
playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
{
p.NewResult += (_, r) => NewResult?.Invoke(r);
@ -197,8 +196,7 @@ namespace osu.Game.Rulesets.UI
audioContainer.WithChild(KeyBindingInputManager
.WithChildren(new Drawable[]
{
PlayfieldAdjustmentContainer = CreatePlayfieldAdjustmentContainer()
.WithChild(Playfield),
playfieldAdjustmentContainer.WithChild(Playfield),
Overlays
})),
}
@ -456,6 +454,12 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public abstract Playfield Playfield { get; }
/// <summary>
/// A container which encapsulates the <see cref="Playfield"/> and provides any adjustments to
/// ensure correct scale and position.
/// </summary>
public abstract PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; }
/// <summary>
/// Content to be placed above hitobjects. Will be affected by frame stability and adjustments applied to <see cref="Audio"/>.
/// </summary>

View File

@ -165,7 +165,7 @@ namespace osu.Game.Scoring
}
[UsedImplicitly] // Realm
private ScoreInfo()
protected ScoreInfo()
{
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
private Color4 selectedBackgroundColour;
private Color4 selectedIconColour;
private Drawable icon = null!;
protected Drawable Icon { get; private set; } = null!;
public readonly TernaryButton Button;
@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
defaultIconColour = defaultBackgroundColour.Darken(0.5f);
selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
Add(Icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
@ -75,17 +75,17 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
icon.Colour = selectedIconColour.Darken(0.5f);
Icon.Colour = selectedIconColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
icon.Colour = defaultIconColour;
Icon.Colour = defaultIconColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
icon.Colour = selectedIconColour;
Icon.Colour = selectedIconColour;
BackgroundColour = selectedBackgroundColour;
break;
}

View File

@ -0,0 +1,78 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public partial class SampleBankTernaryButton : CompositeDrawable
{
public readonly TernaryButton NormalButton;
public readonly TernaryButton AdditionsButton;
public SampleBankTernaryButton(TernaryButton normalButton, TernaryButton additionsButton)
{
NormalButton = normalButton;
AdditionsButton = additionsButton;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.5f,
Padding = new MarginPadding { Right = 1 },
Child = new InlineDrawableTernaryButton(NormalButton),
},
new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.5f,
Padding = new MarginPadding { Left = 1 },
Child = new InlineDrawableTernaryButton(AdditionsButton),
},
};
}
private partial class InlineDrawableTernaryButton : DrawableTernaryButton
{
public InlineDrawableTernaryButton(TernaryButton button)
: base(button)
{
}
[BackgroundDependencyLoader]
private void load()
{
Content.Masking = false;
Content.CornerRadius = 0;
Icon.X = 4.5f;
}
protected override SpriteText CreateText() => new ExpandableSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 31f
};
}
}
}

View File

@ -32,7 +32,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
protected DragBox DragBox { get; private set; }
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
public SelectionBlueprintContainer SelectionBlueprints { get; private set; }
public partial class SelectionBlueprintContainer : Container<SelectionBlueprint<T>>
{
public new virtual void ChangeChildDepth(SelectionBlueprint<T> child, float newDepth) => base.ChangeChildDepth(child, newDepth);
}
public SelectionHandler<T> SelectionHandler { get; private set; }
@ -95,7 +100,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
});
}
protected virtual Container<SelectionBlueprint<T>> CreateSelectionBlueprintContainer() => new Container<SelectionBlueprint<T>> { RelativeSizeAxes = Axes.Both };
protected virtual SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
/// <summary>
/// Creates a <see cref="Components.SelectionHandler{T}"/> which outlines items and handles movement of selections.

View File

@ -65,7 +65,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void load()
{
MainTernaryStates = CreateTernaryButtons().ToArray();
SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray();
SampleBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionBankStates).ToArray();
SampleAdditionBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionAdditionBankStates).ToArray();
AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
{
@ -91,6 +92,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var kvp in SelectionHandler.SelectionBankStates)
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
foreach (var kvp in SelectionHandler.SelectionAdditionBankStates)
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
}
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
@ -179,6 +183,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var kvp in SelectionHandler.SelectionBankStates)
bankChanged(kvp.Key, kvp.Value.Value);
foreach (var kvp in SelectionHandler.SelectionAdditionBankStates)
additionBankChanged(kvp.Key, kvp.Value.Value);
}
private void sampleChanged(string sampleName, TernaryState state)
@ -210,7 +217,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
CurrentHitObjectPlacement.AutomaticBankAssignment = state == TernaryState.True;
else if (state == TernaryState.True)
CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
}
private void additionBankChanged(string bankName, TernaryState state)
{
if (CurrentHitObjectPlacement == null) return;
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
CurrentHitObjectPlacement.AutomaticAdditionBankAssignment = state == TernaryState.True;
else if (state == TernaryState.True)
CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
@ -222,6 +239,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public TernaryButton[] SampleBankTernaryStates { get; private set; }
public TernaryButton[] SampleAdditionBankTernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
@ -234,36 +253,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => GetIconForSample(kvp.Key));
}
private IEnumerable<TernaryButton> createSampleBankTernaryButtons()
private IEnumerable<TernaryButton> createSampleBankTernaryButtons(Dictionary<string, Bindable<TernaryState>> sampleBankStates)
{
foreach (var kvp in SelectionHandler.SelectionBankStates)
foreach (var kvp in sampleBankStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key));
}
private Drawable getIconForBank(string sampleName)
{
return new Container
return new OsuSpriteText
{
Size = new Vector2(30, 20),
Children = new Drawable[]
{
new SpriteIcon
{
Size = new Vector2(8),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.VolumeOff
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
X = 10,
Y = -1,
Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20),
Text = $"{char.ToUpperInvariant(sampleName.First())}"
}
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -1,
Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20),
Text = $"{char.ToUpperInvariant(sampleName.First())}"
};
}

View File

@ -9,7 +9,6 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
@ -136,7 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.ApplySelectionOrder(blueprints)
.OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime)));
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new EditorSelectionHandler();

View File

@ -59,6 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionBankStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// The state of each sample addition bank type for all selected hitobjects.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionAdditionBankStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
@ -91,7 +96,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Never remove a sample bank.
// These are basically radio buttons, not toggles.
if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName)))
if (SelectedItems.All(h => h.Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName)))
bindable.Value = TernaryState.True;
}
@ -128,8 +133,84 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBankStates[bankName] = bindable;
}
foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable<TernaryState>
{
Description = bankName.Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
if (SelectedItems.Count == 0)
{
// Ensure that if this is the last selected bank, it should remain selected.
if (SelectionAdditionBankStates.Values.All(b => b.Value == TernaryState.False))
bindable.Value = TernaryState.True;
}
else
{
// Auto should never apply when there is a selection made.
if (bankName == HIT_BANK_AUTO)
break;
// Completely empty selections should be allowed in the case that none of the selected objects have any addition samples.
// This is also required to stop a bindable feedback loop when a HitObject has zero addition samples (and LINQ `All` below becomes true).
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL)))
break;
// Never remove a sample bank.
// These are basically radio buttons, not toggles.
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName)))
bindable.Value = TernaryState.True;
}
break;
case TernaryState.True:
if (SelectedItems.Count == 0)
{
// Ensure the user can't stack multiple bank selections when there's no hitobject selection.
// Note that in normal scenarios this is sorted out by the feedback from applying the bank to the selected objects.
foreach (var other in SelectionAdditionBankStates.Values)
{
if (other != bindable)
other.Value = TernaryState.False;
}
}
else
{
// Auto should just not apply if there's a selection already made.
// Maybe we could make it a disabled button in the future, but right now the editor buttons don't support disabled state.
if (bankName == HIT_BANK_AUTO)
{
bindable.Value = TernaryState.False;
break;
}
// If none of the selected objects have any addition samples, we should not apply the addition bank.
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL)))
{
bindable.Value = TernaryState.False;
break;
}
SetSampleAdditionBank(bankName);
}
break;
}
};
SelectionAdditionBankStates[bankName] = bindable;
}
// start with normal selected.
SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
SelectionAdditionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
foreach (string sampleName in HitSampleInfo.AllAdditions)
{
@ -187,18 +268,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach ((string bankName, var bindable) in SelectionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s), h => h.Bank == bankName);
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name == HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName);
}
IEnumerable<IList<HitSampleInfo>> enumerateAllSamples(HitObject hitObject)
foreach ((string bankName, var bindable) in SelectionAdditionBankStates)
{
yield return hitObject.Samples;
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName);
}
}
if (hitObject is IHasRepeats withRepeats)
{
foreach (var node in withRepeats.NodeSamples)
yield return node;
}
private IEnumerable<IList<HitSampleInfo>> enumerateAllSamples(HitObject hitObject)
{
yield return hitObject.Samples;
if (hitObject is IHasRepeats withRepeats)
{
foreach (var node in withRepeats.NodeSamples)
yield return node;
}
}
@ -214,12 +300,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
bool hasRelevantBank(HitObject hitObject)
{
bool result = hitObject.Samples.All(s => s.Bank == bankName);
bool result = hitObject.Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName);
if (hitObject is IHasRepeats hasRepeats)
{
foreach (var node in hasRepeats.NodeSamples)
result &= node.All(s => s.Bank == bankName);
result &= node.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName);
}
return result;
@ -233,12 +319,51 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (hasRelevantBank(h))
return;
h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList();
h.Samples = h.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
if (h is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i)
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.With(newBank: bankName)).ToList();
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
}
EditorBeatmap.Update(h);
});
}
/// <summary>
/// Sets the sample addition bank for all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="bankName">The name of the sample bank.</param>
public void SetSampleAdditionBank(string bankName)
{
bool hasRelevantBank(HitObject hitObject)
{
bool result = hitObject.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName);
if (hitObject is IHasRepeats hasRepeats)
{
foreach (var node in hasRepeats.NodeSamples)
result &= node.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName);
}
return result;
}
if (SelectedItems.All(hasRelevantBank))
return;
EditorBeatmap.PerformOnSelection(h =>
{
if (enumerateAllSamples(h).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName))
return;
h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
if (h is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i)
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
}
EditorBeatmap.Update(h);
@ -415,6 +540,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
State = { BindTarget = drum },
Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.R))
};
yield return new OsuMenuItem("Addition bank")
{
Items = SelectionAdditionBankStates.Select(kvp =>
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
};
}
#endregion

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -14,7 +13,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// A container for <see cref="SelectionBlueprint{HitObject}"/> ordered by their <see cref="HitObject"/> start times.
/// </summary>
public sealed partial class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint<HitObject>>
public sealed partial class HitObjectOrderedSelectionContainer : BlueprintContainer<HitObject>.SelectionBlueprintContainer
{
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
@ -28,16 +27,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
public override void Add(SelectionBlueprint<HitObject> drawable)
{
SortInternal();
Sort();
base.Add(drawable);
}
public override bool Remove(SelectionBlueprint<HitObject> drawable, bool disposeImmediately)
{
SortInternal();
Sort();
return base.Remove(drawable, disposeImmediately);
}
internal void Sort() => SortInternal();
protected override int Compare(Drawable x, Drawable y)
{
var xObj = ((SelectionBlueprint<HitObject>)x).Item;

View File

@ -438,20 +438,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed || !checkRightToggleFromKey(e.Key, out int rightIndex))
if (e.ControlPressed || e.SuperPressed || !checkRightToggleFromKey(e.Key, out int rightIndex))
return base.OnKeyDown(e);
if (e.ShiftPressed)
if (e.ShiftPressed || e.AltPressed)
{
string? newBank = banks.ElementAtOrDefault(rightIndex);
if (string.IsNullOrEmpty(newBank))
return true;
setBank(newBank);
updatePrimaryBankState();
setAdditionBank(newBank);
updateAdditionBankState();
if (e.ShiftPressed)
{
setBank(newBank);
updatePrimaryBankState();
}
if (e.AltPressed)
{
setAdditionBank(newBank);
updateAdditionBankState();
}
}
else
{

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override bool OnDragStart(DragStartEvent e)
{
@ -287,14 +287,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
protected partial class TimelineSelectionBlueprintContainer : Container<SelectionBlueprint<HitObject>>
protected partial class TimelineSelectionBlueprintContainer : SelectionBlueprintContainer
{
protected override Container<SelectionBlueprint<HitObject>> Content { get; }
protected override HitObjectOrderedSelectionContainer Content { get; }
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
public override void ChangeChildDepth(SelectionBlueprint<HitObject> child, float newDepth)
{
// timeline blueprint container also contains a blueprint for current placement, if present
// (see `placementChanged()` callback above).
// because the current placement hitobject is generally going to be mutated during the placement,
// it is possible for `Content`'s children to become unsorted when the user moves the placement around,
// which can culminate in a critical failure when attempting to binary-search children here
// using `HitObjectOrderedSelectionContainer`'s custom comparer.
// thus, always force a re-sort of objects before attempting to change child depth to avoid this scenario.
Content.Sort();
base.ChangeChildDepth(child, newDepth);
}
}
}
}

View File

@ -1098,8 +1098,12 @@ namespace osu.Game.Screens.Edit
private void seekControlPoint(int direction)
{
var found = direction < 1
? editorBeatmap.ControlPointInfo.AllControlPoints.LastOrDefault(p => p.Time < clock.CurrentTime)
// In the case of a backwards seek while playing, it can be hard to jump before a timing point.
// Adding some lenience here makes it more user-friendly.
double seekLenience = clock.IsRunning ? 1000 * ((IAdjustableClock)clock).Rate : 0;
ControlPoint found = direction < 1
? editorBeatmap.ControlPointInfo.AllControlPoints.LastOrDefault(p => p.Time < clock.CurrentTime - seekLenience)
: editorBeatmap.ControlPointInfo.AllControlPoints.FirstOrDefault(p => p.Time > clock.CurrentTime);
if (found != null)

View File

@ -13,6 +13,7 @@ using osu.Framework.Layout;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Rulesets.Judgements;
using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts;
using osu.Game.Skinning;
@ -33,7 +34,7 @@ namespace osu.Game.Screens.Play.HUD
Precision = 1
};
[SettingSource("Use relative size")]
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))]
public BindableBool UseRelativeSize { get; } = new BindableBool(true);
private ArgonHealthDisplayBar mainBar = null!;

View File

@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
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))]
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
@ -99,6 +102,11 @@ namespace osu.Game.Screens.Play.HUD
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), 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)

View File

@ -37,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
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))]
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
@ -83,6 +86,11 @@ namespace osu.Game.Screens.Play.HUD
private void load(OsuColour colours)
{
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()

View File

@ -299,7 +299,7 @@ namespace osu.Game.Screens.Play.HUD
{
case GlobalAction.Back:
if (!pendingAnimation)
BeginConfirm();
Confirm();
return true;
case GlobalAction.PauseGameplay:
@ -307,7 +307,7 @@ namespace osu.Game.Screens.Play.HUD
if (ReplayLoaded.Value) return false;
if (!pendingAnimation)
BeginConfirm();
Confirm();
return true;
}

View File

@ -252,7 +252,7 @@ namespace osu.Game.Screens.Play
PlayfieldSkinLayer.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft);
PlayfieldSkinLayer.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
PlayfieldSkinLayer.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
PlayfieldSkinLayer.Rotation = drawableRuleset.Playfield.Rotation;
PlayfieldSkinLayer.Rotation = drawableRuleset.PlayfieldAdjustmentContainer.Rotation;
}
float? lowestTopScreenSpaceLeft = null;

View File

@ -455,7 +455,19 @@ namespace osu.Game.Screens.Play
MetadataInfo.Loading = true;
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))
{

View File

@ -322,6 +322,11 @@ namespace osu.Game.Screens.Select
{
try
{
// To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions.
// When an update occurs, the previous beatmap set is either soft or hard deleted.
// Check if the current selection was potentially deleted by re-querying its validity.
bool selectedSetMarkedDeleted = SelectedBeatmapSet != null && fetchFromID(SelectedBeatmapSet.ID)?.DeletePending != false;
foreach (var set in setsRequiringRemoval) removeBeatmapSet(set.ID);
foreach (var set in setsRequiringUpdate) updateBeatmapSet(set);
@ -331,11 +336,6 @@ namespace osu.Game.Screens.Select
// If SelectedBeatmapInfo is non-null, the set should also be non-null.
Debug.Assert(SelectedBeatmapSet != null);
// To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions.
// When an update occurs, the previous beatmap set is either soft or hard deleted.
// Check if the current selection was potentially deleted by re-querying its validity.
bool selectedSetMarkedDeleted = fetchFromID(SelectedBeatmapSet.ID)?.DeletePending != false;
if (selectedSetMarkedDeleted && setsRequiringUpdate.Any())
{
// If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices.

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.Select
public BeatmapDeleteDialog(BeatmapSetInfo beatmapSet)
{
this.beatmapSet = beatmapSet;
BodyText = $@"{beatmapSet.Metadata.Artist} - {beatmapSet.Metadata.Title}";
BodyText = beatmapSet.Metadata.GetDisplayTitleRomanisable(false);
}
[BackgroundDependencyLoader]

View File

@ -66,6 +66,8 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length);
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.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);

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