1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-07 15:24:59 +08:00

Compare commits

...

52 Commits

60 changed files with 1741 additions and 699 deletions
@@ -5,8 +5,8 @@ on:
types: [opened]
permissions:
issues: write # to read the labels of any linked issue(s), and to put the found labels if any on the PR
# not granting any `pull_requests` permissions because in github's modeling pull requests are a subset of issues. it's confusing.
issues: read # to read the labels of any linked issue(s)
pull-requests: write # to put the found labels if any on the PR
jobs:
copy-labels:
@@ -3,10 +3,13 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
@@ -30,6 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathType.LINEAR,
new Vector2(100, 0),
new Vector2(100, 100)
),
createPathSegment(
PathType.PERFECT_CURVE,
new Vector2(100.009f, -50.0009f),
new Vector2(200.0089f, -100)
),
createPathSegment(
PathType.PERFECT_CURVE,
new Vector2(25, -50),
new Vector2(100, 75)
)
};
@@ -48,9 +61,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[TestCase(0, 250)]
[TestCase(0, 200)]
[TestCase(1, 120)]
[TestCase(1, 80)]
public void TestSliderReversal(int pathIndex, double length)
[TestCase(1, 120, false, false)]
[TestCase(1, 80, false, false)]
[TestCase(2, 250)]
[TestCase(2, 190)]
[TestCase(3, 250)]
[TestCase(3, 190)]
public void TestSliderReversal(int pathIndex, double length, bool assertEqualDistances = true, bool assertSliderReduction = true)
{
var controlPoints = paths[pathIndex];
@@ -90,6 +107,215 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
InputManager.ReleaseKey(Key.LControl);
});
if (pathIndex == 2)
{
AddRepeatStep("Reverse slider again", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
}, 2);
}
if (assertEqualDistances)
{
AddAssert("Middle control point has the same distance from start to end", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[0].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
}
AddAssert("Middle control point is not at start or end", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldStartPos) > 1 &&
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldEndPos) > 1
);
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
AddAssert("Slider has correct start position", () =>
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
AddAssert("Slider has correct end position", () =>
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
AddAssert("Control points have correct types", () =>
{
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
});
if (assertSliderReduction)
{
AddStep("Move to marker", () =>
{
var marker = this.ChildrenOfType<SliderEndDragMarker>().Single();
var markerPos = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
// sometimes the cursor may miss the marker's hitbox so we
// add a little offset here to be sure it lands in a clickable position.
var position = new Vector2(markerPos.X + 2f, markerPos.Y);
InputManager.MoveMouseTo(position);
});
AddStep("Click", () => InputManager.PressButton(MouseButton.Left));
AddStep("Reduce slider", () =>
{
var middleControlPoint = this.ChildrenOfType<PathControlPointPiece<Slider>>().ToArray()[^2];
InputManager.MoveMouseTo(middleControlPoint);
});
AddStep("Release click", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("Save half slider info", () =>
{
oldStartPos = selectedSlider.Position;
oldEndPos = selectedSlider.EndPosition;
oldDistance = selectedSlider.Path.Distance;
});
AddStep("Reverse slider", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("Middle control point has the same distance from start to end", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[0].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
AddAssert("Middle control point is not at start or end", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldStartPos) > 1 &&
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldEndPos) > 1
);
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
AddAssert("Slider has correct start position", () =>
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
AddAssert("Slider has correct end position", () =>
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
AddAssert("Control points have correct types", () =>
{
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
});
}
}
[Test]
public void TestSegmentedSliderReversal()
{
PathControlPoint[] segmentedSliderPath =
[
new PathControlPoint
{
Position = new Vector2(0, 0),
Type = PathType.PERFECT_CURVE
},
new PathControlPoint
{
Position = new Vector2(100, 150),
},
new PathControlPoint
{
Position = new Vector2(75, -50),
Type = PathType.PERFECT_CURVE
},
new PathControlPoint
{
Position = new Vector2(225, -75),
},
new PathControlPoint
{
Position = new Vector2(350, 50),
Type = PathType.PERFECT_CURVE
},
new PathControlPoint
{
Position = new Vector2(500, -75),
},
new PathControlPoint
{
Position = new Vector2(350, -120),
},
];
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = default;
var oldControlPointTypes = segmentedSliderPath.Select(p => p.Type);
AddStep("Add slider", () =>
{
var slider = new Slider
{
Position = new Vector2(0, 200),
Path = new SliderPath(segmentedSliderPath)
{
ExpectedDistance = { Value = 1314 }
}
};
EditorBeatmap.Add(slider);
oldStartPos = slider.Position;
oldEndPos = slider.EndPosition;
oldDistance = slider.Path.Distance;
});
AddStep("Select slider", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects[0];
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddRepeatStep("Reverse slider", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
}, 3);
AddAssert("First arc's control is not at the slider's middle", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, selectedSlider.Path.PositionAt(0.5)) > 1
);
AddAssert("Last arc's control is not at the slider's middle", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[1].Position, selectedSlider.Path.PositionAt(0.5)) > 1
);
AddAssert("First arc centered middle control point", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[1].Position, pathControlPoints[0].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[1].Position, pathControlPoints[2].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
AddAssert("Last arc centered middle control point", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^3].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
@@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModEasy : OsuModTestScene
{
protected override bool AllowFail => true;
[Test]
public void TestMultipleApplication()
{
bool reapplied = false;
CreateModTest(new ModTestData
{
Mods = [new OsuModEasy { Retries = { Value = 1 } }],
Autoplay = false,
CreateBeatmap = () =>
{
// do stuff to speed up fails
var b = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
Difficulty = { DrainRate = 10 }
};
foreach (var ho in b.HitObjects)
ho.StartTime /= 4;
return b;
},
PassCondition = () =>
{
if (((ModEasyTestPlayer)Player).FailuresSuppressed > 0 && !reapplied)
{
try
{
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(new BeatmapDifficulty());
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToPlayer>())
mod.ApplyToPlayer(Player);
}
catch
{
// don't care if this fails. in fact a failure here is probably better than the alternative.
}
finally
{
reapplied = true;
}
}
return Player.GameplayState.HasFailed && ((ModEasyTestPlayer)Player).FailuresSuppressed <= 1;
}
});
}
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModEasyTestPlayer(CurrentTestData, AllowFail);
private partial class ModEasyTestPlayer : ModTestPlayer
{
public int FailuresSuppressed { get; private set; }
public ModEasyTestPlayer(ModTestData data, bool allowFail)
: base(data, allowFail)
{
}
protected override bool CheckModsAllowFailure()
{
bool failureAllowed = GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
if (!failureAllowed)
FailuresSuppressed++;
return failureAllowed;
}
}
}
}
@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
@@ -37,13 +38,16 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
private void load()
{
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
}
@@ -53,15 +57,22 @@ namespace osu.Game.Rulesets.Osu.Edit
base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState();
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
updateState();
}
private void hitObjectUpdated(HitObject hitObject)
{
if (selectedMovableObjects.Contains(hitObject))
updateState();
}
private void updateState()
{
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanScaleX.Value = quad.Width > 0;
CanScaleY.Value = quad.Height > 0;
CanScaleX.Value = Precision.DefinitelyBigger(quad.Width, 0);
CanScaleY.Value = Precision.DefinitelyBigger(quad.Height, 0);
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any();
IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider;
@@ -339,5 +350,13 @@ namespace osu.Game.Rulesets.Osu.Edit
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorBeatmap.IsNotNull())
editorBeatmap.HitObjectUpdated -= hitObjectUpdated;
}
}
}
@@ -132,6 +132,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect point", () => EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
{
new HitCircle
@@ -185,6 +186,7 @@ namespace osu.Game.Tests.Visual.Editing
var effectPoint = EditorBeatmap.ControlPointInfo.EffectPoints.Single();
return effectPoint.Time == 500 && effectPoint.KiaiMode && effectPoint.ScrollSpeedBindable.IsDefault;
});
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
@@ -223,6 +225,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("add effect points", () =>
{
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
@@ -253,6 +256,8 @@ namespace osu.Game.Tests.Visual.Editing
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("created difficulty has effect points", () =>
{
return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[]
@@ -284,6 +289,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("add effect points", () =>
{
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
@@ -311,6 +317,8 @@ namespace osu.Game.Tests.Visual.Editing
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("created difficulty has effect points", () =>
{
// since this difficulty is on another ruleset, scroll speed specifications are completely reset,
@@ -344,6 +352,7 @@ namespace osu.Game.Tests.Visual.Editing
StartTime = 1000
}
}));
AddStep("add bookmarks", () => EditorBeatmap.Bookmarks.AddRange([500, 1000]));
AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4);
AddStep("set combo colours", () =>
{
@@ -394,6 +403,7 @@ namespace osu.Game.Tests.Visual.Editing
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2);
AddAssert("created difficulty has bookmarks", () => EditorBeatmap.Bookmarks.Count == 2);
AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);
@@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
@@ -112,6 +113,77 @@ namespace osu.Game.Tests.Visual.Editing
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestAutoAdditionsBankMatchesNormalBankWhenChangedViaPopover()
{
clickSamplePiece(0);
setBankViaPopover(HitSampleInfo.BANK_SOFT);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
toggleAdditionViaPopover(1);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
setAdditionBankViaPopover(HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_NORMAL);
setAdditionBankViaPopover(EditorSelectionHandler.HIT_BANK_AUTO);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestAutoAdditionsBankMatchesNormalBankWhenChangedViaHotkeys()
{
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
AddStep("set soft normal bank", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.E);
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
AddStep("toggle finish", () => InputManager.Key(Key.E));
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
AddStep("set drum normal bank", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
AddStep("set normal addition bank", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.AltLeft);
});
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_NORMAL);
AddStep("set auto addition bank", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.AltLeft);
});
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestUndo()
{
@@ -346,7 +418,7 @@ namespace osu.Game.Tests.Visual.Editing
{
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT, newEditorAutoBank: false);
}
}
});
@@ -354,7 +426,7 @@ 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));
h.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT, editorAutoBank: false));
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@@ -523,6 +595,172 @@ namespace osu.Game.Tests.Visual.Editing
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
}
[Test]
public void TestNonAutoBankHotkeysDuringPlacementPersistAfterPlacement()
{
AddStep("Clear all objects", () => EditorBeatmap.Clear());
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
AddStep("Move to 3000", () => EditorClock.Seek(3000));
AddStep("Press drum bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert($"Placement sample is {HitSampleInfo.BANK_DRUM}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(HitSampleInfo.BANK_DRUM));
AddStep("Press normal addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.AltLeft);
});
AddStep("Press finish sample shortcut", () =>
{
InputManager.Key(Key.E);
});
AddAssert($"Placement sample addition is {HitSampleInfo.BANK_NORMAL}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(HitSampleInfo.BANK_NORMAL));
AddStep("Finish placement", () => InputManager.Click(MouseButton.Left));
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasAutoNormalBankFlag(0, false);
hitObjectHasAutoAdditionBankFlag(0, false);
clickSamplePiece(0);
samplePopoverIsOpen();
samplePopoverHasSingleAdditionBank(HitSampleInfo.BANK_NORMAL);
}
[Test]
public void TestAutoAdditionBankHotkeyDuringPlacementPersistsAfterPlacement()
{
AddStep("Clear all objects", () => EditorBeatmap.Clear());
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
AddStep("Move to 3000", () => EditorClock.Seek(3000));
AddStep("Press drum bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert($"Placement sample is {HitSampleInfo.BANK_DRUM}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(HitSampleInfo.BANK_DRUM));
AddStep("Press normal addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.AltLeft);
});
AddStep("Press finish sample shortcut", () =>
{
InputManager.Key(Key.E);
});
AddStep("Press auto addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.AltLeft);
});
AddAssert($"Placement sample addition is {HitSampleInfo.BANK_DRUM}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(HitSampleInfo.BANK_DRUM));
AddStep("Finish placement", () => InputManager.Click(MouseButton.Left));
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasAutoNormalBankFlag(0, false);
hitObjectHasAutoAdditionBankFlag(0, true);
clickSamplePiece(0);
samplePopoverIsOpen();
samplePopoverHasSingleAdditionBank(EditorSelectionHandler.HIT_BANK_AUTO);
}
[Test]
public void TestFullAutoBankHotkeyDuringPlacementPersistsAfterPlacement()
{
AddStep("Clear all objects", () => EditorBeatmap.Clear());
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
AddStep("Move to 3000", () => EditorClock.Seek(3000));
AddStep("Press auto normal bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert($"Placement sample is {HitSampleInfo.BANK_NORMAL}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(HitSampleInfo.BANK_NORMAL));
AddStep("Press finish sample shortcut", () =>
{
InputManager.Key(Key.E);
});
AddStep("Press auto addition bank shortcut", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.AltLeft);
});
AddAssert($"Placement sample addition is {HitSampleInfo.BANK_NORMAL}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(HitSampleInfo.BANK_NORMAL));
AddStep("Finish placement", () => InputManager.Click(MouseButton.Left));
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasAutoNormalBankFlag(0, false); // it's the first object - nothing to inherit bank from
hitObjectHasAutoAdditionBankFlag(0, true);
clickSamplePiece(0);
samplePopoverIsOpen();
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleAdditionBank(EditorSelectionHandler.HIT_BANK_AUTO);
dismissPopover();
AddStep("Move to 5000", () => EditorClock.Seek(5000));
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
AddStep("Finish placement", () => InputManager.Click(MouseButton.Left));
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); // finish is still implied, continuing from first placement
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_NORMAL);
hitObjectHasAutoNormalBankFlag(1, true);
hitObjectHasAutoAdditionBankFlag(1, true);
clickSamplePiece(1);
samplePopoverIsOpen();
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleAdditionBank(EditorSelectionHandler.HIT_BANK_AUTO);
}
[Test]
public void PopoverForMultipleSelectionChangesAllSamples()
{
@@ -599,19 +837,19 @@ namespace osu.Game.Tests.Visual.Editing
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, editorAutoBank: false)
},
NodeSamples = new List<IList<HitSampleInfo>>
{
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM, editorAutoBank: false),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM, editorAutoBank: false),
},
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, editorAutoBank: false),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT, editorAutoBank: false),
},
}
});
@@ -880,6 +1118,14 @@ namespace osu.Game.Tests.Visual.Editing
return dropdown?.Current.Value == "(multiple)";
});
private void samplePopoverHasSingleAdditionBank(string bank) => AddUntilStep($"sample popover has bank {bank}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var dropdown = popover?.ChildrenOfType<LabelledDropdown<string>>().ElementAt(1);
return dropdown?.Current.Value == bank;
});
private void dismissPopover()
{
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
@@ -953,6 +1199,18 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void hitObjectHasAutoNormalBankFlag(int objectIndex, bool autoBank) => AddAssert($"{objectIndex.ToOrdinalWords()} has auto normal bank {(autoBank ? "on" : "off")}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.EditorAutoBank == autoBank);
});
private void hitObjectHasAutoAdditionBankFlag(int objectIndex, bool autoBank) => AddAssert($"{objectIndex.ToOrdinalWords()} has auto addition bank {(autoBank ? "on" : "off")}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.EditorAutoBank == autoBank);
});
private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert(
$"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () =>
{
@@ -272,8 +272,8 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[1].Samples, () => Has.Count.EqualTo(2));
AddAssert("normal sample has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank,
() => Is.EqualTo(HitSampleInfo.BANK_SOFT));
AddAssert("clap sample has drum bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_CLAP).Bank,
() => Is.EqualTo(HitSampleInfo.BANK_DRUM));
AddAssert("clap sample has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_CLAP).Bank,
() => Is.EqualTo(HitSampleInfo.BANK_SOFT));
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
AddStep("seek to 1000", () => EditorClock.Seek(1000)); // previous object is the one at time 500, which has no additions
@@ -27,7 +27,7 @@ using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestScenePlaylistsSongSelectV2 : OnlinePlayTestScene
public partial class TestScenePlaylistsSongSelect : OnlinePlayTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager manager = null!;
@@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
rulesets.Dispose();
}
private partial class TestPlaylistsSongSelect : PlaylistsSongSelectV2
private partial class TestPlaylistsSongSelect : PlaylistsSongSelect
{
public new IBindable<bool> Freestyle => base.Freestyle;
@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("edit playlist", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for song select", () => playlistScreen.CurrentSubScreen is PlaylistsSongSelectV2 songSelect && songSelect.IsLoaded && !songSelect.IsFiltering);
AddUntilStep("wait for song select", () => playlistScreen.CurrentSubScreen is PlaylistsSongSelect songSelect && songSelect.IsLoaded && !songSelect.IsFiltering);
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
@@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => playlistScreen.CurrentSubScreen is PlaylistsSongSelectV2 songSelect && songSelect.IsLoaded && !songSelect.IsFiltering);
AddUntilStep("wait for song select", () => playlistScreen.CurrentSubScreen is PlaylistsSongSelect songSelect && songSelect.IsLoaded && !songSelect.IsFiltering);
AddStep("press home button", () =>
{
@@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
base.SetUpSteps();
AddStep("add tray", () => Child = new PlaylistsSongSelectV2.PlaylistTray(room = new Room())
AddStep("add tray", () => Child = new PlaylistsSongSelect.PlaylistTray(room = new Room())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
@@ -13,11 +13,11 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -30,10 +30,12 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Playlists
{
@@ -215,6 +217,85 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("second beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]));
}
[Test]
public void TestFreestyleSelectAbort()
{
Room room = null!;
AddStep("add room", () =>
{
room = new Room
{
RoomID = 1,
Playlist =
[
new PlaylistItem(importedSet.Beatmaps[0])
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
},
]
};
API.Perform(new CreateRoomRequest(room));
});
TestPlaylistsScreen playlistsScreen = null!;
AddStep("load screen", () => LoadScreen(playlistsScreen = new TestPlaylistsScreen(new TestPlaylistsRoomSubScreen(room))));
AddUntilStep("wait for playlist room screen", () => playlistsScreen.Stack.CurrentScreen is PlaylistsRoomSubScreen roomSubScreen && roomSubScreen.IsLoaded);
AddUntilStep("original beatmap", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]));
AddStep("enter freestyle select", () => playlistsScreen.Stack.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().Single(b => b.IsPresent).TriggerClick());
AddUntilStep("wait for select screen", () => playlistsScreen.Stack.CurrentScreen is PlaylistsRoomFreestyleSelect selectScreen && selectScreen.CarouselItemsPresented);
AddStep("select next beatmap", () => InputManager.Key(Key.Down));
AddStep("abort", () => playlistsScreen.Stack.CurrentScreen.Exit());
AddUntilStep("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]));
}
[Test]
public void TestFreestyleSelect()
{
Room room = null!;
AddStep("add room", () =>
{
room = new Room
{
RoomID = 1,
Playlist =
[
new PlaylistItem(importedSet.Beatmaps[0])
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
},
]
};
API.Perform(new CreateRoomRequest(room));
});
TestPlaylistsScreen playlistsScreen = null!;
AddStep("load screen", () => LoadScreen(playlistsScreen = new TestPlaylistsScreen(new TestPlaylistsRoomSubScreen(room))));
AddUntilStep("wait for playlist room screen", () => playlistsScreen.Stack.CurrentScreen is PlaylistsRoomSubScreen roomSubScreen && roomSubScreen.IsLoaded);
AddUntilStep("original beatmap", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]));
AddStep("enter freestyle select", () => playlistsScreen.Stack.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().Single(b => b.IsPresent).TriggerClick());
AddUntilStep("wait for select screen", () => playlistsScreen.Stack.CurrentScreen is PlaylistsRoomFreestyleSelect selectScreen && selectScreen.CarouselItemsPresented);
AddStep("select next beatmap", () => InputManager.Key(Key.Down));
AddStep("select (beatmap)", () => InputManager.Key(Key.Enter));
AddStep("select (exit screen)", () => InputManager.Key(Key.Enter));
AddUntilStep("beatmap changed", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1]));
}
/// <summary>
/// Tests that the ruleset style is reset when the selected item is changed and it's no longer valid.
/// </summary>
@@ -591,30 +672,26 @@ namespace osu.Game.Tests.Visual.Playlists
private partial class TestPlaylistsScreen : OsuScreen
{
public readonly OnlinePlaySubScreenStack Stack;
public TestPlaylistsScreen(PlaylistsRoomSubScreen screen)
{
OnlinePlaySubScreenStack stack;
InternalChildren = new Drawable[]
ScreenFooter footer;
InternalChild = new DependencyProvidingContainer
{
stack = new OnlinePlaySubScreenStack
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both
},
new BackButton
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
State = { Value = Visibility.Visible },
Action = () =>
Stack = new OnlinePlaySubScreenStack
{
if (stack.CurrentScreen is not PlaylistsRoomSubScreen)
stack.Exit();
}
}
RelativeSizeAxes = Axes.Both
},
footer = new ScreenFooter(),
},
CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) },
};
stack.Push(screen);
Stack.Push(screen);
}
}
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@@ -266,6 +267,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddAssert("still has selection", () => Beatmap.IsDefault, () => Is.False);
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMinimum, 0.0));
}
[Test]
@@ -365,13 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortBy(SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value));
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
scopeBeatmap(false);
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -389,9 +386,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortBy(SortMode.Artist);
checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
scopeBeatmap(true);
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -413,9 +408,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForFiltering();
checkMatchedBeatmaps(3);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
scopeBeatmap(true);
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -424,6 +417,179 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("text filter not emptied", () => filterTextBox.Current.Value, () => Is.Not.Empty);
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeRevertsToOriginalSelection(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("select normal difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Normal")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("exit scoped view", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FilterControl.ScopedBeatmapSetDisplay>().First());
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
checkMatchedBeatmaps(6);
AddAssert("normal difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeWhenSelectedBeatmapHiddenByFilters(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("set star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, findBeatmap("Hard").StarRating + 0.1));
WaitForFiltering();
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("exit scoped view", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FilterControl.ScopedBeatmapSetDisplay>().First());
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
AddAssert("hard difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByChangingRuleset(bool grouped)
{
bool showConverts = Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps);
AddStep("hide converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0, 2);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(2);
scopeBeatmap(grouped);
checkMatchedBeatmaps(2);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("change ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
WaitForFiltering();
AddAssert("hard catch difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("revert convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, showConverts));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByShowingConverts(bool grouped)
{
bool showConverts = Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps);
AddStep("hide converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("set star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1).StarRating + 0.1));
WaitForFiltering();
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("show converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
WaitForFiltering();
AddAssert("hard difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("revert convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, showConverts));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByChangingFilterText(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("set filter text", () => filterTextBox.Current.Value = findBeatmap("Normal").DifficultyName);
WaitForFiltering();
AddAssert("normal difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
}
private void scopeBeatmap(bool grouped)
{
if (grouped)
{
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
}
else
{
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
}
WaitForFiltering();
}
private BeatmapInfo findBeatmap(string difficultySubstring) => Beatmap.Value.BeatmapSetInfo.Beatmaps.First(b => b.DifficultyName.Contains(difficultySubstring));
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
@@ -7,21 +7,27 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneLoadingLayer : OsuTestScene
public partial class TestSceneLoadingLayer : OsuManualInputManagerTestScene
{
private TestLoadingLayer overlay;
private Container content;
private PressableButton pressableButton;
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -51,10 +57,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{
new OsuSpriteText { Text = "Sample content" },
new RoundedButton { Text = "can't puush me", Width = 200, },
new RoundedButton { Text = "puush me", Width = 200, Action = () => { } },
pressableButton = new PressableButton { Text = "puush me", Width = 200 },
}
},
overlay = new TestLoadingLayer(true),
}
},
};
@@ -63,20 +68,62 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestShowHide()
{
AddStep("create loading layer", () => content.Add(overlay = new TestLoadingLayer(true)));
AddAssert("not visible", () => !overlay.IsPresent);
AddStep("show", () => overlay.Show());
AddUntilStep("wait for content dim", () => overlay.Alpha > 0);
AddStep("hide", () => overlay.Hide());
AddUntilStep("wait for content restore", () => Precision.AlmostEquals(overlay.Alpha, 0));
}
[TestCase(true)]
[TestCase(false)]
public void TestBlockPositional(bool blockInput)
{
AddStep("create loading layer", () => content.Add(overlay = new TestLoadingLayer(true) { BlockPositionalInput = blockInput }));
AddStep("show", () => overlay.Show());
AddStep("click button", () =>
{
InputManager.MoveMouseTo(pressableButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("check pressed", () => pressableButton.Pressed, () => Is.EqualTo(!blockInput));
}
[TestCase(true)]
[TestCase(false)]
public void TestBlockNonPositional(bool blockKeyboardInput)
{
AddStep("create loading layer", () => content.Add(overlay = new TestLoadingLayer(true) { BlockNonPositionalInput = blockKeyboardInput }));
AddStep("show", () => overlay.Show());
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("check pressed", () => pressableButton.Pressed, () => Is.EqualTo(!blockKeyboardInput));
}
[TestCase(true)]
[TestCase(false)]
public void TestBlockNonPositionalGlobalAction(bool blockKeyboardInput)
{
AddStep("create loading layer", () => content.Add(overlay = new TestLoadingLayer(true) { BlockNonPositionalInput = blockKeyboardInput }));
AddStep("show", () => overlay.Show());
AddStep("press enter", () => InputManager.Key(Key.F8));
AddAssert("check pressed", () => pressableButton.Pressed, () => Is.EqualTo(!blockKeyboardInput));
}
[Test]
public void TestLargeArea()
{
AddStep("create loading layer", () => content.Add(overlay = new TestLoadingLayer(true)));
AddStep("show", () =>
{
content.RelativeSizeAxes = Axes.Both;
@@ -88,6 +135,42 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("hide", () => overlay.Hide());
}
public partial class PressableButton : RoundedButton, IKeyBindingHandler<GlobalAction>
{
public PressableButton()
{
Action = () => Pressed = true;
}
public bool Pressed { get; private set; }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.Enter)
{
Pressed = true;
return true;
}
return base.OnKeyDown(e);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.ToggleChat)
{
Pressed = true;
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
private partial class TestLoadingLayer : LoadingLayer
{
public TestLoadingLayer(bool dimBackground = false, bool withBox = true)
+5 -1
View File
@@ -154,7 +154,11 @@ namespace osu.Game.Beatmaps
{
DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty")
};
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo };
var newBeatmap = new Beatmap
{
BeatmapInfo = newBeatmapInfo,
Bookmarks = referenceWorkingBeatmap.Beatmap.Bookmarks.ToArray()
};
foreach (var timingPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.TimingPoints)
newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone());
@@ -6,7 +6,9 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@@ -17,20 +19,28 @@ namespace osu.Game.Graphics.UserInterface
/// Also optionally dims target elements.
/// Useful for disabling all elements in a form and showing we are waiting on a response, for instance.
/// </summary>
public partial class LoadingLayer : LoadingSpinner
public partial class LoadingLayer : LoadingSpinner, IKeyBindingHandler<GlobalAction>
{
private readonly bool blockInput;
/// <summary>
/// Whether to block positional input of components behind the loading layer.
/// Defaults to <c>true</c>.
/// </summary>
public bool BlockPositionalInput { get; init; } = true;
/// <summary>
/// Whether to block all keyboard input. Includes global actions.
/// Defaults to <c>false</c>.
/// </summary>
public bool BlockNonPositionalInput { get; init; }
/// <summary>
/// Construct a new loading spinner.
/// </summary>
/// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
/// <param name="blockInput">Whether to block input of components behind the loading layer.</param>
public LoadingLayer(bool dimBackground = false, bool withBox = true, bool blockInput = true)
public LoadingLayer(bool dimBackground = false, bool withBox = true)
: base(withBox)
{
this.blockInput = blockInput;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(1);
@@ -48,11 +58,11 @@ namespace osu.Game.Graphics.UserInterface
}
}
public override bool HandleNonPositionalInput => false;
public override bool HandleNonPositionalInput => BlockNonPositionalInput;
protected override bool Handle(UIEvent e)
{
if (!blockInput)
if (!BlockPositionalInput)
return false;
switch (e)
@@ -76,5 +86,11 @@ namespace osu.Game.Graphics.UserInterface
MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 80));
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => BlockNonPositionalInput;
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}
@@ -34,6 +34,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString NotAvailable => new TranslatableString(getKey(@"not_available"), @"(not available)");
/// <summary>
/// "Classic scoring mode is always used for this statistic."
/// </summary>
public static LocalisableString ClassicScoringAlwaysUsed => new TranslatableString(getKey(@"classic_scoring_always_used"), @"Classic scoring mode is always used for this statistic.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -55,6 +55,12 @@ namespace osu.Game.Online.Multiplayer
[Key(7)]
public bool VotedToSkipIntro;
/// <summary>
/// The role of this user in the room.
/// </summary>
[Key(8)]
public MultiplayerRoomUserRole Role;
[IgnoreMember]
public APIUser? User { get; set; }
@@ -0,0 +1,11 @@
// 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.
namespace osu.Game.Online.Multiplayer
{
public enum MultiplayerRoomUserRole
{
Player,
Referee,
}
}
@@ -30,56 +30,54 @@ namespace osu.Game.Overlays.Settings.Sections
private readonly BindableBool handlerEnabled = new BindableBool();
private ToggleableHeader header = null!;
public InputSubsection(InputHandler handler)
{
this.handler = handler;
FlowContent.AlwaysPresent = true;
}
[BackgroundDependencyLoader]
private void load()
protected override Drawable CreateHeader() => header = new ToggleableHeader(Header, IsToggleable)
{
HeaderContainer.Child = new ToggleableHeader(Header, IsToggleable)
{
Current = { BindTarget = handlerEnabled },
};
}
Current = { BindTarget = handlerEnabled },
};
protected override void LoadComplete()
{
base.LoadComplete();
handlerEnabled.BindTo(handler.Enabled);
handlerEnabled.BindValueChanged(v => updateEnabledState(), true);
handlerEnabled.BindValueChanged(updateEnabledState, true);
// We use masking to hide the content of these sections.
FlowContent.Masking = true;
}
private void updateEnabledState()
private void updateEnabledState(ValueChangedEvent<bool> state)
{
// set negative bottom margin to not have too much vertical gap between disabled input subsections.
bool negativeBottomMargin = !handlerEnabled.Value || FlowContent.Count == 0;
HeaderContainer.TransformTo(nameof(Margin), new MarginPadding { Bottom = negativeBottomMargin ? -15 : 0 }, 300, Easing.OutQuint);
header.TransformTo(nameof(Margin), new MarginPadding { Bottom = negativeBottomMargin ? -VERTICAL_PADDING : 0 }, 300, Easing.OutQuint);
// Avoid crashes from toggling `AutoSizeAxes` while active `AutoSizeDuration` transforms are still running.
// This is probably a framework bug.
FlowContent.ClearTransforms();
if (!handlerEnabled.Value)
{
FlowContent.AutoSizeAxes = Axes.None;
FlowContent.ResizeHeightTo(0, 300, Easing.OutQuint);
FlowContent.FadeOut(200, Easing.OutQuint);
}
else
{
// enable auto size transform momentarily for smooth pop in animation, and disable it right after the transform is added.
// we don't want this specification to apply when a dropdown in the input settings is being open, it causes too slow animation.
// (try removing the schedule below then watch a settings dropdown menu opening animation).
FlowContent.AutoSizeDuration = 300;
FlowContent.AutoSizeDuration = state.NewValue == state.OldValue ? 0 : 300;
FlowContent.AutoSizeEasing = Easing.OutQuint;
FlowContent.AutoSizeAxes = Axes.Y;
ScheduleAfterChildren(() => FlowContent.AutoSizeDuration = 0);
FlowContent.FadeIn(300, Easing.OutQuint);
ScheduleAfterChildren(() => FlowContent.AutoSizeDuration = 0);
}
}
@@ -92,6 +90,11 @@ namespace osu.Game.Overlays.Settings.Sections
public ToggleableHeader(LocalisableString text, bool toggleable)
{
Padding = SettingsPanel.CONTENT_PADDING;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
this.text = text;
this.toggleable = toggleable;
}
@@ -105,8 +108,6 @@ namespace osu.Game.Overlays.Settings.Sections
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
switchButton = new SwitchButton
@@ -24,7 +24,10 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
Caption = UserInterfaceStrings.CursorRotation,
Current = config.GetBindable<bool>(OsuSetting.CursorRotation)
}),
})
{
Keywords = [@"spin"],
},
new SettingsItemV2(new FormSliderBar<float>
{
Caption = UserInterfaceStrings.MenuCursorSize,
@@ -45,7 +48,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelFormat = v => $"{v:N0} ms",
})
{
Keywords = new[] { @"delay" },
Keywords = [@"delay"],
ApplyClassicDefault = c => ((IHasCurrentValue<double>)c).Current.Value = 0,
},
};
@@ -14,12 +14,12 @@ namespace osu.Game.Overlays.Settings
{
public abstract partial class SettingsSubsection : FillFlowContainer, IFilterable
{
public const float VERTICAL_PADDING = (header_height - header_font_size) * 0.5f;
protected override Container<Drawable> Content => FlowContent;
protected readonly FillFlowContainer FlowContent;
protected Container HeaderContainer { get; private set; } = null!;
protected abstract LocalisableString Header { get; }
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
@@ -53,25 +53,22 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
AddRangeInternal(new[]
{
HeaderContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = SettingsPanel.CONTENT_PADDING,
Children = new[]
{
new OsuSpriteText
{
Text = Header,
Font = OsuFont.GetFont(size: header_font_size),
Margin = new MarginPadding { Vertical = (header_height - header_font_size) * 0.5f },
},
},
},
CreateHeader(),
FlowContent
});
}
protected virtual Drawable CreateHeader()
{
return new OsuSpriteText
{
Text = Header,
Font = OsuFont.GetFont(size: header_font_size),
Margin = new MarginPadding { Vertical = VERTICAL_PADDING },
Padding = SettingsPanel.CONTENT_PADDING,
};
}
}
}
+16 -55
View File
@@ -1,23 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@@ -36,20 +33,7 @@ namespace osu.Game.Overlays.Toolbar
IconContainer.Show();
}
[Resolved]
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;
public void SetIcon(IconUsage icon) =>
SetIcon(new SpriteIcon
{
Icon = icon,
});
public LocalisableString Text
{
get => DrawableText.Text;
set => DrawableText.Text = value;
}
public void SetIcon(IconUsage icon) => SetIcon(new SpriteIcon { Icon = icon });
public LocalisableString TooltipMain
{
@@ -67,21 +51,16 @@ namespace osu.Game.Overlays.Toolbar
protected readonly Container ButtonContent;
protected ConstrainedIconContainer IconContainer;
protected SpriteText DrawableText;
protected Box HoverBackground;
private readonly Box flashBackground;
private readonly FillFlowContainer tooltipContainer;
private readonly SpriteText tooltip1;
private readonly SpriteText tooltip2;
private readonly SpriteText keyBindingTooltip;
protected FillFlowContainer Flow;
protected readonly Container BackgroundContent;
private IDisposable? realmSubscription;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private readonly FillFlowContainer subTooltipFlow;
protected ToolbarButton()
{
@@ -124,7 +103,6 @@ namespace osu.Game.Overlays.Toolbar
Flow = new FillFlowContainer
{
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 },
@@ -139,11 +117,6 @@ namespace osu.Game.Overlays.Toolbar
Size = new Vector2(20),
Alpha = 0,
},
DrawableText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
},
},
},
@@ -165,16 +138,15 @@ namespace osu.Game.Overlays.Toolbar
Shadow = true,
Font = OsuFont.GetFont(size: 22, weight: FontWeight.Bold),
},
new FillFlowContainer
subTooltipFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = TooltipAnchor,
Origin = TooltipAnchor,
Direction = FillDirection.Horizontal,
Children = new[]
Children = new Drawable[]
{
tooltip2 = new OsuSpriteText { Shadow = true },
keyBindingTooltip = new OsuSpriteText { Shadow = true }
}
}
}
@@ -187,8 +159,13 @@ namespace osu.Game.Overlays.Toolbar
{
if (Hotkey != null)
{
realmSubscription = realm.SubscribeToPropertyChanged(r => r.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value),
kb => kb.KeyCombinationString, updateKeyBindingTooltip);
subTooltipFlow.Add(new HotkeyDisplay
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Hotkey = new Hotkey(Hotkey.Value),
Margin = new MarginPadding { Left = 3 },
});
}
}
@@ -203,16 +180,16 @@ namespace osu.Game.Overlays.Toolbar
protected override bool OnHover(HoverEvent e)
{
HoverBackground.FadeIn(200);
tooltipContainer.FadeIn(100);
HoverBackground.FadeIn(300, Easing.OutQuint);
tooltipContainer.FadeIn(200, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
HoverBackground.FadeOut(200);
tooltipContainer.FadeOut(100);
HoverBackground.FadeOut(200, Easing.Out);
tooltipContainer.FadeOut(100, Easing.Out);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@@ -229,22 +206,6 @@ namespace osu.Game.Overlays.Toolbar
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void updateKeyBindingTooltip(string keyCombination)
{
string keyBindingString = keyCombinationProvider.GetReadableString(keyCombination);
keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString)
? $" ({keyBindingString})"
: string.Empty;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
}
public partial class OpaqueBackground : Container
@@ -43,7 +43,7 @@ namespace osu.Game.Overlays.Toolbar
Origin = Anchor.CentreLeft,
Width = 3f,
Height = IconContainer.Height,
Margin = new MarginPadding { Horizontal = 2.5f },
Margin = new MarginPadding { Left = 7.5f, Right = 2.5f },
Masking = true,
Children = new[]
{
+46 -36
View File
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
@@ -32,6 +33,8 @@ namespace osu.Game.Overlays.Toolbar
private IBindable<APIState> apiState = null!;
private OsuSpriteText usernameText = null!;
public ToolbarUserButton()
{
ButtonContent.AutoSizeAxes = Axes.X;
@@ -40,51 +43,58 @@ namespace osu.Game.Overlays.Toolbar
[BackgroundDependencyLoader]
private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login)
{
Flow.Add(new Container
Flow.AddRange(new Drawable[]
{
Masking = true,
CornerRadius = 4,
Size = new Vector2(32),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
EdgeEffect = new EdgeEffectParameters
usernameText = new OsuSpriteText
{
Type = EdgeEffectType.Shadow,
Radius = 4,
Colour = Color4.Black.Opacity(0.1f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 5 },
},
Children = new Drawable[]
new Container
{
avatar = new UpdateableAvatar(isInteractive: false)
Masking = true,
CornerRadius = 4,
Size = new Vector2(32),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
EdgeEffect = new EdgeEffectParameters
{
RelativeSizeAxes = Axes.Both,
Type = EdgeEffectType.Shadow,
Radius = 4,
Colour = Color4.Black.Opacity(0.1f),
},
spinner = new LoadingLayer(dimBackground: true, withBox: false, blockInput: false)
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
failingIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Size = new Vector2(0.3f),
Icon = FontAwesome.Solid.ExclamationTriangle,
RelativeSizeAxes = Axes.Both,
Colour = colours.YellowLight,
},
avatar = new UpdateableAvatar(isInteractive: false)
{
RelativeSizeAxes = Axes.Both,
},
spinner = new LoadingLayer(dimBackground: true, withBox: false)
{
BlockPositionalInput = false,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
failingIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Size = new Vector2(0.3f),
Icon = FontAwesome.Solid.ExclamationTriangle,
RelativeSizeAxes = Axes.Both,
Colour = colours.YellowLight,
},
}
},
new TransientUserStatisticsUpdateDisplay
{
Alpha = 0,
}
});
Flow.Add(new TransientUserStatisticsUpdateDisplay
{
Alpha = 0
});
Flow.AutoSizeEasing = Easing.OutQuint;
Flow.AutoSizeDuration = 250;
apiState = api.State.GetBoundCopy();
apiState.BindValueChanged(onlineStateChanged, true);
@@ -96,7 +106,7 @@ namespace osu.Game.Overlays.Toolbar
private void userChanged(ValueChangedEvent<APIUser> user) => Schedule(() =>
{
Text = user.NewValue.Username;
usernameText.Text = user.NewValue.Username;
avatar.User = user.NewValue;
});
@@ -26,12 +26,12 @@ namespace osu.Game.Overlays.Toolbar
private Statistic<int> globalRank = null!;
private Statistic<int> pp = null!;
private ScheduledDelegate? shrinkDelegate;
[BackgroundDependencyLoader]
private void load(UserStatisticsWatcher? userStatisticsWatcher)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
Alpha = 0;
InternalChild = new FillFlowContainer
{
@@ -40,7 +40,7 @@ namespace osu.Game.Overlays.Toolbar
Padding = new MarginPadding { Horizontal = 10 },
Spacing = new Vector2(10),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
Children = new[]
{
globalRank = new Statistic<int>(UsersStrings.ShowRankGlobalSimple, @"#", Comparer<int>.Create((before, after) => before - after)),
pp = new Statistic<int>(RankingsStrings.StatPerformance, string.Empty, Comparer<int>.Create((before, after) => Math.Sign(after - before))),
@@ -71,8 +71,7 @@ namespace osu.Game.Overlays.Toolbar
return;
FinishTransforms(true);
this.FadeIn(500, Easing.OutQuint);
shrinkDelegate?.Cancel();
if (update.After.GlobalRank != null)
{
@@ -90,7 +89,21 @@ namespace osu.Game.Overlays.Toolbar
pp.Display(before, delta, after);
}
this.Delay(5000).FadeOut(500, Easing.OutQuint);
this.FadeIn(500, Easing.OutQuint);
AutoSizeAxes = Axes.X;
AutoSizeDuration = 500;
AutoSizeEasing = Easing.OutQuint;
using (BeginDelayedSequence(5000))
{
this.FadeOut(500, Easing.OutQuint);
shrinkDelegate = Schedule(() =>
{
AutoSizeAxes = Axes.None;
this.ResizeWidthTo(0, 500, Easing.OutQuint);
});
}
});
}
@@ -113,22 +113,14 @@ namespace osu.Game.Rulesets.Edit
var lastHitObject = getPreviousHitObject();
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (AutomaticAdditionBankAssignment)
{
// 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();
}
if (lastHitNormal != null && 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, newEditorAutoBank: true) : s).ToList();
else
HitObject.Samples = HitObject.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newEditorAutoBank: false) : s).ToList();
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 and sample set info from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.With(
newVolume: lastHitNormal.Volume,
@@ -136,6 +128,14 @@ namespace osu.Game.Rulesets.Edit
newUseBeatmapSamples: lastHitNormal.UseBeatmapSamples)).ToList();
}
if (AutomaticAdditionBankAssignment)
{
string bank = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
HitObject.Samples = HitObject.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bank, newEditorAutoBank: true) : s).ToList();
}
else
HitObject.Samples = HitObject.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newEditorAutoBank: false) : s).ToList();
if (HitObject is IHasRepeats hasRepeats)
{
// Make sure all the node samples are identical to the hit object's samples
@@ -3,17 +3,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor
public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToPlayer, IApplicableToHealthProcessor
{
[SettingSource("Extra Lives", "Number of extra lives")]
public Bindable<int> Retries { get; } = new BindableInt(2)
@@ -33,18 +34,26 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries;
private int? retries;
private readonly BindableNumber<double> health = new BindableDouble();
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
public void ApplyToPlayer(Player player)
{
base.ApplyToDifficulty(difficulty);
// this throw works for two reasons:
// - every time `Player` loads, it deep-clones mods into itself, and the deep clone copies *only* `[SettingsSource]` properties
// - `Player` is the only consumer of `IApplicableToPlayer` and it calls `ApplyToPlayer()` exactly once per mod instance
// if either of the above assumptions no longer holds true for any reason, this will need to be reconsidered
if (retries != null)
throw new InvalidOperationException(@"Cannot apply this mod instance to a player twice.");
retries = Retries.Value;
}
public bool PerformFail()
{
Debug.Assert(retries != null);
if (retries == 0) return true;
health.Value = health.MaxValue;
+28 -9
View File
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -81,6 +80,7 @@ namespace osu.Game.Rulesets.Mods
private PausableSkinnableSound? finishSample;
private int? firstBeat;
private int lastBeat = -1;
public NightcoreBeatContainer()
{
@@ -116,21 +116,35 @@ namespace osu.Game.Rulesets.Mods
if (!firstBeat.HasValue || beatIndex < firstBeat)
// decide on a good starting beat index if once has not yet been decided.
firstBeat = beatIndex < 0 ? 0 : (beatIndex / segmentLength + 1) * segmentLength;
firstBeat = beatIndex < 0 ? 0 : (beatIndex / segmentLength) * segmentLength;
if (beatIndex >= firstBeat)
playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature);
playBeatFor(beatIndex, segmentLength, timingPoint);
}
private void playBeatFor(int beatIndex, TimeSignature signature)
private void playBeatFor(int beatIndex, int segmentLength, TimingControlPoint timingPoint)
{
if (beatIndex == 0)
finishSample?.Play();
// https://github.com/peppy/osu-stable-reference/blob/6ab0cf1f9f7b3449f5c0d8defcd458aae72cdb88/osu!/Audio/NightcoreBeat.cs#L41
if (lastBeat == beatIndex)
return;
switch (signature.Numerator)
lastBeat = beatIndex;
int beatInSegment = beatIndex % segmentLength;
if (beatInSegment == 0)
{
// https://github.com/peppy/osu-stable-reference/blob/6ab0cf1f9f7b3449f5c0d8defcd458aae72cdb88/osu!/Audio/NightcoreBeat.cs#L53
bool playFinish = beatIndex > 0 || !timingPoint.OmitFirstBarLine;
if (playFinish)
finishSample?.Play();
}
switch (timingPoint.TimeSignature.Numerator)
{
case 3:
switch (beatIndex % 6)
switch (beatInSegment % 6)
{
case 0:
kickSample?.Play();
@@ -148,7 +162,7 @@ namespace osu.Game.Rulesets.Mods
break;
case 4:
switch (beatIndex % 4)
switch (beatInSegment % 4)
{
case 0:
kickSample?.Play();
@@ -159,6 +173,11 @@ namespace osu.Game.Rulesets.Mods
break;
default:
// note that in stable hat samples would only play if the beatmap tick rate was even
// (https://github.com/peppy/osu-stable-reference/blob/6ab0cf1f9f7b3449f5c0d8defcd458aae72cdb88/osu!/Audio/NightcoreBeat.cs#L30-L32)
// that kind of presumes that only music timed in 4/4 exists, and does not really work well
// if the beatmap e.g. mixes 4/4 and 3/4 signature timing control points.
// therefore this conditional behaviour is not reimplemented.
hatSample?.Play();
break;
}
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
@@ -56,12 +55,12 @@ namespace osu.Game.Rulesets.Objects
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any())
{
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
double lastSegmentEnd = segmentEnds[^1];
var circleArcPath = new List<Vector2>();
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
// we want to shorten the last perfect segment, preserving its shape, so that its end is consistent with the slider path's end.
// therefore we also reposition the middle point of the segment to be ideally halfway through its arc.
// the end of the segment is assumed to be at path position 1 at all times,
// but the start of the segment cannot be assumed to be at 0 because multi-segment sliders exist.
controlPoints[^2].Position = sliderPath.PositionAt((lastSegmentStart + 1) / 2);
}
sliderPath.reverseControlPoints(out positionalOffset);
@@ -46,6 +46,7 @@ namespace osu.Game.Screens.Edit.Components
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
private readonly Dictionary<(string name, string bank), SampleButton> buttons = new Dictionary<(string, string), SampleButton>();
private readonly Bindable<DirectoryInfo?> lastSelectedFileDirectory = new Bindable<DirectoryInfo?>();
private FormControlBackground background = null!;
private FormFieldCaption caption = null!;
@@ -125,6 +126,7 @@ namespace osu.Game.Screens.Edit.Components
Margin = new MarginPadding(5),
SampleAddRequested = SampleAddRequested,
SampleRemoveRequested = SampleRemoveRequested,
LastSelectedFileDirectory = { BindTarget = lastSelectedFileDirectory },
};
protected override void LoadComplete()
@@ -197,6 +199,7 @@ namespace osu.Game.Screens.Edit.Components
public Action<string>? SampleRemoveRequested { get; init; }
private Bindable<FileInfo?> selectedFile { get; } = new Bindable<FileInfo?>();
public Bindable<DirectoryInfo?> LastSelectedFileDirectory { get; } = new Bindable<DirectoryInfo?>();
private TrianglesV2? triangles { get; set; }
@@ -313,6 +316,7 @@ namespace osu.Game.Screens.Edit.Components
this.HidePopover();
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString();
LastSelectedFileDirectory.Value = selectedFile.Value.Directory;
}
private void deleteSample()
@@ -324,7 +328,9 @@ namespace osu.Game.Screens.Edit.Components
ActualFilename.Value = null;
}
public Popover? GetPopover() => ActualFilename.Value == null ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, null) : null;
public Popover? GetPopover() => ActualFilename.Value == null
? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, LastSelectedFileDirectory.Value?.FullName)
: null;
public MenuItem[]? ContextMenuItems =>
ActualFilename.Value != null
@@ -16,6 +16,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
@@ -296,22 +297,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
foreach ((string sampleName, var bindable) in SelectionSampleStates)
if (samplesInSelection.Length > 0)
{
bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Name == sampleName));
}
foreach ((string sampleName, var bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Name == sampleName));
}
foreach ((string bankName, var bindable) in SelectionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name == HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName);
}
foreach ((string bankName, var bindable) in SelectionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name == HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName);
}
SelectionAdditionBanksEnabled.Value = samplesInSelection.SelectMany(s => s).Any(o => o.Name != HitSampleInfo.HIT_NORMAL);
SelectionAdditionBanksEnabled.Value = samplesInSelection.SelectMany(s => s).Any(o => o.Name != HitSampleInfo.HIT_NORMAL);
foreach ((string bankName, var bindable) in SelectionAdditionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL),
h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank));
foreach ((string bankName, var bindable) in SelectionAdditionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL),
h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank));
}
}
}
@@ -342,6 +346,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// Sets the sample bank for all selected <see cref="HitObject"/>s.
/// </summary>
/// <remarks>
/// Should be kept in sync with <see cref="SamplePointPiece.SampleEditPopover.setBank"/>.
/// </remarks>
/// <param name="bankName">The name of the sample bank.</param>
public void SetSampleBank(string bankName)
{
@@ -366,12 +373,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (hasRelevantBank(h))
return;
h.Samples = h.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
h.Samples = h.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL || s.EditorAutoBank ? 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();
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name == HitSampleInfo.HIT_NORMAL || s.EditorAutoBank ? s.With(newBank: bankName) : s).ToList();
}
});
}
@@ -379,6 +386,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// Sets the sample addition bank for all selected <see cref="HitObject"/>s.
/// </summary>
/// <remarks>
/// Should be kept in sync with <see cref="SamplePointPiece.SampleEditPopover.setAdditionBank"/>.
/// </remarks>
/// <param name="bankName">The name of the sample bank.</param>
public void SetSampleAdditionBank(string bankName)
{
@@ -432,6 +442,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary>
/// <remarks>
/// Should be kept in sync with <see cref="SamplePointPiece.SampleEditPopover.addHitSample"/>.
/// </remarks>
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
@@ -464,6 +464,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
beatmap.EndChange();
}
/// <remarks>
/// Should be kept in sync with <see cref="EditorSelectionHandler.SetSampleBank"/>.
/// </remarks>
private void setBank(string newBank)
{
updateAllRelevantSamples((_, relevantSamples) =>
@@ -477,6 +480,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
});
}
/// <remarks>
/// Should be kept in sync with <see cref="EditorSelectionHandler.SetSampleAdditionBank"/>.
/// </remarks>
private void setAdditionBank(string newBank)
{
updateAllRelevantSamples((_, relevantSamples) =>
@@ -583,6 +589,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
/// <remarks>
/// Should be kept in sync with <see cref="EditorSelectionHandler.AddHitSample"/>.
/// </remarks>
private void addHitSample(string sampleName)
{
if (string.IsNullOrEmpty(sampleName))
+12 -1
View File
@@ -43,7 +43,18 @@ namespace osu.Game.Screens.OnlinePlay
}
}
private void updateSubScreenTitle() => title.Screen = stack?.CurrentScreen as IOnlinePlaySubScreen;
private void updateSubScreenTitle()
{
IOnlinePlaySubScreen? screen = stack?.CurrentScreen as IOnlinePlaySubScreen;
if (screen?.ShowHeaderLine == true)
{
title.FadeIn(200, Easing.OutQuint);
title.Screen = screen;
}
else
title.FadeOut(200, Easing.OutQuint);
}
private partial class MultiHeaderTitle : CompositeDrawable
{
@@ -8,5 +8,7 @@ namespace osu.Game.Screens.OnlinePlay
string Title { get; }
string ShortTitle { get; }
bool ShowHeaderLine => true;
}
}
@@ -25,15 +25,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private LoadingLayer loadingLayer = null!;
private IDisposable? selectionOperation;
public MultiplayerMatchFreestyleSelect(Room room, PlaylistItem item)
: base(room, item)
public MultiplayerMatchFreestyleSelect(PlaylistItem item)
: base(item)
{
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(loadingLayer = new LoadingLayer(true));
AddInternal(loadingLayer = new LoadingLayer(dimBackground: true) { BlockNonPositionalInput = true });
}
protected override void LoadComplete()
@@ -52,17 +52,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
loadingLayer.Hide();
}
protected override bool OnStart()
protected override void StartAction()
{
if (operationInProgress.Value)
{
Logger.Log($"{nameof(OnStart)} aborted due to {nameof(operationInProgress)}");
return false;
return;
}
if (!base.OnStart())
return false;
selectionOperation = operationTracker.BeginOperation();
client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID)
@@ -79,14 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}, onError: _ =>
{
selectionOperation.Dispose();
Schedule(() =>
{
Carousel.AllowSelection = true;
});
});
return true;
}
}
}
@@ -2,21 +2,41 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerMatchSongSelect : OnlinePlaySongSelect
public partial class MultiplayerMatchSongSelect : SongSelect, IOnlinePlaySubScreen
{
public string ShortTitle => "song selection";
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
@@ -30,21 +50,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private LoadingLayer loadingLayer = null!;
private IDisposable? selectionOperation;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly Bindable<bool> freestyle = new Bindable<bool>(true);
private readonly PlaylistItem? initialItem;
private readonly FreeModSelectOverlay freeModSelect;
private FooterButton freeModsFooterButton = null!;
private IDisposable? freeModSelectOverlayRegistration;
/// <summary>
/// Construct a new instance of multiplayer song select.
/// </summary>
/// <param name="room">The room.</param>
/// <param name="itemToEdit">The item to be edited. May be null, in which case a new item will be added to the playlist.</param>
public MultiplayerMatchSongSelect(Room room, PlaylistItem? itemToEdit = null)
: base(room, itemToEdit)
{
this.room = room;
this.itemToEdit = itemToEdit;
initialItem = itemToEdit ?? room.Playlist.LastOrDefault();
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
freeModSelect = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods },
IsValidMod = isValidAllowedMod,
};
}
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
LoadComponent(freeModSelect);
AddInternal(loadingLayer = new LoadingLayer(true));
}
@@ -52,23 +101,133 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
if (initialItem != null)
{
// Prefer using a local databased beatmap lookup since OnlineId may be -1 for an invalid beatmap selection.
BeatmapInfo? beatmapInfo = initialItem.Beatmap as BeatmapInfo;
// And in the case that this isn't a local databased beatmap, query by online ID.
if (beatmapInfo == null)
{
int onlineId = initialItem.Beatmap.OnlineID;
beatmapInfo = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId);
}
if (beatmapInfo != null)
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
RulesetInfo? ruleset = rulesets.GetRuleset(initialItem.RulesetID);
if (ruleset != null)
{
Ruleset.Value = ruleset;
var rulesetInstance = ruleset.CreateInstance();
Debug.Assert(rulesetInstance != null);
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
freestyle.Value = initialItem.Freestyle;
}
Mods.BindValueChanged(_ => updateValidMods());
Ruleset.BindValueChanged(onRulesetChanged);
freestyle.BindValueChanged(onFreestyleChanged);
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect);
updateFooterButtons();
updateValidMods();
operationInProgress.BindTo(operationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
operationInProgress.BindValueChanged(operation =>
{
if (operation.NewValue)
loadingLayer.Show();
else
loadingLayer.Hide();
}, true);
}
private void updateLoadingLayer()
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
private void onFreestyleChanged(ValueChangedEvent<bool> enabled)
{
if (operationInProgress.Value)
loadingLayer.Show();
updateFooterButtons();
updateValidMods();
if (enabled.NewValue)
{
// Freestyle allows all mods to be selected as freemods. This does not play nicely for some components:
// - We probably don't want to store a gigantic list of acronyms to the database.
// - The mod select overlay isn't built to handle duplicate mods/mods from all rulesets being shoved into it.
// Instead, freestyle inherently assumes this list is empty, and must be empty for server-side validation to pass.
FreeMods.Value = [];
}
else
loadingLayer.Hide();
{
// When disabling freestyle, enable freemods by default.
FreeMods.Value = freeModSelect.AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod).ToArray();
}
}
protected override bool SelectItem(PlaylistItem item)
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
{
// Todo: We can probably attempt to preserve across rulesets like the global mods do.
FreeMods.Value = [];
}
private void updateFooterButtons()
{
if (freestyle.Value)
{
freeModsFooterButton.Enabled.Value = false;
freeModSelect.Hide();
}
else
freeModsFooterButton.Enabled.Value = true;
}
/// <summary>
/// Removes invalid mods from <see cref="OsuScreen.Mods"/> and <see cref="FreeMods"/>,
/// and updates mod selection overlays to display the new mods valid for selection.
/// </summary>
private void updateValidMods()
{
Mod[] validMods = Mods.Value.Where(isValidRequiredMod).ToArray();
if (!validMods.SequenceEqual(Mods.Value))
Mods.Value = validMods;
Mod[] validFreeMods = FreeMods.Value.Where(isValidAllowedMod).ToArray();
if (!validFreeMods.SequenceEqual(FreeMods.Value))
FreeMods.Value = validFreeMods;
ModSelect.IsValidMod = isValidRequiredMod;
freeModSelect.IsValidMod = isValidAllowedMod;
}
protected sealed override bool OnStart()
{
var item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID,
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(),
Freestyle = freestyle.Value
};
return selectItem(item);
}
private bool selectItem(PlaylistItem item)
{
if (operationInProgress.Value)
{
Logger.Log($"{nameof(SelectItem)} aborted due to {nameof(operationInProgress)}");
Logger.Log($"{nameof(selectItem)} aborted due to {nameof(operationInProgress)}");
return false;
}
@@ -120,6 +279,70 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
public override bool OnBackButton()
{
if (freeModSelect.State.Value == Visibility.Visible)
{
freeModSelect.Hide();
return true;
}
return base.OnBackButton();
}
public override bool OnExiting(ScreenExitEvent e)
{
freeModSelect.Hide();
return base.OnExiting(e);
}
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = isValidRequiredMod
};
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
{
var baseButtons = base.CreateSongSelectFooterButtons().ToList();
baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip;
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
{
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect)
{
FreeMods = { BindTarget = FreeMods },
Freestyle = { BindTarget = freestyle }
}, null),
(new FooterButtonFreestyle
{
Freestyle = { BindTarget = freestyle }
}, null)
});
return baseButtons;
}
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid to be selected as a required mod.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
private bool isValidRequiredMod(Mod mod) => ModUtils.IsValidModForMatch(mod, true, room.Type, freestyle.Value);
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid to be selected as an allowed mod.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
private bool isValidAllowedMod(Mod mod) => ModUtils.IsValidModForMatch(mod, false, room.Type, freestyle.Value)
// Mod must not be contained in the required mods.
&& Mods.Value.All(m => m.Acronym != mod.Acronym)
// Mod must be compatible with all the required mods.
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray());
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
freeModSelectOverlayRegistration?.Dispose();
}
}
}
@@ -722,7 +722,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
this.Push(new MultiplayerMatchFreestyleSelect(room, new PlaylistItem(item)));
this.Push(new MultiplayerMatchFreestyleSelect(new PlaylistItem(item)));
}
/// <summary>
@@ -47,6 +47,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
Masking = true;
CornerRadius = 5;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 10,
Colour = Colour4.Black.Opacity(0.2f),
};
}
protected override void Update()
@@ -6,45 +6,77 @@ using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Select;
using osu.Game.Users;
using SongSelect = osu.Game.Screens.SelectV2.SongSelect;
namespace osu.Game.Screens.OnlinePlay
{
public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap
public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IHandlePresentBeatmap, IOnlinePlaySubScreen
{
public string ShortTitle => "style selection";
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
private readonly Room room;
private readonly PlaylistItem item;
protected OnlinePlayFreestyleSelect(Room room, PlaylistItem item)
public string ShortTitle => "style selection";
public override string Title => ShortTitle.Humanize();
public bool ShowHeaderLine => false;
protected abstract void StartAction();
[Resolved]
private RealmAccess realm { get; set; } = null!;
protected OnlinePlayFreestyleSelect(PlaylistItem item)
{
this.room = room;
this.item = item;
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
SupportScoping = false;
}
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
FilterControl.ApplyRequiredCriteria = applyRestrictions;
}
protected override bool OnStart()
protected override void OnStart()
{
if (isValidForSelection())
StartAction();
}
private void applyRestrictions(FilterCriteria criteria)
{
double itemLength = 0;
int beatmapSetId = 0;
realm.Run(r =>
{
int beatmapId = item.Beatmap.OnlineID;
BeatmapInfo? beatmap = r.All<BeatmapInfo>().FirstOrDefault(b => b.OnlineID == beatmapId);
itemLength = beatmap?.Length ?? 0;
beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0;
});
// Must be from the same set as the playlist item.
criteria.BeatmapSetId = beatmapSetId;
criteria.HasOnlineID = true;
// Must be within 30s of the playlist item.
criteria.Length.Min = itemLength - 30000;
criteria.Length.Max = itemLength + 30000;
criteria.Length.IsLowerInclusive = true;
criteria.Length.IsUpperInclusive = true;
}
private bool isValidForSelection()
{
FilterCriteria criteria = FilterControl.CreateCriteria();
@@ -78,61 +110,11 @@ namespace osu.Game.Screens.OnlinePlay
return true;
}
protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item);
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => [];
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
{
// Required to create the drawable components.
base.CreateSongSelectFooterButtons();
return Enumerable.Empty<(FooterButton, OverlayContainer?)>();
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
void IHandlePresentBeatmap.PresentBeatmap(WorkingBeatmap workingBeatmap, RulesetInfo ruleset)
{
// This screen cannot present beatmaps.
}
private partial class DifficultySelectFilterControl : FilterControl
{
private readonly PlaylistItem item;
[Resolved]
private RealmAccess realm { get; set; } = null!;
public DifficultySelectFilterControl(PlaylistItem item)
{
this.item = item;
}
public override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
double itemLength = 0;
int beatmapSetId = 0;
realm.Run(r =>
{
int beatmapId = item.Beatmap.OnlineID;
BeatmapInfo? beatmap = r.All<BeatmapInfo>().FirstOrDefault(b => b.OnlineID == beatmapId);
itemLength = beatmap?.Length ?? 0;
beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0;
});
// Must be from the same set as the playlist item.
criteria.BeatmapSetId = beatmapSetId;
criteria.HasOnlineID = true;
// Must be within 30s of the playlist item.
criteria.Length.Min = itemLength - 30000;
criteria.Length.Max = itemLength + 30000;
criteria.Length.IsLowerInclusive = true;
criteria.Length.IsUpperInclusive = true;
return criteria;
}
}
}
}
@@ -1,274 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Users;
using osu.Game.Utils;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay
{
public abstract partial class OnlinePlaySongSelect : SongSelect, IOnlinePlaySubScreen
{
public string ShortTitle => "song selection";
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected readonly Bindable<bool> Freestyle = new Bindable<bool>(true);
private readonly Room room;
private readonly PlaylistItem? initialItem;
private readonly FreeModSelectOverlay freeModSelect;
private FooterButton freeModsFooterButton = null!;
private IDisposable? freeModSelectOverlayRegistration;
/// <summary>
/// Creates a new <see cref="OnlinePlaySongSelect"/>.
/// </summary>
/// <param name="room">The room.</param>
/// <param name="initialItem">An optional initial <see cref="PlaylistItem"/> to use for the initial beatmap/ruleset/mods.
/// If <c>null</c>, the last <see cref="PlaylistItem"/> in the room will be used.</param>
protected OnlinePlaySongSelect(Room room, PlaylistItem? initialItem = null)
{
this.room = room;
this.initialItem = initialItem ?? room.Playlist.LastOrDefault();
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
freeModSelect = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods },
IsValidMod = isValidAllowedMod,
};
}
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
LoadComponent(freeModSelect);
}
protected override void LoadComplete()
{
base.LoadComplete();
if (initialItem != null)
{
// Prefer using a local databased beatmap lookup since OnlineId may be -1 for an invalid beatmap selection.
BeatmapInfo? beatmapInfo = initialItem.Beatmap as BeatmapInfo;
// And in the case that this isn't a local databased beatmap, query by online ID.
if (beatmapInfo == null)
{
int onlineId = initialItem.Beatmap.OnlineID;
beatmapInfo = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId);
}
if (beatmapInfo != null)
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
RulesetInfo? ruleset = rulesets.GetRuleset(initialItem.RulesetID);
if (ruleset != null)
{
Ruleset.Value = ruleset;
var rulesetInstance = ruleset.CreateInstance();
Debug.Assert(rulesetInstance != null);
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
Freestyle.Value = initialItem.Freestyle;
}
Mods.BindValueChanged(onGlobalModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);
Freestyle.BindValueChanged(onFreestyleChanged);
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect);
updateFooterButtons();
updateValidMods();
}
private void onFreestyleChanged(ValueChangedEvent<bool> enabled)
{
updateFooterButtons();
updateValidMods();
if (enabled.NewValue)
{
// Freestyle allows all mods to be selected as freemods. This does not play nicely for some components:
// - We probably don't want to store a gigantic list of acronyms to the database.
// - The mod select overlay isn't built to handle duplicate mods/mods from all rulesets being shoved into it.
// Instead, freestyle inherently assumes this list is empty, and must be empty for server-side validation to pass.
FreeMods.Value = [];
}
else
{
// When disabling freestyle, enable freemods by default.
FreeMods.Value = freeModSelect.AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod).ToArray();
}
}
private void onGlobalModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
updateValidMods();
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
{
// Todo: We can probably attempt to preserve across rulesets like the global mods do.
FreeMods.Value = [];
}
private void updateFooterButtons()
{
if (Freestyle.Value)
{
freeModsFooterButton.Enabled.Value = false;
freeModSelect.Hide();
}
else
freeModsFooterButton.Enabled.Value = true;
}
/// <summary>
/// Removes invalid mods from <see cref="OsuScreen.Mods"/> and <see cref="FreeMods"/>,
/// and updates mod selection overlays to display the new mods valid for selection.
/// </summary>
private void updateValidMods()
{
Mod[] validMods = Mods.Value.Where(isValidRequiredMod).ToArray();
if (!validMods.SequenceEqual(Mods.Value))
Mods.Value = validMods;
Mod[] validFreeMods = FreeMods.Value.Where(isValidAllowedMod).ToArray();
if (!validFreeMods.SequenceEqual(FreeMods.Value))
FreeMods.Value = validFreeMods;
ModSelect.IsValidMod = isValidRequiredMod;
freeModSelect.IsValidMod = isValidAllowedMod;
}
protected sealed override bool OnStart()
{
var item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID,
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(),
Freestyle = Freestyle.Value
};
return SelectItem(item);
}
/// <summary>
/// Invoked when the user has requested a selection of a beatmap.
/// </summary>
/// <param name="item">The resultant <see cref="PlaylistItem"/>. This item has not yet been added to the <see cref="Room"/>'s.</param>
/// <returns><c>true</c> if a selection occurred.</returns>
protected abstract bool SelectItem(PlaylistItem item);
public override bool OnBackButton()
{
if (freeModSelect.State.Value == Visibility.Visible)
{
freeModSelect.Hide();
return true;
}
return base.OnBackButton();
}
public override bool OnExiting(ScreenExitEvent e)
{
freeModSelect.Hide();
return base.OnExiting(e);
}
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = isValidRequiredMod
};
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
{
var baseButtons = base.CreateSongSelectFooterButtons().ToList();
baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip;
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
{
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect)
{
FreeMods = { BindTarget = FreeMods },
Freestyle = { BindTarget = Freestyle }
}, null),
(new FooterButtonFreestyle
{
Freestyle = { BindTarget = Freestyle }
}, null)
});
return baseButtons;
}
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid to be selected as a required mod.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
private bool isValidRequiredMod(Mod mod) => ModUtils.IsValidModForMatch(mod, true, room.Type, Freestyle.Value);
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid to be selected as an allowed mod.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
private bool isValidAllowedMod(Mod mod) => ModUtils.IsValidModForMatch(mod, false, room.Type, Freestyle.Value)
// Mod must not be contained in the required mods.
&& Mods.Value.All(m => m.Acronym != mod.Acronym)
// Mod must be compatible with all the required mods.
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray());
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
freeModSelectOverlayRegistration?.Dispose();
}
}
}
@@ -404,7 +404,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
EditPlaylist = () =>
{
if (this.IsCurrentScreen())
this.Push(new PlaylistsSongSelectV2(room));
this.Push(new PlaylistsSongSelect(room));
}
}
}
@@ -696,7 +696,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
if (!this.IsCurrentScreen() || SelectedItem.Value == null)
return;
this.Push(new PlaylistsRoomFreestyleSelect(room, SelectedItem.Value)
this.Push(new PlaylistsRoomFreestyleSelect(SelectedItem.Value)
{
Beatmap = { BindTarget = UserBeatmap },
Ruleset = { BindTarget = UserRuleset }
@@ -20,7 +20,7 @@ using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public partial class PlaylistsSongSelectV2
public partial class PlaylistsSongSelect
{
public partial class PlaylistTray : CompositeDrawable
{
@@ -23,7 +23,7 @@ using osu.Game.Utils;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public partial class PlaylistsSongSelectV2 : SongSelect, IOnlinePlaySubScreen
public partial class PlaylistsSongSelect : SongSelect, IOnlinePlaySubScreen
{
public string ShortTitle => "song selection";
@@ -43,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private IDisposable? modSelectOverlayRegistration;
public PlaylistsSongSelectV2(Room room)
public PlaylistsSongSelect(Room room)
{
this.room = room;
@@ -7,28 +7,24 @@ using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
namespace osu.Game.Screens.OnlinePlay.Playlists
namespace osu.Game.Screens.OnlinePlay
{
public partial class PlaylistsRoomFreestyleSelect : OnlinePlayFreestyleSelect
{
public new readonly Bindable<BeatmapInfo?> Beatmap = new Bindable<BeatmapInfo?>();
public new readonly Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
public PlaylistsRoomFreestyleSelect(Room room, PlaylistItem item)
: base(room, item)
public PlaylistsRoomFreestyleSelect(PlaylistItem item)
: base(item)
{
}
protected override bool OnStart()
protected override void StartAction()
{
if (!base.OnStart())
return false;
Beatmap.Value = base.Beatmap.Value.BeatmapInfo;
Ruleset.Value = base.Ruleset.Value;
this.Exit();
return true;
}
}
}
@@ -114,7 +114,10 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
FillMode = FillMode.Fill,
},
loading = new LoadingLayer(dimBackground: true, blockInput: false)
loading = new LoadingLayer(dimBackground: true)
{
BlockPositionalInput = false,
}
}
},
versionFlow = new FillFlowContainer
@@ -21,6 +21,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
using osuTK;
@@ -258,6 +259,8 @@ namespace osu.Game.Screens.Ranking.Statistics
preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!";
else if (localUserScore.Rank < ScoreRank.C)
preventTaggingReason = "Set a better score to contribute to beatmap tags!";
else if (localUserScore.Mods.Any(m => (m.Type == ModType.Conversion) && !(m is ModClassic)))
preventTaggingReason = "Play this beatmap without conversion mods to contribute to beatmap tags!";
if (preventTaggingReason == null)
{
@@ -3,6 +3,7 @@
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Ranking.Statistics.User
@@ -15,6 +16,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
}
protected override LocalisableString Label => UsersStrings.ShowStatsRankedScore;
public override LocalisableString TooltipText => RankingStatisticsStrings.ClassicScoringAlwaysUsed;
protected override LocalisableString FormatCurrentValue(long current) => current.ToLocalisableString(@"N0");
@@ -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.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@@ -17,7 +18,7 @@ using osuTK;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public abstract partial class RankingChangeRow<T> : CompositeDrawable
public abstract partial class RankingChangeRow<T> : CompositeDrawable, IHasTooltip
{
public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
@@ -153,6 +154,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
}
protected abstract LocalisableString Label { get; }
public virtual LocalisableString TooltipText => default;
protected abstract LocalisableString FormatCurrentValue(T current);
protected abstract int CalculateDifference(T previous, T current, out LocalisableString formattedDifference);
@@ -3,6 +3,7 @@
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Ranking.Statistics.User
@@ -15,6 +16,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
}
protected override LocalisableString Label => UsersStrings.ShowStatsTotalScore;
public override LocalisableString TooltipText => RankingStatisticsStrings.ClassicScoringAlwaysUsed;
protected override LocalisableString FormatCurrentValue(long current) => current.ToLocalisableString(@"N0");
@@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader]
private void load()
{
CornerRadius = 5;
CornerRadius = 10;
Masking = true;
EdgeEffect = new EdgeEffectParameters
@@ -22,13 +22,8 @@ namespace osu.Game.Screens.SelectV2
{
public partial class ScopedBeatmapSetDisplay : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
private Box flashLayer = null!;
private Container content = null!;
private OsuTextFlowContainer text = null!;
@@ -44,7 +39,7 @@ namespace osu.Game.Screens.SelectV2
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
private void load(ISongSelect? songSelect, OverlayColourProvider colourProvider)
{
Content.AutoSizeEasing = Easing.OutQuint;
Content.AutoSizeDuration = transition_duration;
@@ -97,25 +92,25 @@ namespace osu.Game.Screens.SelectV2
Alpha = 0,
},
});
Action = () => scopedBeatmapSet.Value = null;
Action = () => songSelect?.UnscopeBeatmapSet();
}
protected override void LoadComplete()
{
base.LoadComplete();
scopedBeatmapSet.BindValueChanged(_ => updateState(), true);
ScopedBeatmapSet.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
if (scopedBeatmapSet.Value != null)
if (ScopedBeatmapSet.Value != null)
{
content.BypassAutoSizeAxes = Axes.None;
text.Clear();
text.AddText(SongSelectStrings.TemporarilyShowingAllBeatmapsIn);
text.AddText(@" ");
text.AddText(scopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold));
text.AddText(ScopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold));
}
else
{
@@ -126,7 +121,7 @@ namespace osu.Game.Screens.SelectV2
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (scopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat)
if (ScopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat)
{
TriggerClick();
return true;
+20 -21
View File
@@ -32,14 +32,14 @@ using osuTK.Input;
namespace osu.Game.Screens.SelectV2
{
public partial class FilterControl : OverlayContainer
public sealed partial class FilterControl : OverlayContainer
{
// taken from draw visualiser. used for carousel alignment purposes.
public const float HEIGHT_FROM_SCREEN_TOP = 141 - corner_radius;
private const float corner_radius = 10;
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
private SongSelectSearchTextBox searchTextBox = null!;
private ShearedToggleButton showConvertedBeatmapsButton = null!;
@@ -48,6 +48,14 @@ namespace osu.Game.Screens.SelectV2
private ShearedDropdown<GroupMode> groupDropdown = null!;
private CollectionDropdown collectionDropdown = null!;
/// <summary>
/// An optional method which can force certain criteria adjustments.
/// </summary>
public Action<FilterCriteria>? ApplyRequiredCriteria { get; set; }
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -115,7 +123,7 @@ namespace osu.Game.Screens.SelectV2
{
RelativeSizeAxes = Axes.X,
HoldFocus = true,
ScopedBeatmapSet = ScopedBeatmapSet,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
},
},
new GridContainer
@@ -190,7 +198,7 @@ namespace osu.Game.Screens.SelectV2
},
new ScopedBeatmapSetDisplay
{
ScopedBeatmapSet = ScopedBeatmapSet,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
}
},
}
@@ -291,6 +299,9 @@ namespace osu.Game.Screens.SelectV2
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
FilterQueryParser.ApplyQueries(criteria, query);
ApplyRequiredCriteria?.Invoke(criteria);
return criteria;
}
@@ -298,7 +309,7 @@ namespace osu.Game.Screens.SelectV2
{
if (clearScopedSet && ScopedBeatmapSet.Value != null)
{
ScopedBeatmapSet.Value = null;
songSelect?.UnscopeBeatmapSet();
// because `ScopedBeatmapSet` has a value change callback bound to it that calls `updateCriteria()` again,
// we can just do nothing other than clear it to avoid extra work and duplicated `CriteriaChanged` invocations
return;
@@ -331,34 +342,22 @@ namespace osu.Game.Screens.SelectV2
internal partial class SongSelectSearchTextBox : ShearedFilterTextBox
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox
{
ScopedBeatmapSet = ScopedBeatmapSet,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
};
private partial class InnerTextBox : InnerFilterTextBox
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
public override bool HandleLeftRightArrows => false;
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Back && scopedBeatmapSet.Value != null)
if (e.Action == GlobalAction.Back && ScopedBeatmapSet.Value != null)
return false;
return base.OnPressed(e);
+15 -2
View File
@@ -48,8 +48,21 @@ namespace osu.Game.Screens.SelectV2
IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap);
/// <summary>
/// Set this to a non-<see langword="null"/> value in order to temporarily bypass filter and show all difficulties of the given beatmap set.
/// Temporarily bypasses filters and shows all difficulties of the given beatmapset.
/// </summary>
Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; }
/// <param name="beatmapSet">The beatmapset.</param>
void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet);
/// <summary>
/// Removes the beatmapset scope and reverts the previously selected filters.
/// </summary>
void UnscopeBeatmapSet();
/// <summary>
/// Contains the currently scoped beatmapset. Used by external consumers for displaying its state.
/// Cannot be used to change the value, any changes must be done through <see cref="ScopeToBeatmapSet"/>
/// or <see cref="UnscopeBeatmapSet"/>.
/// </summary>
IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; }
}
}
@@ -34,11 +34,14 @@ namespace osu.Game.Screens.SelectV2
protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly IBindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -59,7 +62,7 @@ namespace osu.Game.Screens.SelectV2
}
[BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager)
private void load(OsuConfigManager configManager)
{
Add(new FillFlowContainer
{
@@ -105,6 +108,7 @@ namespace osu.Game.Screens.SelectV2
showConvertedBeatmaps.BindValueChanged(_ => updateBeatmapSet(), true);
Expanded.BindValueChanged(_ => updateEnabled());
scopedBeatmapSet.BindValueChanged(_ => updateEnabled(), true);
scopedBeatmapSet.BindDisabledChanged(_ => updateEnabled(), true);
Enabled.BindValueChanged(_ => updateAppearance(), true);
FinishTransforms(true);
}
@@ -201,13 +205,13 @@ namespace osu.Game.Screens.SelectV2
}
}
Action = () => scopedBeatmapSet.Value = BeatmapSet.Value;
Action = () => songSelect?.ScopeToBeatmapSet(BeatmapSet.Value);
updateEnabled();
}
private void updateEnabled()
{
Enabled.Value = Expanded.Value && scopedBeatmapSet.Value == null;
Enabled.Value = Expanded.Value && !scopedBeatmapSet.Disabled && scopedBeatmapSet.Value == null;
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -29,11 +29,14 @@ namespace osu.Game.Screens.SelectV2
protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly IBindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -56,12 +59,12 @@ namespace osu.Game.Screens.SelectV2
Action = () =>
{
if (Beatmap.Value != null)
scopedBeatmapSet.Value = Beatmap.Value.BeatmapSet!;
songSelect?.ScopeToBeatmapSet(Beatmap.Value.BeatmapSet!);
};
}
[BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager)
private void load(OsuConfigManager configManager)
{
Add(new FillFlowContainer
{
+39 -9
View File
@@ -93,6 +93,12 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
protected bool ControlGlobalMusic { get; init; } = true;
/// <summary>
/// Whether this song select instance should allow scoping down to a specific beatmap set,
/// exposing other difficulties that are otherwise hidden by filter criteria.
/// </summary>
protected bool SupportScoping { init => scopedBeatmapSet.Disabled = !value; }
/// <summary>
/// Whether the osu! logo should be shown at the bottom-right of the screen.
/// </summary>
@@ -111,7 +117,8 @@ namespace osu.Game.Screens.SelectV2
private BeatmapCarousel carousel = null!;
private FilterControl filterControl = null!;
protected FilterControl FilterControl { get; private set; } = null!;
private BeatmapTitleWedge titleWedge = null!;
private BeatmapDetailsArea detailsArea = null!;
private FillFlowContainer wedgesContainer = null!;
@@ -275,15 +282,16 @@ namespace osu.Game.Screens.SelectV2
},
noResultsPlaceholder = new NoResultsPlaceholder
{
RequestClearFilterText = () => filterControl.Search(string.Empty)
RequestClearFilterText = () => FilterControl.Search(string.Empty)
}
}
},
filterControl = new FilterControl
FilterControl = new FilterControl
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
},
}
},
@@ -378,7 +386,7 @@ namespace osu.Game.Screens.SelectV2
inputManager = GetContainingInputManager()!;
filterControl.CriteriaChanged += criteriaChanged;
FilterControl.CriteriaChanged += criteriaChanged;
modSelectOverlay.State.BindValueChanged(v =>
{
@@ -820,13 +828,13 @@ namespace osu.Game.Screens.SelectV2
{
titleWedge.Hide();
detailsArea.Hide();
filterControl.Hide();
FilterControl.Hide();
}
else
{
titleWedge.Show();
detailsArea.Show();
filterControl.Show();
FilterControl.Show();
}
}
@@ -889,7 +897,7 @@ namespace osu.Game.Screens.SelectV2
// Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
// but also in this case we want support for formatting a number within a string).
filterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match";
FilterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match";
// If there's already a selection update in progress, let's not interrupt it.
// Interrupting could cause the debounce interval to be reduced.
@@ -1147,7 +1155,7 @@ namespace osu.Game.Screens.SelectV2
#region Implementation of ISongSelect
void ISongSelect.Search(string query) => filterControl.Search(query);
void ISongSelect.Search(string query) => FilterControl.Search(query);
void ISongSelect.PresentScore(ScoreInfo score)
{
@@ -1242,7 +1250,29 @@ namespace osu.Game.Screens.SelectV2
beatmaps.Restore(b);
}
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet => filterControl.ScopedBeatmapSet;
private GroupedBeatmap? beforeScopedSelection;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet => scopedBeatmapSet;
public void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet)
{
beforeScopedSelection = carousel.CurrentGroupedBeatmap;
scopedBeatmapSet.Value = beatmapSet;
}
public void UnscopeBeatmapSet()
{
if (scopedBeatmapSet.Value == null)
return;
if (beforeScopedSelection != null)
queueBeatmapSelection(beforeScopedSelection);
scopedBeatmapSet.Value = null;
beforeScopedSelection = null;
}
#endregion
+1
View File
@@ -11,5 +11,6 @@ namespace osu.Game.Skinning
Score,
Combo,
HitCircle,
ScoreEntry,
}
}
+41 -9
View File
@@ -27,14 +27,19 @@ namespace osu.Game.Skinning
set
{
textColour = value;
initialNameText.Colour = value;
overlayKeyText.Colour = value;
}
}
private readonly Container keyContainer;
private readonly OsuSpriteText overlayKeyText;
private readonly LegacySpriteText overlayKeyText;
private readonly OsuSpriteText initialNameText;
private readonly Sprite keySprite;
private bool activatedOnce;
public LegacyKeyCounter(InputTrigger trigger)
: base(trigger)
{
@@ -57,14 +62,26 @@ namespace osu.Game.Skinning
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = overlayKeyText = new OsuSpriteText
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = trigger.Name,
Colour = textColour,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
},
// The legacy font doesn't contain all the characters necessary to display placeholders.
// Keep things simple by using a normal font for this case.
initialNameText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = trigger.Name,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
Colour = textColour,
},
overlayKeyText = new LegacySpriteText(LegacyFont.ScoreEntry)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Colour = textColour,
}
}
},
}
};
@@ -85,10 +102,18 @@ namespace osu.Game.Skinning
protected override void Activate(bool forwardPlayback = true)
{
base.Activate(forwardPlayback);
keyContainer.ScaleTo(0.75f, transition_duration, Easing.Out);
keySprite.Colour = ActiveColour;
overlayKeyText.Text = CountPresses.Value.ToString();
overlayKeyText.Font = overlayKeyText.Font.With(weight: FontWeight.SemiBold);
if (forwardPlayback && !activatedOnce)
{
activatedOnce = true;
initialNameText.FadeOut(transition_duration, Easing.Out);
overlayKeyText.FadeIn(transition_duration, Easing.Out);
}
}
protected override void Deactivate(bool forwardPlayback = true)
@@ -96,6 +121,13 @@ namespace osu.Game.Skinning
base.Deactivate(forwardPlayback);
keyContainer.ScaleTo(1f, transition_duration, Easing.Out);
keySprite.Colour = Colour4.White;
if (!forwardPlayback && activatedOnce && CountPresses.Value == 0)
{
activatedOnce = false;
initialNameText.FadeIn(transition_duration, Easing.Out);
overlayKeyText.FadeOut(transition_duration, Easing.Out);
}
}
}
}
+8 -1
View File
@@ -55,7 +55,8 @@ namespace osu.Game.Skinning
}
}
public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, Vector2? maxSize, out ISkin? retrievalSource)
public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, Vector2? maxSize,
out ISkin? retrievalSource)
{
retrievalSource = null;
@@ -140,6 +141,9 @@ namespace osu.Game.Skinning
{
switch (font)
{
case LegacyFont.ScoreEntry:
return "scoreentry";
case LegacyFont.Score:
return source.GetConfig<LegacySetting, string>(LegacySetting.ScorePrefix)?.Value ?? "score";
@@ -163,6 +167,9 @@ namespace osu.Game.Skinning
{
switch (font)
{
case LegacyFont.ScoreEntry:
return 1;
case LegacyFont.Score:
return source.GetConfig<LegacySetting, float>(LegacySetting.ScoreOverlap)?.Value ?? 0f;
+1 -1
View File
@@ -36,7 +36,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.209.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.210.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.223.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" />