mirror of
https://github.com/ppy/osu.git
synced 2026-06-07 15:24:59 +08:00
Compare commits
52 Commits
@@ -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
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+4
-8
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@ namespace osu.Game.Skinning
|
||||
Score,
|
||||
Combo,
|
||||
HitCircle,
|
||||
ScoreEntry,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user