mirror of
https://github.com/ppy/osu.git
synced 2026-06-03 03:20:16 +08:00
Merge branch 'master' into multiplayer-position-indicator
This commit is contained in:
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
// needs to be scaled down to remain playable.
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio);
|
||||
scaleContainer.Scale = new Vector2(Math.Min(1, base_aspect_ratio / aspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => toggleTouchControls(false));
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero));
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch2, Vector2.Zero));
|
||||
toggleTouchControls(false);
|
||||
});
|
||||
|
||||
#region Without touch controls
|
||||
|
||||
@@ -71,6 +76,35 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBetweenTwoColumns()
|
||||
{
|
||||
AddStep("touch after column 0", () =>
|
||||
{
|
||||
var column = getColumn(0);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 0.5f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 0 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 0 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
AddStep("touch before column 1", () =>
|
||||
{
|
||||
var column = getColumn(1);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-0.5f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 1 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(1).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 1 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(1).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region With touch controls
|
||||
@@ -132,6 +166,38 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchControlBetweenTwoColumns()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
|
||||
AddStep("touch after receptor 0", () =>
|
||||
{
|
||||
var column = getReceptor(0);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 1f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
|
||||
AddAssert("column 0 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(0).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 0 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getReceptor(0).Action.Value));
|
||||
AddStep("touch before receptor 1", () =>
|
||||
{
|
||||
var column = getReceptor(1);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-1f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 1 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(1).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 1 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getReceptor(1).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toggleTouchControls(bool enabled)
|
||||
|
||||
@@ -136,8 +136,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
switch (maniaLookup.Lookup)
|
||||
{
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(2));
|
||||
case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing:
|
||||
case LegacyManiaSkinConfigurationLookups.RightColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingBottom:
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingTop:
|
||||
@@ -151,7 +152,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(width));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
|
||||
var colour = getColourForLayout(columnIndex, stage);
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private IBindable<ManiaMobileLayout> mobilePlayStyle = null!;
|
||||
|
||||
private float leftColumnSpacing;
|
||||
private float rightColumnSpacing;
|
||||
|
||||
public Column(int index, bool isSpecial)
|
||||
{
|
||||
Index = index;
|
||||
@@ -126,6 +129,14 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private void onSourceChanged()
|
||||
{
|
||||
AccentColour.Value = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black;
|
||||
|
||||
leftColumnSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, Index))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
rightColumnSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, Index))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -187,8 +198,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
{
|
||||
// Extend input coverage to the gaps close to this column.
|
||||
var spacingInflation = new MarginPadding { Left = leftColumnSpacing, Right = rightColumnSpacing };
|
||||
return DrawRectangle.Inflate(spacingInflation).Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
#region Touch Input
|
||||
|
||||
|
||||
@@ -124,14 +124,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
float leftSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, i))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
float rightSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, i))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = leftSpacing, Right = rightSpacing };
|
||||
|
||||
float? width = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
receptorGridContent.Add(new ColumnInputReceptor
|
||||
{
|
||||
Action = { BindTarget = column.Action },
|
||||
Spacing = { BindTarget = Spacing },
|
||||
});
|
||||
receptorGridDimensions.Add(new Dimension());
|
||||
|
||||
@@ -122,6 +123,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public partial class ColumnInputReceptor : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
public readonly IBindable<float> Spacing = new BindableFloat();
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
@@ -159,6 +161,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
};
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// Extend input coverage to the gaps close to this receptor.
|
||||
=> DrawRectangle.Inflate(new Vector2(Spacing.Value / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
|
||||
@@ -259,6 +259,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||
|
||||
if (pos.X < 0 || pos.X > OsuPlayfield.BASE_SIZE.X || pos.Y < 0 || pos.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
return null;
|
||||
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonSliderBody : PlaySliderBody
|
||||
{
|
||||
// Eventually this would be a user setting.
|
||||
public float BodyAlpha { get; init; } = 1;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
|
||||
@@ -26,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
|
||||
|
||||
protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
|
||||
{
|
||||
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(BodyAlpha);
|
||||
}
|
||||
|
||||
private partial class DrawableSliderPath : Default.DrawableSliderPath
|
||||
{
|
||||
protected override Color4 ColourAt(float position)
|
||||
|
||||
@@ -16,13 +16,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
bool isPro = Skin is ArgonProSkin;
|
||||
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect))
|
||||
if (isPro && (result == HitResult.Great || result == HitResult.Perfect))
|
||||
return Drawable.Empty();
|
||||
|
||||
switch (result)
|
||||
@@ -46,7 +48,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
return new ArgonMainCirclePiece(false);
|
||||
|
||||
case OsuSkinComponents.SliderBody:
|
||||
return new ArgonSliderBody();
|
||||
return new ArgonSliderBody
|
||||
{
|
||||
BodyAlpha = isPro ? 0.92f : 0.98f
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
return new ArgonSliderBall();
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModSimplifiedRhythm : TaikoModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestOneThirdConversion()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSimplifiedRhythm
|
||||
{
|
||||
OneThirdConversion = { Value = true },
|
||||
},
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2333, Type = HitType.Rim }, // mod removes this
|
||||
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
|
||||
new Hit { StartTime = 3000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 3500, Type = HitType.Centre },
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1200),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1700),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2200),
|
||||
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2700),
|
||||
new TaikoReplayFrame(3000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(3200),
|
||||
new TaikoReplayFrame(3500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(3700),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOneSixthConversion() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSimplifiedRhythm
|
||||
{
|
||||
OneSixthConversion = { Value = true }
|
||||
},
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1250, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1666, Type = HitType.Rim }, // mod removes this
|
||||
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
|
||||
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2250, Type = HitType.Centre },
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1200),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1450),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1600),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1800),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2200),
|
||||
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2450),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestOneEighthConversion() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSimplifiedRhythm
|
||||
{
|
||||
OneEighthConversion = { Value = true }
|
||||
},
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () =>
|
||||
{
|
||||
const double one_eighth_timing = 125;
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1250, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500 + one_eighth_timing * 1, Type = HitType.Rim }, // mod removes this
|
||||
new Hit { StartTime = 1500 + one_eighth_timing * 2 },
|
||||
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 1, Type = HitType.Centre }, // mod removes this
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 2, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 3, Type = HitType.Centre }, // mod removes this
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 4, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 5, Type = HitType.Centre }, // mod removes this
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 6, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 7, Type = HitType.Centre }, // mod removes this
|
||||
},
|
||||
};
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1000),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1250),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1500),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1750),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2000),
|
||||
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2250),
|
||||
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2500),
|
||||
new TaikoReplayFrame(2750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2750),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
@@ -20,6 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
ReadCurrentFromDifficulty = _ => 1,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap
|
||||
{
|
||||
public override string Name => "Simplified Rhythm";
|
||||
public override string Acronym => "SR";
|
||||
public override double ScoreMultiplier => 0.6;
|
||||
public override LocalisableString Description => "Simplify tricky rhythms!";
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
|
||||
[SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")]
|
||||
public Bindable<bool> OneThirdConversion { get; } = new BindableBool();
|
||||
|
||||
[SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")]
|
||||
public Bindable<bool> OneSixthConversion { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")]
|
||||
public Bindable<bool> OneEighthConversion { get; } = new BindableBool();
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var taikoBeatmap = (TaikoBeatmap)beatmap;
|
||||
var controlPointInfo = taikoBeatmap.ControlPointInfo;
|
||||
|
||||
Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToArray();
|
||||
|
||||
if (hits.Length == 0)
|
||||
return;
|
||||
|
||||
var conversions = new List<(int, int)>();
|
||||
|
||||
if (OneEighthConversion.Value) conversions.Add((8, 4));
|
||||
if (OneSixthConversion.Value) conversions.Add((6, 4));
|
||||
if (OneThirdConversion.Value) conversions.Add((3, 2));
|
||||
|
||||
bool inPattern = false;
|
||||
|
||||
foreach ((int baseRhythm, int adjustedRhythm) in conversions)
|
||||
{
|
||||
int patternStartIndex = 0;
|
||||
|
||||
for (int i = 1; i < hits.Length; i++)
|
||||
{
|
||||
double snapValue = getSnapBetweenNotes(controlPointInfo, hits[i - 1], hits[i]);
|
||||
|
||||
if (inPattern)
|
||||
{
|
||||
// pattern continues
|
||||
if (snapValue == baseRhythm) continue;
|
||||
|
||||
inPattern = false;
|
||||
|
||||
processPattern(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (snapValue == baseRhythm)
|
||||
{
|
||||
patternStartIndex = i - 1;
|
||||
inPattern = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the last pattern if we reached the end of the beatmap and are still in a pattern.
|
||||
if (inPattern)
|
||||
processPattern(hits.Length);
|
||||
|
||||
void processPattern(int patternEndIndex)
|
||||
{
|
||||
// Iterate through the pattern
|
||||
for (int j = patternStartIndex; j < patternEndIndex; j++)
|
||||
{
|
||||
int indexInPattern = j - patternStartIndex;
|
||||
|
||||
switch (baseRhythm)
|
||||
{
|
||||
// 1/8: Remove every second note
|
||||
case 8:
|
||||
{
|
||||
if (indexInPattern % 2 == 1)
|
||||
{
|
||||
taikoBeatmap.HitObjects.Remove(hits[j]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// 1/6 and 1/3: Remove every second note and adjust time of every third
|
||||
case 6:
|
||||
case 3:
|
||||
{
|
||||
if (indexInPattern % 3 == 1)
|
||||
taikoBeatmap.HitObjects.Remove(hits[j]);
|
||||
else if (indexInPattern % 3 == 2)
|
||||
hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(baseRhythm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote)
|
||||
{
|
||||
var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime);
|
||||
return controlPointInfo.GetClosestBeatDivisor(currentTimingPoint.Time + (nextNote.StartTime - currentNote.StartTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new TaikoModEasy(),
|
||||
new TaikoModNoFail(),
|
||||
new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()),
|
||||
new TaikoModSimplifiedRhythm(),
|
||||
};
|
||||
|
||||
case ModType.DifficultyIncrease:
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("toggle expanded", () =>
|
||||
{
|
||||
if (leaderboard.IsNotNull())
|
||||
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
|
||||
leaderboard.ForceExpand.Value = !leaderboard.ForceExpand.Value;
|
||||
});
|
||||
|
||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public void TestScoreUpdates()
|
||||
{
|
||||
AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
|
||||
AddToggleStep("switch compact mode", expanded => Leaderboard!.Expanded.Value = expanded);
|
||||
AddToggleStep("switch compact mode", expanded => Leaderboard!.ForceExpand.Value = expanded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Expanded = { Value = true }
|
||||
ForceExpand = { Value = true }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Origin = Anchor.BottomCentre,
|
||||
Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] },
|
||||
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] },
|
||||
Expanded = { BindTarget = Leaderboard!.Expanded },
|
||||
Expanded = { BindTarget = Leaderboard!.ForceExpand },
|
||||
}, Add);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,9 +75,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)]
|
||||
[TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)]
|
||||
[TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found)
|
||||
[TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)]
|
||||
[TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)]
|
||||
[TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)]
|
||||
[TestCase("Join my multiplayer game osu://room/12346.", LinkAction.JoinRoom)]
|
||||
[TestCase("Join my multiplayer gameosu://room/12346.", LinkAction.JoinRoom)]
|
||||
[TestCase("Join my [multiplayer game](osu://room/12346).", LinkAction.JoinRoom)]
|
||||
[TestCase("Join my multiplayer game http://dev.ppy.sh/multiplayer/rooms/12346", LinkAction.JoinRoom)]
|
||||
[TestCase("Join my [multiplayer game](http://dev.ppy.sh/multiplayer/rooms/12346).", LinkAction.JoinRoom)]
|
||||
[TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)]
|
||||
[TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
||||
[TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Overlays.Comments;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[Ignore("This test hits online resources (and online retrieval can fail at any time), and also performs network calls to the production instance of the website. Un-ignore this test when it's actually actively needed.")]
|
||||
public partial class TestSceneImageProxying : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneSoloResultsScreen : ScreenTestScene
|
||||
{
|
||||
private ScoreManager scoreManager = null!;
|
||||
private RulesetStore rulesetStore = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
|
||||
private LeaderboardManager leaderboardManager = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
|
||||
dependencies.Cache(leaderboardManager = new LeaderboardManager());
|
||||
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("load leaderboard manager", () => LoadComponent(leaderboardManager));
|
||||
|
||||
AddStep(@"set beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
Realm.Write(r =>
|
||||
{
|
||||
foreach (var set in r.All<BeatmapSetInfo>())
|
||||
set.Status = BeatmapOnlineStatus.Ranked;
|
||||
|
||||
foreach (var b in r.All<BeatmapInfo>())
|
||||
b.Status = BeatmapOnlineStatus.Ranked;
|
||||
});
|
||||
importedBeatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
});
|
||||
AddStep("clear all scores", () => Realm.Write(r => r.RemoveAll<ScoreInfo>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalLeaderboardWithOfflineScore()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Local, null)));
|
||||
AddStep("import some local scores", () =>
|
||||
{
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 10_000 * (30 - i);
|
||||
scoreManager.Import(score);
|
||||
}
|
||||
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
scoreManager.Import(localScore);
|
||||
localScore = localScore.Detach();
|
||||
});
|
||||
|
||||
AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore)));
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalLeaderboardWithOnlineScore()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Local, null)));
|
||||
AddStep("import some local scores", () =>
|
||||
{
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.OnlineID = i;
|
||||
score.TotalScore = 10_000 * (30 - i);
|
||||
scoreManager.Import(score);
|
||||
}
|
||||
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.OnlineID = 30;
|
||||
localScore.Position = null;
|
||||
scoreManager.Import(localScore);
|
||||
localScore = localScore.Detach();
|
||||
});
|
||||
|
||||
AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore)));
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithLessThan50Scores()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 10_000 * (30 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection { Scores = scores });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 300_000 + 10_000 * (30 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection { Scores = scores });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #31", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(31));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
userBest.TotalScore = 50_000;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
UserScore = new APIScoreWithPosition
|
||||
{
|
||||
Score = SoloScoreInfo.ForSubmission(userBest),
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score has no position", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null);
|
||||
AddAssert("user best position preserved", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_337));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserInTop50()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
userBest.TotalScore = 50_000;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
UserScore = new APIScoreWithPosition
|
||||
{
|
||||
Score = SoloScoreInfo.ForSubmission(userBest),
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 651_000;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("local score is #36", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36));
|
||||
AddAssert("user best position incremented by 1", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_338));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardDeduplication()
|
||||
{
|
||||
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetScoresRequest getScoresRequest:
|
||||
var scores = new List<SoloScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||
score.Position = i + 1;
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap));
|
||||
userBest.TotalScore = 151_000;
|
||||
userBest.ID = 12345;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
UserScore = new APIScoreWithPosition
|
||||
{
|
||||
Score = userBest,
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("show results", () =>
|
||||
{
|
||||
var localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
localScore.TotalScore = 151_000;
|
||||
localScore.OnlineID = 12345;
|
||||
localScore.Position = null;
|
||||
LoadScreen(new SoloResultsScreen(localScore));
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("only one score with ID 12345", () => this.ChildrenOfType<ScorePanel>().Count(s => s.Score.OnlineID == 12345), () => Is.EqualTo(1));
|
||||
AddAssert("user best position preserved", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_337));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
|
||||
@@ -1277,12 +1277,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl-x", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.X);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
|
||||
|
||||
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text, () => Is.Empty);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
@@ -48,6 +49,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
private int beatmapCount;
|
||||
|
||||
protected int NewItemsPresentedInvocationCount;
|
||||
|
||||
protected BeatmapCarouselTestScene()
|
||||
{
|
||||
store = new TestBeatmapStore
|
||||
@@ -64,6 +67,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create components", () =>
|
||||
{
|
||||
NewItemsPresentedInvocationCount = 0;
|
||||
|
||||
Box topBox;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@@ -97,6 +102,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
Carousel = new BeatmapCarousel
|
||||
{
|
||||
NewItemsPresented = () => NewItemsPresentedInvocationCount++,
|
||||
BleedTop = 50,
|
||||
BleedBottom = 50,
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -127,12 +133,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable.
|
||||
SortBy(SortMode.Title);
|
||||
}
|
||||
|
||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria));
|
||||
protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.ToString().ToLowerInvariant()}", c => c.Sort = mode);
|
||||
protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.ToString().ToLowerInvariant()}", c => c.Group = mode);
|
||||
|
||||
protected void SortAndGroupBy(SortMode sort, GroupMode group)
|
||||
{
|
||||
ApplyToFilter($"sort by {sort.ToString().ToLowerInvariant()} & group by {group.ToString().ToLowerInvariant()}", c =>
|
||||
{
|
||||
c.Sort = sort;
|
||||
c.Group = group;
|
||||
});
|
||||
}
|
||||
|
||||
protected void ApplyToFilter(string description, Action<FilterCriteria>? apply)
|
||||
{
|
||||
AddStep(description, () =>
|
||||
{
|
||||
var criteria = Carousel.Criteria;
|
||||
apply?.Invoke(criteria);
|
||||
Carousel.Filter(criteria);
|
||||
});
|
||||
}
|
||||
|
||||
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
|
||||
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
|
||||
protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False);
|
||||
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
|
||||
|
||||
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
||||
@@ -145,6 +174,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
protected void CheckDisplayedBeatmapsCount(int expected)
|
||||
{
|
||||
AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
protected void CheckDisplayedBeatmapSetsCount(int expected)
|
||||
{
|
||||
AddAssert($"{expected} sets displayed", () =>
|
||||
{
|
||||
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||
|
||||
// Using groupingFilter.SetItems.Count alone doesn't work.
|
||||
// When sorting by difficulty, there can be more than one set panel for the same set displayed.
|
||||
return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo));
|
||||
}, () => Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
protected void CheckDisplayedGroupsCount(int expected)
|
||||
{
|
||||
AddAssert($"{expected} groups displayed", () =>
|
||||
{
|
||||
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||
return groupingFilter.GroupItems.Count;
|
||||
}, () => Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
};
|
||||
|
||||
private Container? resizeContainer;
|
||||
private float relativeWidth;
|
||||
|
||||
protected virtual Anchor ComponentAnchor => Anchor.TopLeft;
|
||||
protected virtual float InitialRelativeWidth => 0.5f;
|
||||
@@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Origin = ComponentAnchor,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = relativeWidth,
|
||||
Width = InitialRelativeWidth,
|
||||
Child = Content
|
||||
}
|
||||
};
|
||||
@@ -49,8 +48,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
if (resizeContainer != null)
|
||||
resizeContainer.Width = v;
|
||||
|
||||
relativeWidth = v;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
@@ -34,9 +33,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Explicit]
|
||||
public void TestSorting()
|
||||
{
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.All);
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -5,9 +5,10 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
@@ -19,7 +20,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
||||
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||
|
||||
AddBeatmaps(10, 3, true);
|
||||
WaitForDrawablePanels();
|
||||
@@ -173,5 +175,71 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(1, 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInputHandlingWithinGaps()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
// Clicks just above the first group panel should not actuate any action.
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1)));
|
||||
|
||||
AddAssert("no sets visible", () => !GetVisiblePanels<PanelBeatmapSet>().Any());
|
||||
|
||||
// add lenience to avoid floating-point inaccuracies at edge.
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1)));
|
||||
|
||||
AddUntilStep("wait for sets visible", () => GetVisiblePanels<PanelBeatmapSet>().Any());
|
||||
CheckNoSelection();
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFiltering()
|
||||
{
|
||||
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(1);
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
CheckNoSelection();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
SelectNextPanel();
|
||||
|
||||
Select();
|
||||
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(5);
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
@@ -21,7 +20,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
@@ -179,7 +179,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2)));
|
||||
// add lenience to avoid floating-point inaccuracies at edge.
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1)));
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||
CheckNoSelection();
|
||||
@@ -191,5 +192,37 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFiltering()
|
||||
{
|
||||
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(3);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
CheckNoSelection();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
SelectNextPanel();
|
||||
|
||||
Select();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
WaitForGroupSelection(1, 0);
|
||||
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(3);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselFiltering : BeatmapCarouselTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFiltering()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
|
||||
|
||||
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
WaitForSelection(2, 0);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
SelectNextPanel();
|
||||
|
||||
Select();
|
||||
WaitForSelection(2, 1);
|
||||
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilteringByUserStarDifficulty()
|
||||
{
|
||||
AddStep("add mixed difficulty set", () =>
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(1);
|
||||
set.Beatmaps.Clear();
|
||||
|
||||
for (int i = 1; i <= 15; i++)
|
||||
{
|
||||
set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata())
|
||||
{
|
||||
BeatmapSet = set,
|
||||
DifficultyName = $"Stars: {i}",
|
||||
StarRating = i,
|
||||
});
|
||||
}
|
||||
|
||||
BeatmapSets.Add(set);
|
||||
});
|
||||
|
||||
WaitForDrawablePanels();
|
||||
|
||||
ApplyToFilter("filter [5..]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 5;
|
||||
c.UserStarDifficulty.Max = null;
|
||||
});
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(11);
|
||||
|
||||
ApplyToFilter("filter to [0..7]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = null;
|
||||
c.UserStarDifficulty.Max = 7;
|
||||
});
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(7);
|
||||
|
||||
ApplyToFilter("filter to [5..7]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 5;
|
||||
c.UserStarDifficulty.Max = 7;
|
||||
});
|
||||
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
ApplyToFilter("filter to [2..2]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 2;
|
||||
c.UserStarDifficulty.Max = 2;
|
||||
});
|
||||
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(1);
|
||||
|
||||
ApplyToFilter("filter to [0..]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 0;
|
||||
c.UserStarDifficulty.Max = null;
|
||||
});
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(15);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
Guid selectedID = Guid.Empty;
|
||||
|
||||
AddBeatmaps(50, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelectionDifficultySort()
|
||||
{
|
||||
Guid selectedID = Guid.Empty;
|
||||
|
||||
AddBeatmaps(50, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
|
||||
SelectNextGroup();
|
||||
|
||||
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRetainsSelectionFromDifficultySort()
|
||||
{
|
||||
AddBeatmaps(50, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
BeatmapInfo chosenBeatmap = null!;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int diff = i;
|
||||
|
||||
AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]);
|
||||
AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
SortBy(SortMode.Title);
|
||||
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternalRulesetChange()
|
||||
{
|
||||
ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true);
|
||||
ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0));
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddStep("add mixed ruleset beatmapset", () =>
|
||||
{
|
||||
var testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
|
||||
for (int i = 0; i <= 2; i++)
|
||||
testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
|
||||
|
||||
BeatmapSets.Add(testMixed);
|
||||
});
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1;
|
||||
});
|
||||
|
||||
ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1));
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 2
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1;
|
||||
});
|
||||
|
||||
ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2));
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 2
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Difficulty sorting is broken when set headers are included.")] // todo: fix.
|
||||
public void TestSortingWithDifficultyFiltered()
|
||||
{
|
||||
const int diffs_per_set = 3;
|
||||
const int local_set_count = 2;
|
||||
|
||||
AddStep("populate beatmap sets", () =>
|
||||
{
|
||||
for (int i = 0; i < local_set_count; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(diffs_per_set);
|
||||
set.Beatmaps[0].StarRating = 3 - i;
|
||||
set.Beatmaps[0].DifficultyName += $" ({3 - i}*)";
|
||||
set.Beatmaps[1].StarRating = 6 + i;
|
||||
set.Beatmaps[1].DifficultyName += $" ({6 + i}*)";
|
||||
BeatmapSets.Add(set);
|
||||
}
|
||||
});
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(3);
|
||||
CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set);
|
||||
|
||||
ApplyToFilter("filter to normal", c => c.SearchText = "Normal");
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(local_set_count);
|
||||
CheckDisplayedBeatmapsCount(local_set_count);
|
||||
|
||||
ApplyToFilter("filter to insane", c => c.SearchText = "Insane");
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(local_set_count);
|
||||
CheckDisplayedBeatmapsCount(local_set_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@@ -22,7 +20,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Title });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -221,7 +218,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2)));
|
||||
// add lenience to avoid floating-point inaccuracies at edge.
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 - 1)));
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
@@ -18,7 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria());
|
||||
|
||||
SortBy(SortMode.Artist);
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
@@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
@@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
BeatmapSets.Add(baseTestBeatmap);
|
||||
});
|
||||
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap]));
|
||||
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
|
||||
}
|
||||
|
||||
@@ -78,21 +78,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
updateBeatmap(b => b.Metadata = metadata);
|
||||
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
AddAssert("drawables changed", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.EqualTo(originalDrawables));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectionHeld()
|
||||
{
|
||||
SelectPrevGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
WaitForSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
updateBeatmap();
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
@@ -101,14 +101,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test] // Checks that we keep selection based on online ID where possible.
|
||||
public void TestSelectionHeldDifficultyNameChanged()
|
||||
{
|
||||
SelectPrevGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
WaitForSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
updateBeatmap(b => b.DifficultyName = "new name");
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
@@ -117,14 +117,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test] // Checks that we fallback to keeping selection based on difficulty name.
|
||||
public void TestSelectionHeldDifficultyOnlineIDChanged()
|
||||
{
|
||||
SelectPrevGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
WaitForSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene
|
||||
{
|
||||
private FilterControl filterControl = null!;
|
||||
|
||||
protected override Anchor ComponentAnchor => Anchor.TopRight;
|
||||
protected override float InitialRelativeWidth => 0.7f;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = filterControl = new FilterControl
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestSearch()
|
||||
{
|
||||
AddStep("search for text", () => filterControl.Search("test search"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-33
@@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
@@ -28,7 +29,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneLeaderboardScore : SongSelectComponentsTestScene
|
||||
public partial class TestSceneBeatmapLeaderboardScore : SongSelectComponentsTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
@@ -44,18 +45,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
@@ -78,22 +84,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
{
|
||||
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
|
||||
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false)
|
||||
{
|
||||
Rank = scoreInfo.Position,
|
||||
IsPersonalBest = scoreInfo.User.Id == 2,
|
||||
@@ -112,18 +123,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
var scoreInfo = new ScoreInfo
|
||||
@@ -260,9 +276,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
scores[2].TotalScore = RNG.Next(120_000, 400_000);
|
||||
scores[2].MaximumStatistics[HitResult.Great] = 3000;
|
||||
|
||||
scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() };
|
||||
scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 2 } }, new OsuModHardRock(), new OsuModFlashlight() };
|
||||
scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() };
|
||||
scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() };
|
||||
scores[3].Mods = new Mod[]
|
||||
{ new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust { CircleSize = { Value = 3.2f } } };
|
||||
scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray();
|
||||
|
||||
return scores;
|
||||
@@ -0,0 +1,370 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.SongSelect;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapLeaderboardWedge : SongSelectComponentsTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private TestBeatmapLeaderboardWedge leaderboard = null!;
|
||||
private ScoreManager scoreManager = null!;
|
||||
private RulesetStore rulesetStore = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private OsuContextMenuContainer contentContainer = null!;
|
||||
private DialogOverlay dialogOverlay = null!;
|
||||
|
||||
private LeaderboardManager leaderboardManager = null!;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
|
||||
dependencies.Cache(leaderboardManager = new LeaderboardManager());
|
||||
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LoadComponent(dialogOverlay = new DialogOverlay
|
||||
{
|
||||
Depth = -1
|
||||
});
|
||||
|
||||
LoadComponent(leaderboardManager);
|
||||
|
||||
Child = contentContainer = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 500,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
dialogOverlay,
|
||||
}
|
||||
};
|
||||
|
||||
AddSliderStep("change relative height", 0f, 1f, 0.65f, v => Schedule(() =>
|
||||
{
|
||||
contentContainer.Height = v * DrawHeight;
|
||||
}));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
if (leaderboard.IsNotNull())
|
||||
contentContainer.Remove(leaderboard, false);
|
||||
|
||||
contentContainer.Add(leaderboard = new TestBeatmapLeaderboardWedge
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
});
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPersonalBest()
|
||||
{
|
||||
AddStep(@"Show personal best", showPersonalBest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGlobalScoresDisplay()
|
||||
{
|
||||
setScope(BeatmapLeaderboardScope.Global);
|
||||
|
||||
AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo())));
|
||||
AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s =>
|
||||
{
|
||||
s.User.Team = new APITeam();
|
||||
return s;
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPersonalBestWithNullPosition()
|
||||
{
|
||||
AddStep("null personal best position", showPersonalBestWithNullPosition);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderStates()
|
||||
{
|
||||
AddStep("ensure no scores displayed", () => leaderboard.SetScores(Array.Empty<ScoreInfo>()));
|
||||
|
||||
AddStep(@"Retrieving", () => leaderboard.SetState(LeaderboardState.Retrieving));
|
||||
AddStep(@"Network failure", () => leaderboard.SetState(LeaderboardState.NetworkFailure));
|
||||
AddStep(@"No team", () => leaderboard.SetState(LeaderboardState.NoTeam));
|
||||
AddStep(@"No supporter", () => leaderboard.SetState(LeaderboardState.NotSupporter));
|
||||
AddStep(@"Not logged in", () => leaderboard.SetState(LeaderboardState.NotLoggedIn));
|
||||
AddStep(@"Ruleset unavailable", () => leaderboard.SetState(LeaderboardState.RulesetUnavailable));
|
||||
AddStep(@"Beatmap unavailable", () => leaderboard.SetState(LeaderboardState.BeatmapUnavailable));
|
||||
AddStep(@"None selected", () => leaderboard.SetState(LeaderboardState.NoneSelected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseTheseModsDoesNotCopySystemMods()
|
||||
{
|
||||
AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Position = 999,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), },
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
}
|
||||
}));
|
||||
AddUntilStep("wait for scores", () => this.ChildrenOfType<BeatmapLeaderboardScore>().Count(), () => Is.GreaterThan(0));
|
||||
AddStep("right click panel", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapLeaderboardScore>().Last());
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddStep("click use these mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableOsuMenuItem>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("received HD", () => this.ChildrenOfType<BeatmapLeaderboardScore>().Last().SelectedMods.Value.Any(m => m is OsuModHidden));
|
||||
AddAssert("did not receive SV2", () => !this.ChildrenOfType<BeatmapLeaderboardScore>().Last().SelectedMods.Value.Any(m => m is ModScoreV2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplay()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Set beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayWorksWhenStartingOffline()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
AddStep("Log out", () => API.Logout());
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayOnBeatmapEdit()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
string originalHash = string.Empty;
|
||||
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
AddStep(@"Perform initial save to guarantee stable hash", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
|
||||
originalHash = beatmapInfo.Hash;
|
||||
});
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(10);
|
||||
|
||||
AddStep(@"Save with changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 12;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash));
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
checkStoredCount(30);
|
||||
|
||||
AddStep(@"Revert changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 8;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash));
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(30);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(0);
|
||||
}
|
||||
|
||||
private void showPersonalBestWithNullPosition()
|
||||
{
|
||||
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void showPersonalBest()
|
||||
{
|
||||
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Position = 999,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setScope(BeatmapLeaderboardScope scope)
|
||||
{
|
||||
AddStep(@"Set scope", () => ((Bindable<BeatmapLeaderboardScope>)leaderboard.Scope).Value = scope);
|
||||
}
|
||||
|
||||
private void importMoreScores(Func<BeatmapInfo> beatmapInfo)
|
||||
{
|
||||
AddStep(@"Import new scores", () =>
|
||||
{
|
||||
foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo()))
|
||||
scoreManager.Import(score);
|
||||
});
|
||||
}
|
||||
|
||||
private void clearScores()
|
||||
{
|
||||
AddStep("Clear all scores", () => scoreManager.Delete());
|
||||
}
|
||||
|
||||
private void checkDisplayedCount(int expected) =>
|
||||
AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType<BeatmapLeaderboardScore>().Count(), () => Is.EqualTo(expected));
|
||||
|
||||
private void checkStoredCount(int expected) =>
|
||||
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
|
||||
|
||||
private partial class TestBeatmapLeaderboardWedge : BeatmapLeaderboardWedge
|
||||
{
|
||||
public new void SetState(LeaderboardState state) => base.SetState(state);
|
||||
public new void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,63 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3));
|
||||
onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" };
|
||||
onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
|
||||
onlineSet.Beatmaps.Single().TopTags = Enumerable.Repeat(onlineSet.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("rating wedge visible", () => wedge.RatingsVisible);
|
||||
AddUntilStep("fail time wedge visible", () => wedge.FailRetryVisible);
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.ResetOnlineInfo();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible);
|
||||
AddUntilStep("fail time wedge hidden", () => !wedge.FailRetryVisible);
|
||||
AddStep("local beatmap", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = null;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible);
|
||||
AddAssert("fail time wedge still hidden", () => !wedge.FailRetryVisible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserTags()
|
||||
{
|
||||
AddStep("user tags", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("no user tags", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
onlineSet.Beatmaps.Single().TopTags = null;
|
||||
onlineSet.RelatedTags = null;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
@@ -164,13 +221,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
OnlineID = working.BeatmapInfo.OnlineID,
|
||||
PlayCount = 10000,
|
||||
PassCount = 4567,
|
||||
TopTags =
|
||||
[
|
||||
new APIBeatmapTag { TagId = 4, VoteCount = 1 },
|
||||
new APIBeatmapTag { TagId = 2, VoteCount = 1 },
|
||||
new APIBeatmapTag { TagId = 23, VoteCount = 5 },
|
||||
],
|
||||
FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
RelatedTags =
|
||||
[
|
||||
new APITag
|
||||
{
|
||||
Id = 2,
|
||||
Name = "song representation/simple",
|
||||
Description = "Accessible and straightforward map design."
|
||||
},
|
||||
new APITag
|
||||
{
|
||||
Id = 4,
|
||||
Name = "style/clean",
|
||||
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects."
|
||||
},
|
||||
new APITag
|
||||
{
|
||||
Id = 23,
|
||||
Name = "aim/aim control",
|
||||
Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual.SongSelect;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
@@ -26,6 +38,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
private BeatmapTitleWedge titleWedge = null!;
|
||||
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
|
||||
|
||||
private APIBeatmapSet? currentOnlineSet;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
@@ -36,11 +50,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapSetRequest set:
|
||||
if (set.ID == currentOnlineSet?.OnlineID)
|
||||
{
|
||||
set.TriggerSuccess(currentOnlineSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleWedge = new BeatmapTitleWedge
|
||||
@@ -115,11 +148,45 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddAssert("check visibility", () => titleWedge.Alpha > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "10,000");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.ResetOnlineInfo();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddStep("local beatmapset", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = null;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "-");
|
||||
}
|
||||
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
@@ -134,6 +201,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
checkDisplayedBPM(expectedDisplay);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestPerformanceWithLongBeatmap()
|
||||
{
|
||||
AddStep("select heavy beatmap", () => Beatmap.Value = new HeavyWorkingBeatmap(Audio));
|
||||
|
||||
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||
setRuleset(rulesetInfo);
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
AddStep("set ruleset", () => Ruleset.Value = rulesetInfo);
|
||||
@@ -155,5 +232,73 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
return label.Text == target;
|
||||
});
|
||||
}
|
||||
|
||||
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
|
||||
{
|
||||
var working = CreateWorkingBeatmap(Ruleset.Value);
|
||||
var onlineSet = new APIBeatmapSet
|
||||
{
|
||||
OnlineID = working.BeatmapSetInfo.OnlineID,
|
||||
FavouriteCount = 2345,
|
||||
Beatmaps = new[]
|
||||
{
|
||||
new APIBeatmap
|
||||
{
|
||||
OnlineID = working.BeatmapInfo.OnlineID,
|
||||
PlayCount = 10000,
|
||||
PassCount = 4567,
|
||||
UserPlayCount = 123,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
|
||||
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
|
||||
return (working, onlineSet);
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject;
|
||||
|
||||
private class HeavyWorkingBeatmap : WorkingBeatmap
|
||||
{
|
||||
private static readonly BeatmapInfo beatmap_info = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = "osuAuthor" },
|
||||
Artist = "osuArtist",
|
||||
Source = "osuSource",
|
||||
Title = "osuTitle"
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
StarRating = 6,
|
||||
DifficultyName = "osuVersion",
|
||||
Difficulty = new BeatmapDifficulty()
|
||||
};
|
||||
|
||||
public HeavyWorkingBeatmap(AudioManager audioManager)
|
||||
: base(beatmap_info, audioManager)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap()
|
||||
{
|
||||
List<HitObject> objects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 200_000; i++)
|
||||
objects.Add(new TestHitObject { StartTime = i * 1000 });
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = beatmap_info,
|
||||
HitObjects = objects
|
||||
};
|
||||
}
|
||||
|
||||
public override Texture? GetBackground() => null;
|
||||
public override Stream? GetStream(string storagePath) => null;
|
||||
protected override Track? GetBeatmapTrack() => null;
|
||||
protected internal override ISkin? GetSkin() => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneDifficultyRangeSlider : ThemeComparisonTestScene
|
||||
{
|
||||
private readonly BindableNumber<double> customStart = new BindableNumber<double>
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
private readonly BindableNumber<double> customEnd = new BindableNumber<double>(10)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
public TestSceneDifficultyRangeSlider()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
},
|
||||
new FilterControl.DifficultyRangeSlider
|
||||
{
|
||||
Width = 600,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1),
|
||||
LowerBound = customStart,
|
||||
UpperBound = customEnd,
|
||||
TooltipSuffix = "suffix",
|
||||
NubWidth = 32,
|
||||
MinRange = 0.1f,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,11 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
@@ -30,11 +28,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
public partial class TestSceneSongSelect : ScreenTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly ScreenFooter screenScreenFooter;
|
||||
private readonly ScreenFooter screenFooter;
|
||||
|
||||
[Cached]
|
||||
private readonly OsuLogo logo;
|
||||
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
private readonly INotificationOverlay notificationOverlay = new NotificationOverlay();
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
public TestSceneSongSelect()
|
||||
@@ -44,16 +45,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = screenScreenFooter = new ScreenFooter
|
||||
Children = new Drawable[]
|
||||
{
|
||||
OnBack = () => Stack.CurrentScreen.Exit(),
|
||||
new Toolbar
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
screenFooter = new ScreenFooter
|
||||
{
|
||||
OnBack = () => Stack.CurrentScreen.Exit(),
|
||||
},
|
||||
logo = new OsuLogo
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
},
|
||||
},
|
||||
logo = new OsuLogo
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
};
|
||||
|
||||
Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -73,29 +83,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Stack.ScreenExited += updateFooter;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("load screen", () => Stack.Push(new SoloSongSelect()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesets()
|
||||
{
|
||||
AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
||||
AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
|
||||
AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
#region Footer
|
||||
|
||||
[Test]
|
||||
public void TestMods()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
|
||||
AddStep("two mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock() });
|
||||
AddStep("three mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
|
||||
@@ -123,6 +117,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test]
|
||||
public void TestShowOptions()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("enable options", () =>
|
||||
{
|
||||
var optionsButton = this.ChildrenOfType<ScreenFooterButton>().Last();
|
||||
@@ -135,6 +131,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test]
|
||||
public void TestState()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddToggleStep("set options enabled state", state => this.ChildrenOfType<ScreenFooterButton>().Last().Enabled.Value = state);
|
||||
}
|
||||
|
||||
@@ -142,6 +140,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRandom()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("press F2", () => InputManager.Key(Key.F2));
|
||||
// AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
|
||||
// }
|
||||
@@ -149,6 +149,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRandomViaMouse()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("click button", () =>
|
||||
// {
|
||||
// InputManager.MoveMouseTo(randomButton);
|
||||
@@ -160,6 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRewind()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("press Shift+F2", () =>
|
||||
// {
|
||||
// InputManager.PressKey(Key.LShift);
|
||||
@@ -173,6 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRewindViaShiftMouseLeft()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("shift + click button", () =>
|
||||
// {
|
||||
// InputManager.PressKey(Key.LShift);
|
||||
@@ -186,6 +192,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRewindViaMouseRight()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("right click button", () =>
|
||||
// {
|
||||
// InputManager.MoveMouseTo(randomButton);
|
||||
@@ -197,6 +205,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test]
|
||||
public void TestOverlayPresent()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("Press F1", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
|
||||
@@ -208,17 +218,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
#endregion
|
||||
|
||||
private void loadSongSelect()
|
||||
{
|
||||
AddStep("load screen", () => Stack.Push(new SoloSongSelect()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
|
||||
}
|
||||
|
||||
private void updateFooter(IScreen? _, IScreen? newScreen)
|
||||
{
|
||||
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
||||
{
|
||||
screenScreenFooter.Show();
|
||||
screenScreenFooter.SetButtons(osuScreen.CreateFooterButtons());
|
||||
screenFooter.Show();
|
||||
screenFooter.SetButtons(osuScreen.CreateFooterButtons());
|
||||
}
|
||||
else
|
||||
{
|
||||
screenScreenFooter.Hide();
|
||||
screenScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
screenFooter.Hide();
|
||||
screenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
|
||||
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
|
||||
using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder;
|
||||
using BeatmapDeleteDialog = osu.Game.Screens.Select.BeatmapDeleteDialog;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneSongSelectFiltering : ScreenTestScene
|
||||
{
|
||||
private BeatmapManager manager = null!;
|
||||
private RealmRulesetStore rulesets = null!;
|
||||
|
||||
private OsuConfigManager config = null!;
|
||||
|
||||
private SoloSongSelect songSelect = null!;
|
||||
private BeatmapCarousel carousel => songSelect.ChildrenOfType<BeatmapCarousel>().Single();
|
||||
|
||||
private FilterControl filter => songSelect.ChildrenOfType<FilterControl>().Single();
|
||||
private ShearedFilterTextBox filterTextBox => songSelect.ChildrenOfType<ShearedFilterTextBox>().Single();
|
||||
private int filterOperationsCount;
|
||||
|
||||
[Cached]
|
||||
private readonly ScreenFooter screenFooter;
|
||||
|
||||
[Cached]
|
||||
private readonly OsuLogo logo;
|
||||
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
private readonly INotificationOverlay notificationOverlay = new NotificationOverlay();
|
||||
|
||||
public TestSceneSongSelectFiltering()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Toolbar
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
screenFooter = new ScreenFooter
|
||||
{
|
||||
OnBack = () => Stack.CurrentScreen.Exit(),
|
||||
},
|
||||
logo = new OsuLogo
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
RealmDetachedBeatmapStore beatmapStore;
|
||||
|
||||
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
|
||||
Add(beatmapStore);
|
||||
|
||||
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Stack.ScreenPushed += updateFooter;
|
||||
Stack.ScreenExited += updateFooter;
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("reset defaults", () =>
|
||||
{
|
||||
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
|
||||
config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All);
|
||||
|
||||
songSelect = null!;
|
||||
filterOperationsCount = 0;
|
||||
});
|
||||
|
||||
AddStep("delete all beatmaps", () => manager.Delete());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleFilterOnEnter()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoFilterOnSimpleResume()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||
waitForSuspension();
|
||||
|
||||
AddStep("return", () => songSelect.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilterOnResumeAfterChange()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||
waitForSuspension();
|
||||
|
||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
|
||||
AddStep("return", () => songSelect.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSorting()
|
||||
{
|
||||
loadSongSelect();
|
||||
addManyTestMaps();
|
||||
|
||||
// TODO: old test has this step, but there doesn't seem to be any purpose for it.
|
||||
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
|
||||
|
||||
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||
AddStep(@"Sort by Author", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
|
||||
AddStep(@"Sort by DateAdded", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
|
||||
AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
|
||||
AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
|
||||
AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
|
||||
AddStep(@"Sort by Source", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCutInFilterTextBox()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddStep("set filter text", () => filterTextBox.Current.Value = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
|
||||
|
||||
AddAssert("filter text cleared", () => filterTextBox.Current.Value, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonFilterableModChange()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
// Mod that is guaranteed to never re-filter.
|
||||
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
|
||||
// Removing the mod should still not re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilterableModChange()
|
||||
{
|
||||
importBeatmapForRuleset(3);
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
// Change to mania ruleset.
|
||||
AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
|
||||
// Apply a mod, but this should NOT re-filter because there's no search text.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
|
||||
// Set search text. Should re-filter.
|
||||
AddStep("set search text to match mods", () => filterTextBox.Current.Value = "keys=3");
|
||||
AddAssert("filter count is 2", () => filterOperationsCount, () => Is.EqualTo(2));
|
||||
|
||||
// Change filterable mod. Should re-filter.
|
||||
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
|
||||
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||
|
||||
// Add non-filterable mod. Should NOT re-filter.
|
||||
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
|
||||
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||
|
||||
// Remove filterable mod. Should re-filter.
|
||||
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
|
||||
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||
|
||||
// Remove non-filterable mod. Should NOT re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||
|
||||
// Add filterable mod. Should re-filter.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5));
|
||||
}
|
||||
|
||||
// This test should probably not be in this test class, it has nothing to do with filtering.
|
||||
// TestSceneSongSelect is a better place, but doesn't have local storage isolation setup (yet).
|
||||
[Test]
|
||||
public void TestDeleteHotkey()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
importBeatmapForRuleset(0);
|
||||
|
||||
AddAssert("beatmap imported", () => manager.GetAllUsableBeatmapSets().Any(), () => Is.True);
|
||||
|
||||
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||
// todo: remove when that's the case.
|
||||
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||
AddStep("select beatmap", () => Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||
|
||||
AddStep("press shift-delete", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Delete);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf<BeatmapDeleteDialog>);
|
||||
AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction<PopupDialogDangerousButton>());
|
||||
|
||||
AddAssert("beatmap set deleted", () => manager.GetAllUsableBeatmapSets().Any(), () => Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderVisibleAfterDeleteAll()
|
||||
{
|
||||
loadSongSelect();
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
importBeatmapForRuleset(0);
|
||||
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("delete all beatmaps", () => manager.Delete());
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderVisibleAfterStarDifficultyFilter()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
AddStep("change star filter", () => config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0));
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("star filter reset", () => config.Get<double>(OsuSetting.DisplayStarsMinimum) == 0.0);
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderVisibleWithConvertSetting()
|
||||
{
|
||||
importBeatmapForRuleset(0);
|
||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||
|
||||
loadSongSelect();
|
||||
|
||||
changeRuleset(2);
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("convert setting changed", () => config.Get<bool>(OsuSetting.ShowConvertedBeatmaps));
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectMatchCountAfterDeleteAll()
|
||||
{
|
||||
loadSongSelect();
|
||||
checkMatchedBeatmaps(0);
|
||||
|
||||
importBeatmapForRuleset(0);
|
||||
checkMatchedBeatmaps(3);
|
||||
|
||||
AddStep("delete all beatmaps", () => manager.Delete());
|
||||
checkMatchedBeatmaps(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectMatchCountAfterHardDelete()
|
||||
{
|
||||
loadSongSelect();
|
||||
checkMatchedBeatmaps(0);
|
||||
|
||||
importBeatmapForRuleset(0);
|
||||
checkMatchedBeatmaps(3);
|
||||
|
||||
AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All<BeatmapSetInfo>().Where(s => !s.Protected))));
|
||||
checkMatchedBeatmaps(0);
|
||||
}
|
||||
|
||||
private void loadSongSelect()
|
||||
{
|
||||
AddStep("load screen", () => Stack.Push(songSelect = new SoloSongSelect()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen == songSelect && songSelect.IsLoaded);
|
||||
AddStep("hook events", () =>
|
||||
{
|
||||
filterOperationsCount = 0;
|
||||
filter.CriteriaChanged += _ => filterOperationsCount++;
|
||||
});
|
||||
}
|
||||
|
||||
private NoResultsPlaceholder? getPlaceholder() => songSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
|
||||
|
||||
private void importBeatmapForRuleset(int rulesetId)
|
||||
{
|
||||
int beatmapsCount = 0;
|
||||
|
||||
AddStep($"import test map for ruleset {rulesetId}", () =>
|
||||
{
|
||||
beatmapsCount = songSelect.IsNull() ? 0 : carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count;
|
||||
manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray()));
|
||||
});
|
||||
|
||||
// This is specifically for cases where the add is happening post song select load.
|
||||
// For cases where song select is null, the assertions are provided by the load checks.
|
||||
AddUntilStep("wait for imported to arrive in carousel", () => songSelect.IsNull() || carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count > beatmapsCount);
|
||||
}
|
||||
|
||||
private void changeRuleset(int rulesetId)
|
||||
{
|
||||
AddStep($"change ruleset to {rulesetId}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == rulesetId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports test beatmap sets to show in the carousel.
|
||||
/// </summary>
|
||||
/// <param name="difficultyCountPerSet">
|
||||
/// The exact count of difficulties to create for each beatmap set.
|
||||
/// A <see langword="null"/> value causes the count of difficulties to be selected randomly.
|
||||
/// </param>
|
||||
private void addManyTestMaps(int? difficultyCountPerSet = null)
|
||||
{
|
||||
AddStep("import test maps", () =>
|
||||
{
|
||||
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets));
|
||||
});
|
||||
}
|
||||
|
||||
private void checkMatchedBeatmaps(int expected) =>
|
||||
AddUntilStep($"{expected} matching shown", () => carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
||||
|
||||
private void waitForSuspension() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen());
|
||||
|
||||
private void updateFooter(IScreen? _, IScreen? newScreen)
|
||||
{
|
||||
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
||||
{
|
||||
screenFooter.Show();
|
||||
screenFooter.SetButtons(osuScreen.CreateFooterButtons());
|
||||
}
|
||||
else
|
||||
{
|
||||
screenFooter.Hide();
|
||||
screenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,22 +12,81 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneModIcon : OsuTestScene
|
||||
{
|
||||
private FillFlowContainer spreadOutFlow = null!;
|
||||
private ModDisplay modDisplay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create flows", () =>
|
||||
{
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Relative, 0.5f),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
modDisplay = new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
spreadOutFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void addRange(IEnumerable<IMod> mods)
|
||||
{
|
||||
spreadOutFlow.AddRange(mods.Select(m => new ModIcon(m)));
|
||||
modDisplay.Current.Value = modDisplay.Current.Value.Concat(mods.OfType<Mod>()).ToList();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowAllMods()
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)),
|
||||
};
|
||||
if (m is OsuModFlashlight fl)
|
||||
fl.FollowDelay.Value = 1245;
|
||||
|
||||
if (m is OsuModDaycore dc)
|
||||
dc.SpeedChange.Value = 0.74f;
|
||||
|
||||
if (m is OsuModDifficultyAdjust da)
|
||||
da.CircleSize.Value = 8.2f;
|
||||
|
||||
if (m is ModAdaptiveSpeed ad)
|
||||
ad.AdjustPitch.Value = false;
|
||||
|
||||
return m;
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("toggle selected", () =>
|
||||
@@ -42,26 +101,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
var rateAdjustMods = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>();
|
||||
|
||||
addRange(rateAdjustMods.SelectMany(m =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>()
|
||||
.SelectMany(m =>
|
||||
{
|
||||
List<ModIcon> icons = new List<ModIcon> { new ModIcon(m) };
|
||||
List<Mod> mods = new List<Mod> { m };
|
||||
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
icons.Add(new ModIcon(m));
|
||||
}
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
mods.Add(m);
|
||||
}
|
||||
|
||||
return icons;
|
||||
}),
|
||||
};
|
||||
return mods;
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("adjust rates", () =>
|
||||
@@ -81,21 +136,50 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestChangeModType()
|
||||
{
|
||||
ModIcon icon = null!;
|
||||
|
||||
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
|
||||
AddStep("change mod", () => icon.Mod = new OsuModEasy());
|
||||
AddStep("create mod icon", () => addRange([new OsuModDoubleTime()]));
|
||||
AddStep("change mod", () =>
|
||||
{
|
||||
foreach (var modIcon in this.ChildrenOfType<ModIcon>())
|
||||
modIcon.Mod = new OsuModEasy();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInterfaceModType()
|
||||
{
|
||||
ModIcon icon = null!;
|
||||
|
||||
var ruleset = new OsuRuleset();
|
||||
|
||||
AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT")));
|
||||
AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"));
|
||||
AddStep("create mod icon", () => addRange([ruleset.AllMods.First(m => m.Acronym == "DT")]));
|
||||
AddStep("change mod", () =>
|
||||
{
|
||||
foreach (var modIcon in this.ChildrenOfType<ModIcon>())
|
||||
modIcon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyAdjust()
|
||||
{
|
||||
AddStep("create icons", () =>
|
||||
{
|
||||
addRange([
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 8 }
|
||||
},
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 5.5f }
|
||||
},
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 8 },
|
||||
ApproachRate = { Value = 8 },
|
||||
OverallDifficulty = { Value = 8 },
|
||||
DrainRate = { Value = 8 },
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@@ -183,32 +182,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(2.5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearedButton(120)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding(),
|
||||
Height = 30,
|
||||
},
|
||||
new ShearedButton(120, 40)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding { Left = -1f },
|
||||
Height = 30,
|
||||
},
|
||||
new ShearedButton(120, 70)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding { Left = 3f },
|
||||
Height = 30,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneShearedRangeSlider : ThemeComparisonTestScene
|
||||
{
|
||||
private readonly BindableNumber<double> customStart = new BindableNumber<double>
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
private readonly BindableNumber<double> customEnd = new BindableNumber<double>(10)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f
|
||||
};
|
||||
|
||||
private ShearedRangeSlider shearedRangeSlider = null!;
|
||||
|
||||
public TestSceneShearedRangeSlider()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
},
|
||||
shearedRangeSlider = new ShearedRangeSlider("Test")
|
||||
{
|
||||
Width = 600,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1),
|
||||
LowerBound = customStart,
|
||||
UpperBound = customEnd,
|
||||
TooltipSuffix = "suffix",
|
||||
NubWidth = 32,
|
||||
DefaultStringLowerBound = "0.0",
|
||||
DefaultStringUpperBound = "∞",
|
||||
MinRange = 0.1f,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset range", () =>
|
||||
{
|
||||
customStart.SetDefault();
|
||||
customEnd.SetDefault();
|
||||
});
|
||||
|
||||
AddAssert("Initial lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.1f));
|
||||
AddAssert("Initial upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(10).Within(0.1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustRange()
|
||||
{
|
||||
AddStep("Adjust range", () =>
|
||||
{
|
||||
customStart.Value = 5;
|
||||
customEnd.Value = 7.5;
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(5).Within(0.1f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.1f));
|
||||
|
||||
AddStep("Test nub pushing", () =>
|
||||
{
|
||||
customStart.Value = 9;
|
||||
});
|
||||
|
||||
AddAssert("Pushed lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(9).Within(0.1f));
|
||||
AddAssert("Pushed upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(9.1).Within(0.1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustRangeClickOutsideNub()
|
||||
{
|
||||
Vector2 lowerBoundNub = Vector2.Zero;
|
||||
Vector2 upperBoundNub = Vector2.Zero;
|
||||
|
||||
AddStep("click 75%", () =>
|
||||
{
|
||||
// save out original positions so we can use as absolute selection range.
|
||||
lowerBoundNub = shearedRangeSlider.ChildrenOfType<ShearedNub>().Last().ScreenSpaceDrawQuad.Centre - OsuGame.SHEAR * 2;
|
||||
upperBoundNub = shearedRangeSlider.ChildrenOfType<ShearedNub>().First().ScreenSpaceDrawQuad.Centre - OsuGame.SHEAR * 2;
|
||||
|
||||
InputManager.MoveMouseTo(lowerBoundNub + new Vector2((upperBoundNub.X - lowerBoundNub.X) * 0.75f, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.11f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f));
|
||||
|
||||
AddStep("click 30%", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(lowerBoundNub + new Vector2((upperBoundNub.X - lowerBoundNub.X) * 0.3f, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(3.0).Within(0.11f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f));
|
||||
|
||||
AddStep("click 0%", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(lowerBoundNub);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.11f));
|
||||
AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@@ -30,16 +32,32 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
(typeof(OverlayColourProvider), colourProvider)
|
||||
},
|
||||
Children = new Drawable[]
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
new ShearedSearchTextBox
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f
|
||||
new ShearedSearchTextBox
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f
|
||||
},
|
||||
new ShearedFilterTextBox
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
StatusText = "12345 matches",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,38 +3,50 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneShearedSliderBar : OsuManualInputManagerTestScene
|
||||
public partial class TestSceneShearedSliderBar : ThemeComparisonTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
private TestSliderBar slider = null!;
|
||||
|
||||
private ShearedSliderBar<double> slider = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
protected override Drawable CreateContent() => slider = new TestSliderBar
|
||||
{
|
||||
AddStep("create slider", () => Child = slider = new ShearedSliderBar<double>
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = new BindableDouble(5)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = new BindableDouble(5)
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestNubDisplay()
|
||||
{
|
||||
AddSliderStep("nub width", 20, 80, 50, v =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
{
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
slider.Nub.Width = v;
|
||||
slider.RangePadding = v / 2f;
|
||||
}
|
||||
});
|
||||
AddToggleStep("nub shadow", v =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
slider.NubShadowColour = v ? Color4.Black.Opacity(0.2f) : Color4.Black.Opacity(0f);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,6 +81,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
|
||||
AddStep("enable slider", () => slider.Current.Disabled = false);
|
||||
}
|
||||
|
||||
public partial class TestSliderBar : ShearedSliderBar<double>
|
||||
{
|
||||
public new ShearedNub Nub => base.Nub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,58 +72,66 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
first.PerformWrite(updated =>
|
||||
{
|
||||
var realm = updated.Realm;
|
||||
|
||||
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
||||
|
||||
// Re-fetch as we are likely on a different thread.
|
||||
original = realm!.Find<BeatmapSetInfo>(originalId)!;
|
||||
|
||||
// Generally the import process will do this for us if the OnlineIDs match,
|
||||
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
||||
original.DeletePending = true;
|
||||
|
||||
// Transfer local values which should be persisted across a beatmap update.
|
||||
updated.DateAdded = originalDateAdded;
|
||||
|
||||
transferCollectionReferences(realm, original, updated);
|
||||
|
||||
foreach (var beatmap in original.Beatmaps.ToArray())
|
||||
try
|
||||
{
|
||||
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
|
||||
var realm = updated.Realm;
|
||||
|
||||
if (updatedBeatmap != null)
|
||||
// Re-fetch as we are likely on a different thread.
|
||||
original = realm!.Find<BeatmapSetInfo>(originalId)!;
|
||||
|
||||
// Generally the import process will do this for us if the OnlineIDs match,
|
||||
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
||||
original.DeletePending = true;
|
||||
|
||||
// Transfer local values which should be persisted across a beatmap update.
|
||||
updated.DateAdded = originalDateAdded;
|
||||
|
||||
transferCollectionReferences(realm, original, updated);
|
||||
|
||||
foreach (var beatmap in original.Beatmaps.ToArray())
|
||||
{
|
||||
// If the updated beatmap matches an existing one, transfer any user data across..
|
||||
if (beatmap.Scores.Any())
|
||||
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
|
||||
|
||||
if (updatedBeatmap != null)
|
||||
{
|
||||
Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database);
|
||||
// If the updated beatmap matches an existing one, transfer any user data across..
|
||||
if (beatmap.Scores.Any())
|
||||
{
|
||||
Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database);
|
||||
|
||||
foreach (var score in beatmap.Scores)
|
||||
score.BeatmapInfo = updatedBeatmap;
|
||||
foreach (var score in beatmap.Scores)
|
||||
score.BeatmapInfo = updatedBeatmap;
|
||||
}
|
||||
|
||||
// ..then nuke the old beatmap completely.
|
||||
// this is done instead of a soft deletion to avoid a user potentially creating weird
|
||||
// interactions, like restoring the outdated beatmap then updating a second time
|
||||
// (causing user data to be wiped).
|
||||
original.Beatmaps.Remove(beatmap);
|
||||
|
||||
realm.Remove(beatmap.Metadata);
|
||||
realm.Remove(beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the beatmap differs in the original, leave it in a soft-deleted state but reset online info.
|
||||
// This caters to the case where a user has made modifications they potentially want to restore,
|
||||
// but after restoring we want to ensure it can't be used to trigger an update of the beatmap.
|
||||
beatmap.ResetOnlineInfo();
|
||||
}
|
||||
|
||||
// ..then nuke the old beatmap completely.
|
||||
// this is done instead of a soft deletion to avoid a user potentially creating weird
|
||||
// interactions, like restoring the outdated beatmap then updating a second time
|
||||
// (causing user data to be wiped).
|
||||
original.Beatmaps.Remove(beatmap);
|
||||
|
||||
realm.Remove(beatmap.Metadata);
|
||||
realm.Remove(beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the beatmap differs in the original, leave it in a soft-deleted state but reset online info.
|
||||
// This caters to the case where a user has made modifications they potentially want to restore,
|
||||
// but after restoring we want to ensure it can't be used to trigger an update of the beatmap.
|
||||
beatmap.ResetOnlineInfo();
|
||||
}
|
||||
|
||||
// If the original has no beatmaps left, delete the set as well.
|
||||
if (!original.Beatmaps.Any())
|
||||
realm.Remove(original);
|
||||
|
||||
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, $"Failed to update beatmap \"{updated}\"", LoggingTarget.Database);
|
||||
throw;
|
||||
}
|
||||
|
||||
// If the original has no beatmaps left, delete the set as well.
|
||||
if (!original.Beatmaps.Any())
|
||||
realm.Remove(original);
|
||||
});
|
||||
|
||||
return first;
|
||||
|
||||
@@ -179,7 +179,10 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f, 0.01f);
|
||||
SetDefault(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f, 0.01f);
|
||||
|
||||
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
|
||||
if (RuntimeInfo.IsMobile)
|
||||
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.1f, 0.01f);
|
||||
else
|
||||
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
|
||||
|
||||
SetDefault(OsuSetting.UIHoldActivationDelay, 200.0, 0.0, 500.0, 50.0);
|
||||
|
||||
@@ -222,6 +225,8 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true);
|
||||
SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true);
|
||||
|
||||
SetDefault(OsuSetting.WasSupporter, false);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@@ -463,5 +468,11 @@ namespace osu.Game.Configuration
|
||||
EditorShowStoryboard,
|
||||
EditorSubmissionNotifyOnDiscussionReplies,
|
||||
EditorSubmissionLoadInBrowserAfterSubmission,
|
||||
|
||||
/// <summary>
|
||||
/// Cached state of whether local user is a supporter.
|
||||
/// Used to allow early checks (ie for startup samples) to be in the correct state, even if the API authentication process has not completed.
|
||||
/// </summary>
|
||||
WasSupporter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Extensions
|
||||
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
|
||||
/// <param name="asPercentage">Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%.</param>
|
||||
/// <returns>The formatted output.</returns>
|
||||
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage = false) where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
double floatValue = double.CreateTruncating(value);
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
#region Properties and methods for external usage
|
||||
|
||||
/// <summary>
|
||||
/// Called after a filter operation or change in items results in the visible carousel items changing.
|
||||
/// </summary>
|
||||
public Action? NewItemsPresented { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
|
||||
/// </summary>
|
||||
@@ -68,7 +73,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
public int ItemsTracked => Items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The number of carousel items currently in rotation for display.
|
||||
/// The items currently in rotation for display.
|
||||
/// </summary>
|
||||
public int DisplayableItems => carouselItems?.Count ?? 0;
|
||||
|
||||
@@ -265,7 +270,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
|
||||
// Copy must be performed on update thread for now (see ConfigureAwait above).
|
||||
// Could potentially be optimised in the future if it becomes an issue.
|
||||
IEnumerable<CarouselItem> items = new List<CarouselItem>(Items.Select(m => new CarouselItem(m)));
|
||||
List<CarouselItem> items = new List<CarouselItem>(Items.Select(m => new CarouselItem(m)));
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
@@ -275,6 +280,11 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
log($"Performing {filter.GetType().ReadableName()}");
|
||||
items = await filter.Run(items, cts.Token).ConfigureAwait(false);
|
||||
|
||||
// To avoid shooting ourselves in the foot, ensure that we manifest a list after each filter.
|
||||
//
|
||||
// A future improvement may be passing a reference list through each filter rather than copying each time,
|
||||
// but this is the safest approach.
|
||||
}
|
||||
|
||||
log("Updating Y positions");
|
||||
@@ -292,13 +302,15 @@ namespace osu.Game.Graphics.Carousel
|
||||
Schedule(() =>
|
||||
{
|
||||
log("Items ready for display");
|
||||
carouselItems = items.ToList();
|
||||
carouselItems = items;
|
||||
displayedRange = null;
|
||||
|
||||
// Need to call this to ensure correct post-selection logic is handled on the new items list.
|
||||
HandleItemSelected(currentSelection.Model);
|
||||
|
||||
refreshAfterSelection();
|
||||
|
||||
NewItemsPresented?.Invoke();
|
||||
});
|
||||
|
||||
void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}");
|
||||
|
||||
@@ -18,6 +18,6 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="items">The items to be filtered.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>The post-filtered items.</returns>
|
||||
Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken);
|
||||
Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Layout;
|
||||
using osuTK;
|
||||
|
||||
@@ -18,6 +19,10 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry);
|
||||
|
||||
// Sheared components regularly end up off the side of the screen due to padding considerations.
|
||||
// If we use this class in places where performance is important, we should reconsider the handling of this.
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
public ShearAligningWrapper(Drawable drawable)
|
||||
{
|
||||
RelativeSizeAxes = drawable.RelativeSizeAxes;
|
||||
|
||||
@@ -88,12 +88,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT)
|
||||
{
|
||||
Height = height;
|
||||
Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height };
|
||||
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Shear = OsuGame.SHEAR;
|
||||
Content.Masking = true;
|
||||
Shear = OsuGame.SHEAR;
|
||||
|
||||
Content.Anchor = Content.Origin = Anchor.Centre;
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Masking = true;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ShearedFilterTextBox : ShearedSearchTextBox
|
||||
{
|
||||
private const float filter_text_size = 12;
|
||||
|
||||
public LocalisableString StatusText
|
||||
{
|
||||
get => ((InnerFilterTextBox)TextBox).StatusText.Text;
|
||||
set => Schedule(() => ((InnerFilterTextBox)TextBox).StatusText.Text = value);
|
||||
}
|
||||
|
||||
public ShearedFilterTextBox()
|
||||
{
|
||||
Height += filter_text_size;
|
||||
}
|
||||
|
||||
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerFilterTextBox();
|
||||
|
||||
protected partial class InnerFilterTextBox : InnerSearchTextBox
|
||||
{
|
||||
public OsuSpriteText StatusText { get; private set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
TextContainer.Add(StatusText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Torus.With(size: filter_text_size, weight: FontWeight.SemiBold),
|
||||
Margin = new MarginPadding { Top = 2, Left = -1 },
|
||||
Colour = colours.Yellow
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
TextContainer.Height *= (DrawHeight - filter_text_size) / DrawHeight;
|
||||
TextContainer.Margin = new MarginPadding { Bottom = filter_text_size };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,37 +21,54 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public Action? OnDoubleClicked { get; init; }
|
||||
|
||||
protected const float BORDER_WIDTH = 3;
|
||||
|
||||
public const int HEIGHT = 30;
|
||||
public const float EXPANDED_SIZE = 50;
|
||||
public const float CORNER_RADIUS = 5;
|
||||
|
||||
private readonly Box fill;
|
||||
private readonly Container main;
|
||||
private readonly Container shadow;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the shape for the nub, allowing for any type of container to be used.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ShearedNub()
|
||||
{
|
||||
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
|
||||
InternalChild = main = new Container
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = BORDER_WIDTH,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = fill = new Box
|
||||
shadow = new Container
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 20f,
|
||||
},
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
},
|
||||
main = new Container
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = 8f,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = fill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +93,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(onCurrentValueChanged, true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private bool glowing;
|
||||
@@ -89,22 +107,22 @@ namespace osu.Game.Graphics.UserInterface
|
||||
return;
|
||||
|
||||
glowing = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
|
||||
private Color4 shadowColour = Color4.Black.Opacity(0f);
|
||||
|
||||
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
|
||||
main.FadeColour(AccentColour, 800, Easing.OutQuint);
|
||||
}
|
||||
public Color4 ShadowColour
|
||||
{
|
||||
get => shadowColour;
|
||||
set
|
||||
{
|
||||
if (shadowColour == value)
|
||||
return;
|
||||
|
||||
shadowColour = value;
|
||||
shadow.FadeEdgeEffectTo(value, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +148,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
accentColour = value;
|
||||
if (!Glowing)
|
||||
main.Colour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +160,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
glowingAccentColour = value;
|
||||
if (Glowing)
|
||||
main.Colour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +172,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
glowColour = value;
|
||||
|
||||
var effect = main.EdgeEffect;
|
||||
effect.Colour = Glowing ? value : value.Opacity(0);
|
||||
main.EdgeEffect = effect;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +190,26 @@ namespace osu.Game.Graphics.UserInterface
|
||||
else
|
||||
{
|
||||
main.ResizeWidthTo(0.75f, duration, Easing.OutQuint);
|
||||
main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint);
|
||||
main.TransformTo(nameof(BorderThickness), 8f, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (Glowing)
|
||||
{
|
||||
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
|
||||
|
||||
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
|
||||
main.FadeColour(AccentColour, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ShearedRangeSlider : CompositeDrawable
|
||||
{
|
||||
private readonly LocalisableString label;
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> lowerBound = new BindableNumberWithCurrent<double>();
|
||||
|
||||
/// <summary>
|
||||
/// The lower limiting value.
|
||||
/// </summary>
|
||||
public Bindable<double> LowerBound
|
||||
{
|
||||
get => lowerBound.Current;
|
||||
set => lowerBound.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> upperBound = new BindableNumberWithCurrent<double>();
|
||||
|
||||
/// <summary>
|
||||
/// The upper limiting value.
|
||||
/// </summary>
|
||||
public Bindable<double> UpperBound
|
||||
{
|
||||
get => upperBound.Current;
|
||||
set => upperBound.Current = value;
|
||||
}
|
||||
|
||||
public float NubWidth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum difference between the lower bound and higher bound
|
||||
/// </summary>
|
||||
public float MinRange
|
||||
{
|
||||
set => minRange = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lower bound display for when it is set to its default value.
|
||||
/// </summary>
|
||||
public string DefaultStringLowerBound { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Upper bound display for when it is set to its default value.
|
||||
/// </summary>
|
||||
public string DefaultStringUpperBound { get; init; } = string.Empty;
|
||||
|
||||
public LocalisableString DefaultTooltipLowerBound { get; init; } = string.Empty;
|
||||
|
||||
public LocalisableString DefaultTooltipUpperBound { get; init; } = string.Empty;
|
||||
|
||||
public string TooltipSuffix { get; init; } = string.Empty;
|
||||
|
||||
private float minRange = 0.1f;
|
||||
|
||||
protected Container SliderContainer { get; private set; } = null!;
|
||||
|
||||
protected BoundSliderBar LowerBoundSlider { get; private set; } = null!;
|
||||
protected BoundSliderBar UpperBoundSlider { get; private set; } = null!;
|
||||
|
||||
protected Vector2 ScreenSpaceHalfwayPoint
|
||||
{
|
||||
get
|
||||
{
|
||||
var lowerSS = LowerBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft;
|
||||
var upperSS = UpperBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft;
|
||||
|
||||
return lowerSS + (upperSS - lowerSS) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public ShearedRangeSlider(LocalisableString label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Height = ShearedNub.HEIGHT;
|
||||
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Masking = true,
|
||||
CornerRadius = 5f,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = label,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Margin = new MarginPadding { Horizontal = 12, Vertical = 5 },
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
},
|
||||
},
|
||||
SliderContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = -10 },
|
||||
Children = new[]
|
||||
{
|
||||
UpperBoundSlider = CreateBoundSlider(true).With(d =>
|
||||
{
|
||||
d.KeyboardStep = 0.1f;
|
||||
d.RelativeSizeAxes = Axes.X;
|
||||
d.TooltipSuffix = TooltipSuffix;
|
||||
d.DefaultString = DefaultStringUpperBound;
|
||||
d.DefaultTooltip = DefaultTooltipUpperBound;
|
||||
d.NubWidth = NubWidth;
|
||||
d.Current = upperBound;
|
||||
}),
|
||||
LowerBoundSlider = CreateBoundSlider(false).With(d =>
|
||||
{
|
||||
d.KeyboardStep = 0.1f;
|
||||
d.RelativeSizeAxes = Axes.X;
|
||||
d.TooltipSuffix = TooltipSuffix;
|
||||
d.DefaultString = DefaultStringLowerBound;
|
||||
d.DefaultTooltip = DefaultTooltipLowerBound;
|
||||
d.NubWidth = NubWidth;
|
||||
d.Current = lowerBound;
|
||||
}),
|
||||
UpperBoundSlider.Nub.CreateProxy(),
|
||||
LowerBoundSlider.Nub.CreateProxy(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LowerBoundSlider.Current.ValueChanged += min => UpperBoundSlider.Current.Value = Math.Max(min.NewValue + minRange, UpperBoundSlider.Current.Value);
|
||||
UpperBoundSlider.Current.ValueChanged += max => LowerBoundSlider.Current.Value = Math.Min(max.NewValue - minRange, LowerBoundSlider.Current.Value);
|
||||
}
|
||||
|
||||
protected virtual BoundSliderBar CreateBoundSlider(bool isUpper) => new BoundSliderBar(this, isUpper);
|
||||
|
||||
protected partial class BoundSliderBar : ShearedSliderBar<double>
|
||||
{
|
||||
private readonly ShearedRangeSlider rangeSlider;
|
||||
private readonly bool isUpper;
|
||||
|
||||
public new float NormalizedValue => base.NormalizedValue;
|
||||
|
||||
public new ShearedNub Nub => base.Nub;
|
||||
|
||||
public string? DefaultString;
|
||||
public LocalisableString? DefaultTooltip;
|
||||
public string? TooltipSuffix;
|
||||
|
||||
public float NubWidth { get; set; } = ShearedNub.HEIGHT;
|
||||
|
||||
public override LocalisableString TooltipText =>
|
||||
(Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}");
|
||||
|
||||
protected OsuSpriteText NubText { get; private set; } = null!;
|
||||
|
||||
public override bool AcceptsFocus => false;
|
||||
|
||||
public BoundSliderBar(ShearedRangeSlider rangeSlider, bool isUpper)
|
||||
{
|
||||
this.rangeSlider = rangeSlider;
|
||||
this.isUpper = isUpper;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Nub.Width = NubWidth;
|
||||
RangePadding = Nub.Width / 2;
|
||||
|
||||
Nub.Add(NubText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
X = -3,
|
||||
UseFullGlyphHeight = false,
|
||||
Colour = OsuColour.ForegroundTextColourFor(colourProvider.Light1),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
});
|
||||
|
||||
AccentColour = colourProvider.Highlight1.Darken(0.1f);
|
||||
Nub.AccentColour = colourProvider.Highlight1;
|
||||
Nub.GlowingAccentColour = colourProvider.Highlight1;
|
||||
Nub.GlowColour = colourProvider.Highlight1;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (!isUpper)
|
||||
{
|
||||
AccentColour = BackgroundColour;
|
||||
BackgroundColour = Color4.Transparent;
|
||||
}
|
||||
|
||||
Current.BindValueChanged(current => UpdateDisplay(current.NewValue), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected virtual void UpdateDisplay(double value)
|
||||
{
|
||||
string defaultString = DefaultString ?? value.ToString("N1");
|
||||
NubText.Text = Current.IsDefault ? defaultString : value.ToString("N1");
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (isUpper)
|
||||
return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X > rangeSlider.ScreenSpaceHalfwayPoint.X;
|
||||
|
||||
return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (isUpper)
|
||||
{
|
||||
// Only draw left box where required to avoid masking bleed issues.
|
||||
LeftBox.X = ToParentSpace(ToLocalSpace(rangeSlider.LowerBoundSlider.Nub.ScreenSpaceDrawQuad.Centre)).X;
|
||||
LeftBox.Size -= new Vector2(LeftBox.X, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
base.OnHover(e);
|
||||
return true; // Make sure only one nub shows hover effect at once.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,33 +21,33 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private const float corner_radius = 7;
|
||||
|
||||
private readonly Box background;
|
||||
private readonly SearchTextBox textBox;
|
||||
protected readonly InnerSearchTextBox TextBox;
|
||||
|
||||
public Bindable<string> Current
|
||||
{
|
||||
get => textBox.Current;
|
||||
set => textBox.Current = value;
|
||||
get => TextBox.Current;
|
||||
set => TextBox.Current = value;
|
||||
}
|
||||
|
||||
public bool HoldFocus
|
||||
{
|
||||
get => textBox.HoldFocus;
|
||||
set => textBox.HoldFocus = value;
|
||||
get => TextBox.HoldFocus;
|
||||
set => TextBox.HoldFocus = value;
|
||||
}
|
||||
|
||||
public LocalisableString PlaceholderText
|
||||
{
|
||||
get => textBox.PlaceholderText;
|
||||
set => textBox.PlaceholderText = value;
|
||||
get => TextBox.PlaceholderText;
|
||||
set => TextBox.PlaceholderText = value;
|
||||
}
|
||||
|
||||
public new bool HasFocus => textBox.HasFocus;
|
||||
public new bool HasFocus => TextBox.HasFocus;
|
||||
|
||||
public void TakeFocus() => textBox.TakeFocus();
|
||||
public void TakeFocus() => TextBox.TakeFocus();
|
||||
|
||||
public void KillFocus() => textBox.KillFocus();
|
||||
public void KillFocus() => TextBox.KillFocus();
|
||||
|
||||
public bool SelectAll() => textBox.SelectAll();
|
||||
public bool SelectAll() => TextBox.SelectAll();
|
||||
|
||||
public ShearedSearchTextBox()
|
||||
{
|
||||
@@ -69,13 +69,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
textBox = new InnerSearchTextBox
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One
|
||||
},
|
||||
TextBox = CreateInnerTextBox(),
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.Search,
|
||||
@@ -101,10 +95,20 @@ namespace osu.Game.Graphics.UserInterface
|
||||
background.Colour = colourProvider.Background3;
|
||||
}
|
||||
|
||||
public override bool HandleNonPositionalInput => textBox.HandleNonPositionalInput;
|
||||
public override bool HandleNonPositionalInput => TextBox.HandleNonPositionalInput;
|
||||
|
||||
private partial class InnerSearchTextBox : SearchTextBox
|
||||
protected virtual InnerSearchTextBox CreateInnerTextBox() => new InnerSearchTextBox();
|
||||
|
||||
protected partial class InnerSearchTextBox : SearchTextBox
|
||||
{
|
||||
public InnerSearchTextBox()
|
||||
{
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = Vector2.One;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Overlays;
|
||||
using static osu.Game.Graphics.UserInterface.ShearedNub;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@@ -29,6 +28,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private readonly Container mainContent;
|
||||
|
||||
protected virtual bool FocusIndicator => true;
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
@@ -56,43 +57,41 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public Color4 NubShadowColour
|
||||
{
|
||||
get => Nub.ShadowColour;
|
||||
set => Nub.ShadowColour = value;
|
||||
}
|
||||
|
||||
public ShearedSliderBar()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Height = HEIGHT;
|
||||
RangePadding = EXPANDED_SIZE / 2;
|
||||
Height = ShearedNub.HEIGHT;
|
||||
RangePadding = ShearedNub.EXPANDED_SIZE / 2;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Child = new Container
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Children = new Drawable[]
|
||||
LeftBox = new Box
|
||||
{
|
||||
LeftBox = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
RightBox = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
RightBox = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(0, 0.5f),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,7 +101,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Nub = new ShearedNub
|
||||
{
|
||||
X = -OsuGame.SHEAR.X * HEIGHT / 2f,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Current = { Value = true },
|
||||
@@ -146,13 +144,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
mainContent.EdgeEffect = new EdgeEffectParameters
|
||||
if (FocusIndicator)
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = AccentColour.Darken(1),
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
};
|
||||
mainContent.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = AccentColour.Darken(1),
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
@@ -191,8 +192,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
|
||||
LeftBox.Size = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
RightBox.Size = new Vector2(Math.Clamp(DrawWidth - RangePadding - Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
}
|
||||
|
||||
protected override void UpdateValue(float value)
|
||||
|
||||
@@ -135,6 +135,11 @@ Click to see what's new!", version);
|
||||
/// </summary>
|
||||
public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update...");
|
||||
|
||||
/// <summary>
|
||||
/// "This multiplayer room has ended. Click to display room results."
|
||||
/// </summary>
|
||||
public static LocalisableString MultiplayerRoomEnded => new TranslatableString(getKey(@"multiplayer_room_ended"), @"This multiplayer room has ended. Click to display room results.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RightMouseScroll => new TranslatableString(getKey(@"right_mouse_scroll"), @"Right mouse drag to absolute scroll");
|
||||
|
||||
/// <summary>
|
||||
/// "Show converts"
|
||||
/// </summary>
|
||||
public static LocalisableString ShowConverts => new TranslatableString(getKey(@"show_converts"), @"Show converts");
|
||||
|
||||
/// <summary>
|
||||
/// "Show converted beatmaps"
|
||||
/// </summary>
|
||||
|
||||
@@ -72,6 +72,8 @@ namespace osu.Game.Online.API
|
||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||
|
||||
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
|
||||
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
|
||||
|
||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
private readonly Logger log;
|
||||
|
||||
@@ -104,6 +106,7 @@ namespace osu.Game.Online.API
|
||||
authentication.Token.ValueChanged += onTokenChanged;
|
||||
|
||||
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
||||
config.BindWith(OsuSetting.WasSupporter, configSupporter);
|
||||
|
||||
if (HasLogin)
|
||||
{
|
||||
@@ -333,6 +336,7 @@ namespace osu.Game.Online.API
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
localUser.Value = me;
|
||||
configSupporter.Value = me.IsSupporter;
|
||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||
failureCount = 0;
|
||||
};
|
||||
@@ -368,7 +372,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
localUser.Value = new APIUser
|
||||
{
|
||||
Username = ProvidedUsername
|
||||
Username = ProvidedUsername,
|
||||
IsSupporter = configSupporter.Value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -607,6 +612,7 @@ namespace osu.Game.Online.API
|
||||
Schedule(() =>
|
||||
{
|
||||
localUser.Value = createGuestUser();
|
||||
configSupporter.Value = false;
|
||||
friends.Clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -129,8 +129,8 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
switch (args[0])
|
||||
{
|
||||
case "http":
|
||||
case "https":
|
||||
case @"http":
|
||||
case @"https":
|
||||
// length > 3 since all these links need another argument to work
|
||||
if (args.Length > 3 && args[1].EndsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -139,8 +139,8 @@ namespace osu.Game.Online.Chat
|
||||
switch (args[2])
|
||||
{
|
||||
// old site only
|
||||
case "b":
|
||||
case "beatmaps":
|
||||
case @"b":
|
||||
case @"beatmaps":
|
||||
{
|
||||
string trimmed = mainArg.Split('?').First();
|
||||
if (int.TryParse(trimmed, out int id))
|
||||
@@ -149,11 +149,11 @@ namespace osu.Game.Online.Chat
|
||||
break;
|
||||
}
|
||||
|
||||
case "s":
|
||||
case "beatmapsets":
|
||||
case "d":
|
||||
case @"s":
|
||||
case @"beatmapsets":
|
||||
case @"d":
|
||||
{
|
||||
if (mainArg == "discussions")
|
||||
if (mainArg == @"discussions")
|
||||
// handle discussion links externally for now
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
@@ -169,15 +169,15 @@ namespace osu.Game.Online.Chat
|
||||
break;
|
||||
}
|
||||
|
||||
case "u":
|
||||
case "users":
|
||||
case @"u":
|
||||
case @"users":
|
||||
return getUserLink(mainArg);
|
||||
|
||||
case "wiki":
|
||||
case @"wiki":
|
||||
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3)));
|
||||
|
||||
case "home":
|
||||
if (mainArg != "changelog")
|
||||
case @"home":
|
||||
if (mainArg != @"changelog")
|
||||
// handle link other than changelog as external for now
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
@@ -192,13 +192,26 @@ namespace osu.Game.Online.Chat
|
||||
return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case @"multiplayer":
|
||||
if (mainArg != @"rooms")
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
if (args.Length == 5)
|
||||
{
|
||||
// https://osu.ppy.sh/multiplayer/rooms/{id}
|
||||
// route used for both multiplayer and playlists
|
||||
return new LinkDetails(LinkAction.JoinRoom, args[4]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "osu":
|
||||
case @"osu":
|
||||
// every internal link also needs some kind of argument
|
||||
if (args.Length < 3)
|
||||
break;
|
||||
@@ -207,38 +220,39 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
switch (args[1])
|
||||
{
|
||||
case "chan":
|
||||
case @"chan":
|
||||
linkType = LinkAction.OpenChannel;
|
||||
break;
|
||||
|
||||
case "edit":
|
||||
case @"edit":
|
||||
linkType = LinkAction.OpenEditorTimestamp;
|
||||
break;
|
||||
|
||||
case "b":
|
||||
case @"b":
|
||||
linkType = LinkAction.OpenBeatmap;
|
||||
break;
|
||||
|
||||
case "s":
|
||||
case "dl":
|
||||
case @"s":
|
||||
case @"dl":
|
||||
linkType = LinkAction.OpenBeatmapSet;
|
||||
break;
|
||||
|
||||
case "spectate":
|
||||
case @"spectate":
|
||||
linkType = LinkAction.Spectate;
|
||||
break;
|
||||
|
||||
case "u":
|
||||
case @"u":
|
||||
return getUserLink(args[2]);
|
||||
|
||||
case @"room":
|
||||
linkType = LinkAction.JoinRoom;
|
||||
break;
|
||||
|
||||
default:
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
}
|
||||
|
||||
return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2]));
|
||||
|
||||
case "osump":
|
||||
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
|
||||
}
|
||||
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
@@ -337,7 +351,7 @@ namespace osu.Game.Online.Chat
|
||||
OpenBeatmapSet,
|
||||
OpenChannel,
|
||||
OpenEditorTimestamp,
|
||||
JoinMultiplayerMatch,
|
||||
JoinRoom,
|
||||
Spectate,
|
||||
OpenUserProfile,
|
||||
SearchBeatmapSet,
|
||||
|
||||
+62
-1
@@ -67,6 +67,7 @@ using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
@@ -79,6 +80,7 @@ using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Sentry;
|
||||
using MatchType = osu.Game.Online.Rooms.MatchType;
|
||||
|
||||
namespace osu.Game
|
||||
{
|
||||
@@ -491,7 +493,6 @@ namespace osu.Game
|
||||
HandleTimestamp(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.JoinMultiplayerMatch:
|
||||
case LinkAction.Spectate:
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
|
||||
{
|
||||
@@ -523,6 +524,11 @@ namespace osu.Game
|
||||
|
||||
break;
|
||||
|
||||
case LinkAction.JoinRoom:
|
||||
if (long.TryParse(argString, out long roomId))
|
||||
JoinRoom(roomId);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
|
||||
}
|
||||
@@ -598,6 +604,28 @@ namespace osu.Game
|
||||
/// <param name="version">The build version of the update stream</param>
|
||||
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
|
||||
|
||||
/// <summary>
|
||||
/// Joins a multiplayer or playlists room with the given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
public void JoinRoom(long id)
|
||||
{
|
||||
var request = new GetRoomRequest(id);
|
||||
request.Success += room =>
|
||||
{
|
||||
switch (room.Type)
|
||||
{
|
||||
case MatchType.Playlists:
|
||||
PresentPlaylist(room);
|
||||
break;
|
||||
|
||||
default:
|
||||
PresentMultiplayerMatch(room, string.Empty);
|
||||
break;
|
||||
}
|
||||
};
|
||||
API.Queue(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
|
||||
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
|
||||
@@ -725,6 +753,22 @@ namespace osu.Game
|
||||
/// <param name="password">The password to join the room, if any is given.</param>
|
||||
public void PresentMultiplayerMatch(Room room, string password)
|
||||
{
|
||||
if (room.HasEnded)
|
||||
{
|
||||
// TODO: Eventually it should be possible to display ended multiplayer rooms in game too,
|
||||
// but it generally will require turning off the entirety of communication with spectator server which is currently embedded into multiplayer screens.
|
||||
Notifications.Post(new SimpleNotification
|
||||
{
|
||||
Text = NotificationsStrings.MultiplayerRoomEnded,
|
||||
Activated = () =>
|
||||
{
|
||||
OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
PerformFromScreen(screen =>
|
||||
{
|
||||
if (!(screen is Multiplayer multiplayer))
|
||||
@@ -736,6 +780,23 @@ namespace osu.Game
|
||||
// but `PerformFromScreen` doesn't understand nested stacks.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Join a playlist immediately.
|
||||
/// </summary>
|
||||
/// <param name="room">The playlist to join.</param>
|
||||
public void PresentPlaylist(Room room)
|
||||
{
|
||||
PerformFromScreen(screen =>
|
||||
{
|
||||
if (!(screen is Playlists playlists))
|
||||
screen.Push(playlists = new Playlists());
|
||||
|
||||
playlists.Join(room);
|
||||
});
|
||||
// TODO: We should really be able to use `validScreens: new[] { typeof(Playlists) }` here
|
||||
// but `PerformFromScreen` doesn't understand nested stacks.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Present a score's replay immediately.
|
||||
/// The user should have already requested this interactively.
|
||||
|
||||
@@ -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.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
@@ -9,10 +8,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
public enum MetadataType
|
||||
{
|
||||
[Description("User Tags")] // TODO: use translated string after osu-resources update
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoUserTags))]
|
||||
UserTags,
|
||||
|
||||
[Description("Mapper Tags")] // TODO: use translated string after osu-resources update
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoMapperTags))]
|
||||
MapperTags,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Height = ModSelectPanel.HEIGHT;
|
||||
|
||||
// shear will be applied at a higher level in `ModPresetColumn`.
|
||||
Content.Shear = Vector2.Zero;
|
||||
Shear = Vector2.Zero;
|
||||
Padding = new MarginPadding();
|
||||
|
||||
Text = "+";
|
||||
|
||||
@@ -40,11 +40,13 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public AddPresetPopover(AddPresetButton addPresetButton)
|
||||
{
|
||||
const float content_width = 300;
|
||||
|
||||
button = addPresetButton;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
Width = content_width,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
@@ -63,12 +65,24 @@ namespace osu.Game.Overlays.Mods
|
||||
Label = CommonStrings.Description,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
createButton = new ShearedButton
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = createPreset
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
createButton = new ShearedButton(content_width)
|
||||
{
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = createPreset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "One or more values are being adjusted by mods that change speed.",
|
||||
Text = "One or more values are being adjusted by mods.",
|
||||
},
|
||||
attributesFillFlow = new FillFlowContainer
|
||||
{
|
||||
|
||||
@@ -52,9 +52,11 @@ namespace osu.Game.Overlays.Mods
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float content_width = 300;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
Width = content_width,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
@@ -107,25 +109,27 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
useCurrentModsButton = new ShearedButton
|
||||
useCurrentModsButton = new ShearedButton(content_width)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = ModSelectOverlayStrings.UseCurrentMods,
|
||||
DarkerColour = colours.Blue1,
|
||||
LighterColour = colours.Blue0,
|
||||
TextColour = colourProvider.Background6,
|
||||
Action = useCurrentMods,
|
||||
},
|
||||
saveButton = new ShearedButton
|
||||
saveButton = new ShearedButton(content_width)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = Resources.Localisation.Web.CommonStrings.ButtonsSave,
|
||||
DarkerColour = colours.Orange1,
|
||||
LighterColour = colours.Orange0,
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
CurrentPage = CurrentPage?.TakeNext(ItemsPerPage) ?? new PaginationParameters(InitialItemsCount);
|
||||
|
||||
retrievalRequest = CreateRequest(User.Value, CurrentPage.Value);
|
||||
retrievalRequest = CreateRequest(User.Value, new PaginationParameters(CurrentPage.Value.Offset, InitialItemsCount + 1));
|
||||
retrievalRequest.Success += items => UpdateItems(items, loadCancellation);
|
||||
|
||||
api.Queue(retrievalRequest);
|
||||
@@ -124,8 +124,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
protected virtual void UpdateItems(List<TModel> items, CancellationTokenSource cancellationTokenSource) => Schedule(() =>
|
||||
{
|
||||
OnItemsReceived(items);
|
||||
|
||||
if (!items.Any() && CurrentPage?.Offset == 0)
|
||||
{
|
||||
moreButton.Hide();
|
||||
@@ -137,11 +135,18 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasMore = items.Count > CurrentPage?.Limit;
|
||||
|
||||
if (hasMore)
|
||||
items.RemoveAt(items.Count - 1);
|
||||
|
||||
OnItemsReceived(items);
|
||||
|
||||
LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null).Cast<Drawable>(), drawables =>
|
||||
{
|
||||
missing.Hide();
|
||||
|
||||
moreButton.FadeTo(items.Count == CurrentPage?.Limit ? 1 : 0);
|
||||
moreButton.FadeTo(hasMore ? 1 : 0);
|
||||
moreButton.IsLoading = false;
|
||||
|
||||
ItemsContainer.AddRange(drawables);
|
||||
|
||||
@@ -243,11 +243,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Padding = new MarginPadding { Right = OsuGame.SCREEN_EDGE_MARGIN };
|
||||
|
||||
InternalChild = NextButton = new ShearedButton(0)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 12f },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Text = FirstRunSetupOverlayStrings.GetStarted,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -81,5 +83,33 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// Create a fresh <see cref="Mod"/> instance based on this mod.
|
||||
/// </summary>
|
||||
Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType())!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any user adjustable setting attached to this mod has a non-default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This returns the instantaneous state of this mod. It may change over time.
|
||||
/// For tracking changes on a dynamic display, make sure to setup a <see cref="ModSettingChangeTracker"/>.
|
||||
/// </remarks>
|
||||
bool HasNonDefaultSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
bool hasAdjustments = false;
|
||||
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = (IBindable)property.GetValue(this)!;
|
||||
|
||||
if (!bindable.IsDefault)
|
||||
{
|
||||
hasAdjustments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasAdjustments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -67,6 +68,22 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
@@ -94,5 +111,26 @@ namespace osu.Game.Rulesets.Mods
|
||||
if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value;
|
||||
if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of settings on this mod instance which have been adjusted by the user from their default values.
|
||||
/// </summary>
|
||||
protected int UserAdjustedSettingsCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = (IBindable)property.GetValue(this)!;
|
||||
|
||||
if (!bindable.IsDefault)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
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.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
@@ -81,6 +82,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private Container extendedContent = null!;
|
||||
|
||||
private Drawable adjustmentMarker = null!;
|
||||
|
||||
private Circle cogBackground = null!;
|
||||
private SpriteIcon cog = null!;
|
||||
|
||||
private ModSettingChangeTracker? modSettingsChangeTracker;
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +145,7 @@ namespace osu.Game.Rulesets.UI
|
||||
Origin = Anchor.CentreLeft,
|
||||
Name = "main content",
|
||||
Size = MOD_ICON_SIZE,
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
background = new Sprite
|
||||
{
|
||||
@@ -165,6 +171,29 @@ namespace osu.Game.Rulesets.UI
|
||||
Size = new Vector2(45),
|
||||
Icon = FontAwesome.Solid.Question
|
||||
},
|
||||
adjustmentMarker = new Container
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(64, 14),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
cogBackground = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
cog = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Cog,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.6f),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -216,11 +245,18 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
extendedContent.Alpha = showExtended ? 1 : 0;
|
||||
extendedText.Text = mod.ExtendedIconInformation;
|
||||
|
||||
if (mod.HasNonDefaultSettings)
|
||||
adjustmentMarker.Show();
|
||||
else
|
||||
adjustmentMarker.Hide();
|
||||
}
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
cogBackground.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
cog.Colour = backgroundColour;
|
||||
|
||||
extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
|
||||
extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f);
|
||||
|
||||
@@ -108,12 +108,14 @@ namespace osu.Game.Screens.Backgrounds
|
||||
if (Background != null)
|
||||
{
|
||||
newDepth = Background.Depth + 1;
|
||||
Background.FinishTransforms();
|
||||
Background.FadeOut(250);
|
||||
Background.Expire();
|
||||
}
|
||||
|
||||
b.Depth = newDepth;
|
||||
b.Anchor = b.Origin = Anchor.Centre;
|
||||
b.FadeInFromZero(500, Easing.OutQuint);
|
||||
b.ScaleTo(1.02f).ScaleTo(1, 3500, Easing.OutQuint);
|
||||
dimmable.Background = Background = b;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Submission
|
||||
}
|
||||
|
||||
if (playableBeatmap.BeatmapInfo.OnlineID > 0)
|
||||
throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!");
|
||||
throw new InvalidOperationException($@"Difficulty ""{playableBeatmap.BeatmapInfo.DifficultyName}"" has BeatmapID {playableBeatmap.BeatmapInfo.OnlineID} that has not been assigned to it by the server!");
|
||||
|
||||
if (allocatedBeatmapIds.Count == 0)
|
||||
throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!");
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Screens.Footer
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Padding = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
@@ -89,7 +89,7 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Y = 10f,
|
||||
Y = ScreenFooterButton.Y_OFFSET,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(7, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Screens.Footer
|
||||
footerContentContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = -15f,
|
||||
Y = -OsuGame.SCREEN_EDGE_MARGIN,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Screens.Footer
|
||||
hiddenButtonsContainer = new Container<ScreenFooterButton>
|
||||
{
|
||||
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Y = 10f,
|
||||
Y = ScreenFooterButton.Y_OFFSET,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected const int CORNER_RADIUS = 10;
|
||||
public const int Y_OFFSET = 10;
|
||||
|
||||
protected const int BUTTON_HEIGHT = 75;
|
||||
protected const int BUTTON_WIDTH = 116;
|
||||
|
||||
@@ -87,7 +88,7 @@ namespace osu.Game.Screens.Footer
|
||||
},
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@@ -134,7 +135,7 @@ namespace osu.Game.Screens.Footer
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -CORNER_RADIUS,
|
||||
Y = -Y_OFFSET,
|
||||
Size = new Vector2(100, 5),
|
||||
Masking = true,
|
||||
CornerRadius = 3,
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
});
|
||||
leaderboardFlow.Insert(0, Leaderboard = new DrawableGameplayLeaderboard
|
||||
{
|
||||
Expanded = { Value = true }
|
||||
ForceExpand = { Value = true }
|
||||
});
|
||||
|
||||
LoadComponentAsync(new GameplayChatDisplay(room)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class DrawableGameplayLeaderboard : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
public readonly Bindable<bool> ForceExpand = new Bindable<bool>();
|
||||
|
||||
protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private readonly IBindable<LocalUserPlayingState> userPlayingState = new Bindable<LocalUserPlayingState>();
|
||||
private readonly IBindable<bool> holdingForHUD = new Bindable<bool>();
|
||||
|
||||
private const int max_panels = 8;
|
||||
private readonly Bindable<bool> expanded = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new leaderboard.
|
||||
@@ -100,6 +100,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
configVisibility.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
userPlayingState.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
holdingForHUD.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
ForceExpand.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
updateState();
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
scroll.ScrollToStart(false);
|
||||
|
||||
Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && configVisibility.Value ? 1 : 0, 100, Easing.OutQuint);
|
||||
Expanded.Value = userPlayingState.Value == LocalUserPlayingState.Playing || holdingForHUD.Value;
|
||||
expanded.Value = ForceExpand.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,7 +129,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
TrackedScore = drawable;
|
||||
}
|
||||
|
||||
drawable.Expanded.BindTo(Expanded);
|
||||
drawable.Expanded.BindTo(expanded);
|
||||
|
||||
Flow.Add(drawable);
|
||||
drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort));
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
// 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 System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
public partial class SoloResultsScreen : ResultsScreen
|
||||
{
|
||||
private GetScoresRequest? getScoreRequest;
|
||||
private readonly IBindable<LeaderboardScores?> globalScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
public SoloResultsScreen(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
globalScores.BindTo(leaderboardManager.Scores);
|
||||
}
|
||||
|
||||
protected override async Task<ScoreInfo[]> FetchScores()
|
||||
{
|
||||
Debug.Assert(Score != null);
|
||||
@@ -39,52 +41,93 @@ namespace osu.Game.Screens.Ranking
|
||||
if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
|
||||
return [];
|
||||
|
||||
var requestTaskSource = new TaskCompletionSource<APIScoresCollection>();
|
||||
|
||||
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
|
||||
getScoreRequest.Success += requestTaskSource.SetResult;
|
||||
getScoreRequest.Failure += requestTaskSource.SetException;
|
||||
api.Queue(getScoreRequest);
|
||||
|
||||
try
|
||||
var criteria = new LeaderboardCriteria(
|
||||
Score.BeatmapInfo!,
|
||||
Score.Ruleset,
|
||||
leaderboardManager.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global,
|
||||
leaderboardManager.CurrentCriteria?.ExactMods
|
||||
);
|
||||
var requestTaskSource = new TaskCompletionSource<LeaderboardScores>();
|
||||
globalScores.BindValueChanged(_ =>
|
||||
{
|
||||
var scores = await requestTaskSource.Task.ConfigureAwait(false);
|
||||
var toDisplay = new List<ScoreInfo>();
|
||||
if (globalScores.Value != null && leaderboardManager.CurrentCriteria?.Equals(criteria) == true)
|
||||
requestTaskSource.TrySetResult(globalScores.Value);
|
||||
});
|
||||
leaderboardManager.FetchWithCriteria(criteria, forceRefresh: true);
|
||||
|
||||
for (int i = 0; i < scores.Scores.Count; ++i)
|
||||
{
|
||||
var score = scores.Scores[i];
|
||||
int position = i + 1;
|
||||
var result = await requestTaskSource.Task.ConfigureAwait(false);
|
||||
|
||||
if (score.MatchesOnlineID(Score))
|
||||
{
|
||||
// we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect,
|
||||
// so we have to fish out the actual drawable panel and set the position to it directly.
|
||||
var panel = ScorePanelList.GetPanelForScore(Score);
|
||||
Score.Position = panel.ScorePosition.Value = position;
|
||||
}
|
||||
else
|
||||
{
|
||||
var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo);
|
||||
converted.Position = position;
|
||||
toDisplay.Add(converted);
|
||||
}
|
||||
}
|
||||
|
||||
return toDisplay.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (result.FailState != null)
|
||||
{
|
||||
Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}");
|
||||
Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {result.FailState}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
var clonedScores = result.AllScores.Select(s => s.DeepClone()).ToArray();
|
||||
|
||||
getScoreRequest?.Cancel();
|
||||
List<ScoreInfo> sortedScores = [];
|
||||
|
||||
foreach (var clonedScore in clonedScores)
|
||||
{
|
||||
// ensure that we do not double up on the score being presented here.
|
||||
// additionally, ensure that the reference that ends up in `sortedScores` is the `Score` reference specifically.
|
||||
// this simplifies handling later.
|
||||
if (clonedScore.Equals(Score) || clonedScore.MatchesOnlineID(Score))
|
||||
{
|
||||
Score.Position = clonedScore.Position;
|
||||
sortedScores.Add(Score);
|
||||
}
|
||||
else
|
||||
sortedScores.Add(clonedScore);
|
||||
}
|
||||
|
||||
// if we haven't encountered a match for the presented score, we still need to attach it.
|
||||
// note that the above block ensuring that the `Score` reference makes it in here makes this valid to write in this way.
|
||||
if (!sortedScores.Contains(Score))
|
||||
sortedScores.Add(Score);
|
||||
|
||||
sortedScores = sortedScores.OrderByTotalScore().ToList();
|
||||
|
||||
int delta = 0;
|
||||
bool isPartialLeaderboard = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && result.TopScores.Count >= 50;
|
||||
|
||||
for (int i = 0; i < sortedScores.Count; i++)
|
||||
{
|
||||
var sortedScore = sortedScores[i];
|
||||
|
||||
// see `SoloGameplayLeaderboardProvider.sort()` for another place that does the same thing with slight deviations
|
||||
// if this code is changed, that code should probably be changed as well
|
||||
|
||||
if (!isPartialLeaderboard)
|
||||
sortedScore.Position = i + 1;
|
||||
else
|
||||
{
|
||||
if (ReferenceEquals(sortedScore, Score) && sortedScore.Position == null)
|
||||
{
|
||||
int? previousScorePosition = i > 0 ? sortedScores[i - 1].Position : 0;
|
||||
int? nextScorePosition = i < result.TopScores.Count - 1 ? sortedScores[i + 1].Position : null;
|
||||
|
||||
if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition)
|
||||
{
|
||||
sortedScore.Position = previousScorePosition + 1;
|
||||
delta += 1;
|
||||
}
|
||||
else
|
||||
sortedScore.Position = null;
|
||||
}
|
||||
else
|
||||
sortedScore.Position += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// there's a non-zero chance that the `Score.Position` was mutated above,
|
||||
// but that is not actually coupled to `ScorePosition` of the relevant score panel in any way,
|
||||
// so ensure that the drawable panel also receives the updated position.
|
||||
// note that this is valid to do precisely because we ensured `Score` was in `sortedScores` earlier.
|
||||
ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = Score.Position;
|
||||
|
||||
sortedScores.Remove(Score);
|
||||
return sortedScores.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
Tracked = tracked;
|
||||
TotalScore.Value = scoreInfo.TotalScore;
|
||||
Accuracy.Value = scoreInfo.Accuracy;
|
||||
Combo.Value = scoreInfo.Combo;
|
||||
Combo.Value = scoreInfo.MaxCombo;
|
||||
TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
GetDisplayScore = scoreInfo.GetDisplayScore;
|
||||
InitialPosition = scoreInfo.Position;
|
||||
|
||||
@@ -70,6 +70,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
|
||||
// see `SoloResultsScreen.FetchScores()` for another place that does the same thing with slight deviations
|
||||
// if this code is changed, that code should probably be changed as well
|
||||
|
||||
score.DisplayOrder.Value = i + 1;
|
||||
|
||||
// if we know we have all scores there can ever be, we can do the simple and obvious thing.
|
||||
|
||||
@@ -137,8 +137,8 @@ namespace osu.Game.Screens.Select
|
||||
// TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
|
||||
if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false)
|
||||
{
|
||||
textFlow.AddParagraph("- Try");
|
||||
textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
textFlow.AddParagraph("- Try ");
|
||||
textFlow.AddLink("enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
textFlow.AddText("automatic conversion!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
@@ -31,12 +32,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private readonly LoadingLayer loading;
|
||||
|
||||
private readonly BeatmapCarouselFilterMatching matching;
|
||||
private readonly BeatmapCarouselFilterGrouping grouping;
|
||||
|
||||
/// <summary>
|
||||
/// Total number of beatmap difficulties displayed with the filter.
|
||||
/// </summary>
|
||||
public int MatchedBeatmapsCount => matching.BeatmapItemsCount;
|
||||
|
||||
protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom)
|
||||
{
|
||||
// Group panels do not overlap with any other panel but should overlap with themselves.
|
||||
if ((top.Model is GroupDefinition) ^ (bottom.Model is GroupDefinition))
|
||||
return SPACING * 2;
|
||||
|
||||
// Beatmap difficulty panels do not overlap with themselves or any other panel.
|
||||
if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo)
|
||||
// Beatmap difficulty panels do not overlap with themselves or any other panel.
|
||||
return SPACING;
|
||||
|
||||
return -SPACING;
|
||||
@@ -49,6 +60,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Filters = new ICarouselFilter[]
|
||||
{
|
||||
matching = new BeatmapCarouselFilterMatching(() => Criteria),
|
||||
new BeatmapCarouselFilterSorting(() => Criteria),
|
||||
grouping = new BeatmapCarouselFilterGrouping(() => Criteria),
|
||||
};
|
||||
@@ -331,11 +343,21 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public FilterCriteria Criteria { get; private set; } = new FilterCriteria();
|
||||
|
||||
private ScheduledDelegate? loadingDebounce;
|
||||
|
||||
public void Filter(FilterCriteria criteria)
|
||||
{
|
||||
Criteria = criteria;
|
||||
loading.Show();
|
||||
FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide()));
|
||||
|
||||
loadingDebounce ??= Scheduler.AddDelayed(() => loading.Show(), 250);
|
||||
|
||||
FilterAsync().ContinueWith(_ => Schedule(() =>
|
||||
{
|
||||
loadingDebounce?.Cancel();
|
||||
loadingDebounce = null;
|
||||
|
||||
loading.Hide();
|
||||
}));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
this.getCriteria = getCriteria;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken)
|
||||
public async Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public class BeatmapCarouselFilterMatching : ICarouselFilter
|
||||
{
|
||||
private readonly Func<FilterCriteria> getCriteria;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of beatmap difficulties displayed post filter.
|
||||
/// </summary>
|
||||
public int BeatmapItemsCount { get; private set; }
|
||||
|
||||
public BeatmapCarouselFilterMatching(Func<FilterCriteria> getCriteria)
|
||||
{
|
||||
this.getCriteria = getCriteria;
|
||||
}
|
||||
|
||||
public async Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
|
||||
{
|
||||
var criteria = getCriteria();
|
||||
|
||||
return matchItems(items, criteria).ToList();
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
private IEnumerable<CarouselItem> matchItems(IEnumerable<CarouselItem> items, FilterCriteria criteria)
|
||||
{
|
||||
int countMatching = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var beatmap = (BeatmapInfo)item.Model;
|
||||
|
||||
if (checkMatch(beatmap, criteria))
|
||||
{
|
||||
countMatching++;
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
BeatmapItemsCount = countMatching;
|
||||
}
|
||||
|
||||
private static bool checkMatch(BeatmapInfo beatmap, FilterCriteria criteria)
|
||||
{
|
||||
bool match = criteria.Ruleset == null ||
|
||||
beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName ||
|
||||
(beatmap.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps);
|
||||
|
||||
if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
|
||||
{
|
||||
// only check ruleset equality or convertability for selected beatmap
|
||||
return match;
|
||||
}
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
if (criteria.SearchTerms.Length > 0)
|
||||
{
|
||||
match = beatmap.Match(criteria.SearchTerms);
|
||||
|
||||
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
|
||||
// this should be done after text matching so we can prioritise matching numbers in metadata.
|
||||
if (!match && criteria.SearchNumber.HasValue)
|
||||
{
|
||||
match = (beatmap.OnlineID == criteria.SearchNumber.Value) ||
|
||||
(beatmap.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(beatmap.StarRating);
|
||||
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(beatmap.Difficulty.ApproachRate);
|
||||
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(beatmap.Difficulty.DrainRate);
|
||||
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(beatmap.Difficulty.CircleSize);
|
||||
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(beatmap.Difficulty.OverallDifficulty);
|
||||
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(beatmap.Length);
|
||||
match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(beatmap.LastPlayed ?? DateTimeOffset.MinValue);
|
||||
match &= !criteria.DateRanked.HasFilter || (beatmap.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(beatmap.BeatmapSet.DateRanked.Value));
|
||||
match &= !criteria.DateSubmitted.HasFilter || (beatmap.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(beatmap.BeatmapSet.DateSubmitted.Value));
|
||||
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(beatmap.BPM);
|
||||
|
||||
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(beatmap.BeatDivisor);
|
||||
match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(beatmap.Status);
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(beatmap.Metadata.Author.Username);
|
||||
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(beatmap.Metadata.Artist) ||
|
||||
criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode);
|
||||
match &= !criteria.Title.HasFilter || criteria.Title.Matches(beatmap.Metadata.Title) ||
|
||||
criteria.Title.Matches(beatmap.Metadata.TitleUnicode);
|
||||
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(beatmap.DifficultyName);
|
||||
match &= !criteria.Source.HasFilter || criteria.Source.Matches(beatmap.Metadata.Source);
|
||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(beatmap.StarRating);
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(beatmap.MD5Hash) ?? true;
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(beatmap, criteria);
|
||||
|
||||
if (match && criteria.HasOnlineID == true)
|
||||
match &= beatmap.OnlineID >= 0;
|
||||
|
||||
if (match && criteria.BeatmapSetId != null)
|
||||
match &= criteria.BeatmapSetId == beatmap.BeatmapSet?.OnlineID;
|
||||
|
||||
return match;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
this.getCriteria = getCriteria;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
|
||||
public async Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
|
||||
{
|
||||
var criteria = getCriteria();
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
|
||||
return comparison;
|
||||
}));
|
||||
})).ToList();
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
/// <summary>
|
||||
/// The left portion of the song select screen which houses the metadata or leaderboards wedge, along with controls
|
||||
/// to switch between them and adjust specifics.
|
||||
/// </summary>
|
||||
public partial class BeatmapDetailsArea : VisibilityContainer
|
||||
{
|
||||
private Header header = null!;
|
||||
private Container contentContainer = null!;
|
||||
|
||||
public BeatmapDetailsArea()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float header_height = 35f;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new ShearAligningWrapper(header = new Header
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = header_height,
|
||||
}),
|
||||
new ShearAligningWrapper(contentContainer = new Container
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Top = header_height },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
})
|
||||
{
|
||||
Depth = 1f,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
header.Type.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToX(-150, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
private Drawable? currentContent;
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (currentContent != null)
|
||||
{
|
||||
currentContent.Hide();
|
||||
currentContent.Expire();
|
||||
}
|
||||
|
||||
switch (header.Type.Value)
|
||||
{
|
||||
default:
|
||||
case Header.Selection.Details:
|
||||
currentContent = new BeatmapMetadataWedge();
|
||||
break;
|
||||
|
||||
case Header.Selection.Ranking:
|
||||
currentContent = new BeatmapLeaderboardWedge
|
||||
{
|
||||
Scope = { BindTarget = header.Scope },
|
||||
FilterBySelectedMods = { BindTarget = header.FilterBySelectedMods },
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
contentContainer.Add(currentContent);
|
||||
currentContent.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapDetailsArea
|
||||
{
|
||||
public partial class Header : CompositeDrawable
|
||||
{
|
||||
private WedgeSelector<Selection> tabControl = null!;
|
||||
private FillFlowContainer leaderboardControls = null!;
|
||||
|
||||
private ShearedDropdown<BeatmapLeaderboardScope> scopeDropdown = null!;
|
||||
private ShearedToggleButton selectedModsToggle = null!;
|
||||
|
||||
public IBindable<Selection> Type => tabControl.Current;
|
||||
|
||||
public IBindable<BeatmapLeaderboardScope> Scope => scopeDropdown.Current;
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods => selectedModsToggle.Active;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
tabControl = new WedgeSelector<Selection>(20f)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 200,
|
||||
Height = 22,
|
||||
Margin = new MarginPadding { Top = 2f },
|
||||
},
|
||||
leaderboardControls = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(5f, 0f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(128f, 30f),
|
||||
Child = selectedModsToggle = new ShearedToggleButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = @"Selected Mods",
|
||||
Height = 30,
|
||||
},
|
||||
},
|
||||
// new Container
|
||||
// {
|
||||
// Anchor = Anchor.CentreRight,
|
||||
// Origin = Anchor.CentreRight,
|
||||
// Size = new Vector2(150f, 33f),
|
||||
// Child = new ShearedDropdown<RankingsSort>(@"Sort")
|
||||
// {
|
||||
// Width = 150f,
|
||||
// Items = Enum.GetValues<RankingsSort>(),
|
||||
// },
|
||||
// },
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(160f, 32f),
|
||||
Child = scopeDropdown = new ScopeDropdown
|
||||
{
|
||||
Width = 160f,
|
||||
Current = { Value = BeatmapLeaderboardScope.Global },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
tabControl.Current.BindValueChanged(v =>
|
||||
{
|
||||
leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public enum Selection
|
||||
{
|
||||
Details,
|
||||
Ranking,
|
||||
}
|
||||
|
||||
// public enum RankingsSort
|
||||
// {
|
||||
// Score,
|
||||
// Accuracy,
|
||||
// Combo,
|
||||
// Misses,
|
||||
// Date,
|
||||
// }
|
||||
|
||||
private partial class ScopeDropdown : ShearedDropdown<BeatmapLeaderboardScope>
|
||||
{
|
||||
public ScopeDropdown()
|
||||
: base("Scope")
|
||||
{
|
||||
Items = Enum.GetValues<BeatmapLeaderboardScope>();
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(BeatmapLeaderboardScope item) => item.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapDetailsArea
|
||||
{
|
||||
public partial class WedgeSelector<T> : TabControl<T>
|
||||
where T : struct, Enum
|
||||
{
|
||||
private Circle strip = null!;
|
||||
|
||||
protected override Dropdown<T>? CreateDropdown() => null;
|
||||
|
||||
protected override TabItem<T> CreateTabItem(T value) => new TabItem(value);
|
||||
|
||||
protected new TabItem SelectedTab => (TabItem)base.SelectedTab;
|
||||
|
||||
public WedgeSelector(float spacing)
|
||||
{
|
||||
TabContainer.Spacing = new Vector2(spacing, 0f);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
AddInternal(strip = new Circle
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Height = 2,
|
||||
Colour = colourProvider.Highlight1,
|
||||
});
|
||||
|
||||
foreach (var type in Enum.GetValues<T>())
|
||||
AddItem(type);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
updateDisplay();
|
||||
FinishTransforms(true);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
strip.MoveToX(SelectedTab.Text.ToSpaceOfOtherDrawable(Vector2.Zero, this).X, 300, Easing.OutQuint);
|
||||
strip.ResizeWidthTo(SelectedTab.Text.Width, 0, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected partial class TabItem : TabItem<T>
|
||||
{
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public readonly OsuSpriteText Text;
|
||||
|
||||
public TabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
Text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = value.ToString(),
|
||||
Font = OsuFont.Style.Body,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
protected override void OnActivated() => updateDisplay();
|
||||
|
||||
protected override void OnDeactivated() => updateDisplay();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e) => updateDisplay();
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (Active.Value || IsHovered)
|
||||
Text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
|
||||
else
|
||||
Text.FadeColour(colourProvider.Content2, 300, Easing.OutQuint);
|
||||
|
||||
Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,371 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardScore
|
||||
{
|
||||
public partial class LeaderboardScoreTooltip : VisibilityContainer, ITooltip<ScoreInfo>
|
||||
{
|
||||
private const float spacing = 20f;
|
||||
|
||||
private DateAndStatisticsPanel dateAndStatistics = null!;
|
||||
private ModsPanel modsPanel = null!;
|
||||
private TotalScoreRankPanel totalScoreRankPanel = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider;
|
||||
|
||||
public LeaderboardScoreTooltip(OverlayColourProvider colourProvider)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Width = 170;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, -spacing),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
dateAndStatistics = new DateAndStatisticsPanel(),
|
||||
modsPanel = new ModsPanel(),
|
||||
totalScoreRankPanel = new TotalScoreRankPanel(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private ScoreInfo? lastContent;
|
||||
|
||||
public void SetContent(ScoreInfo content)
|
||||
{
|
||||
if (lastContent != null && lastContent.Equals(content))
|
||||
return;
|
||||
|
||||
dateAndStatistics.Score = content;
|
||||
modsPanel.Score = content;
|
||||
totalScoreRankPanel.Score = content;
|
||||
lastContent = content;
|
||||
}
|
||||
|
||||
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
|
||||
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
private partial class DateAndStatisticsPanel : CompositeDrawable
|
||||
{
|
||||
private OsuSpriteText absoluteDate = null!;
|
||||
private DrawableDate relativeDate = null!;
|
||||
private FillFlowContainer statistics = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt");
|
||||
relativeDate.Date = value.Date;
|
||||
|
||||
var judgementsStatistics = value.GetStatisticsForDisplay().Select(s =>
|
||||
new StatisticRow(s.DisplayName.ToUpper(), colours.ForHitResult(s.Result), s.Count.ToLocalisableString("N0")));
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in value.Mods)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
|
||||
var generalStatistics = new[]
|
||||
{
|
||||
new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)),
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")),
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()),
|
||||
};
|
||||
|
||||
if (value.PP != null)
|
||||
{
|
||||
generalStatistics = new[]
|
||||
{
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, value.PP.ToLocalisableString("N0"))
|
||||
}.Concat(generalStatistics).ToArray();
|
||||
}
|
||||
|
||||
statistics.ChildrenEnumerable = judgementsStatistics
|
||||
.Append(Empty().With(d => d.Height = 20))
|
||||
.Concat(generalStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background4,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Margin = new MarginPadding { Top = 8f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
absoluteDate = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
relativeDate = new DrawableDate(default, OsuFont.Style.Caption1.Size)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = colourProvider.Content2,
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
CornerRadius = corner_radius,
|
||||
Masking = true,
|
||||
Margin = new MarginPadding { Top = 4f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
statistics = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Padding = new MarginPadding(8f),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class StatisticRow : CompositeDrawable
|
||||
{
|
||||
public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = label,
|
||||
Colour = labelColour,
|
||||
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = value,
|
||||
Colour = Color4.White,
|
||||
Font = OsuFont.Style.Caption2,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ModsPanel : CompositeDrawable
|
||||
{
|
||||
private FillFlowContainer modsFlow = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
var mods = value.Mods;
|
||||
|
||||
if (!mods.Any())
|
||||
Hide();
|
||||
else
|
||||
{
|
||||
Show();
|
||||
|
||||
modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModIcon(m)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.3f),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background4,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Transparent,
|
||||
},
|
||||
modsFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Bottom = 6f, Top = 6f + spacing },
|
||||
Padding = new MarginPadding { Horizontal = 16f },
|
||||
Spacing = new Vector2(2f, -4f),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class TotalScoreRankPanel : CompositeDrawable
|
||||
{
|
||||
private Box rankBackground = null!;
|
||||
private Container<DrawableRank> rankContainer = null!;
|
||||
private OsuSpriteText totalScore = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
rankBackground.Colour = ColourInfo.GradientVertical(
|
||||
OsuColour.ForRank(value.Rank).Opacity(0f),
|
||||
OsuColour.ForRank(value.Rank).Opacity(0.5f));
|
||||
rankContainer.Child = new DrawableRank(value.Rank);
|
||||
totalScore.Current = scoreManager.GetBindableTotalScoreString(value);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#353535"),
|
||||
},
|
||||
rankBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
rankContainer = new Container<DrawableRank>
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = new Vector2(25f, 14f),
|
||||
Margin = new MarginPadding { Bottom = 5f },
|
||||
},
|
||||
totalScore = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Margin = new MarginPadding { Bottom = 25f, Top = 10f + spacing },
|
||||
Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true),
|
||||
Spacing = new Vector2(-1.5f),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// 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.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Placeholders;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardWedge : VisibilityContainer
|
||||
{
|
||||
public IBindable<BeatmapLeaderboardScope> Scope { get; } = new Bindable<BeatmapLeaderboardScope>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods { get; } = new BindableBool();
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
private Container<Placeholder> placeholderContainer = null!;
|
||||
private Placeholder? placeholder;
|
||||
|
||||
private Container scoresContainer = null!;
|
||||
|
||||
private OsuScrollContainer scoresScroll = null!;
|
||||
private Container personalBestDisplay = null!;
|
||||
|
||||
private Container<BeatmapLeaderboardScore> personalBestScoreContainer = null!;
|
||||
private LoadingLayer loading = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
private const float personal_best_height = 80;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
scoresScroll = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Child = scoresContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = 5,
|
||||
// Left padding offsets the shear to create a visually appealing list display.
|
||||
Left = 80f,
|
||||
// Bottom padding ensures the last entry's full width is displayed
|
||||
// (ie it is fully on screen after shear is considered).
|
||||
Bottom = BeatmapLeaderboardScore.HEIGHT * 3
|
||||
},
|
||||
},
|
||||
},
|
||||
personalBestDisplay = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = personal_best_height,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Margin = new MarginPadding { Left = -40f },
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
// push the personal best 1px down to hide masking issues
|
||||
Y = 1f,
|
||||
X = -100f,
|
||||
Alpha = 0f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Top = 5f, Bottom = 5f, Left = 70f, Right = 10f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Text = "Personal Best",
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
personalBestScoreContainer = new Container<BeatmapLeaderboardScore>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 20f },
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
placeholderContainer = new Container<Placeholder>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
loading = new LoadingLayer(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Scope.BindValueChanged(_ => refetchScores());
|
||||
FilterBySelectedMods.BindValueChanged(_ => refetchScores());
|
||||
beatmap.BindValueChanged(_ => refetchScores());
|
||||
ruleset.BindValueChanged(_ => refetchScores());
|
||||
mods.BindValueChanged(_ => refetchScoresFromMods());
|
||||
|
||||
refetchScores();
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void refetchScoresFromMods()
|
||||
{
|
||||
if (FilterBySelectedMods.Value)
|
||||
refetchScores();
|
||||
}
|
||||
|
||||
private bool initialFetchComplete;
|
||||
|
||||
private void refetchScores()
|
||||
{
|
||||
SetScores(Array.Empty<ScoreInfo>(), null);
|
||||
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
SetState(LeaderboardState.NoneSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
SetState(LeaderboardState.Retrieving);
|
||||
|
||||
var fetchBeatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
|
||||
|
||||
// For now, we forcefully refresh to keep things simple.
|
||||
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
|
||||
// (like returning from gameplay after setting a new score, returning to song select after main menu).
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true);
|
||||
|
||||
if (!initialFetchComplete)
|
||||
{
|
||||
// only bind this after the first fetch to avoid reading stale scores.
|
||||
fetchedScores.BindTo(leaderboardManager.Scores);
|
||||
fetchedScores.BindValueChanged(_ => updateScores(), true);
|
||||
initialFetchComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScores()
|
||||
{
|
||||
var scores = fetchedScores.Value;
|
||||
|
||||
if (scores == null) return;
|
||||
|
||||
if (scores.FailState != null)
|
||||
SetState((LeaderboardState)scores.FailState);
|
||||
else
|
||||
SetScores(scores.TopScores, scores.UserScore);
|
||||
}
|
||||
|
||||
protected void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore)
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
clearScores();
|
||||
SetState(LeaderboardState.Success);
|
||||
|
||||
if (!scores.Any())
|
||||
{
|
||||
SetState(LeaderboardState.NoScores);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s)
|
||||
{
|
||||
Rank = i + 1,
|
||||
IsPersonalBest = s.OnlineID == userScore?.OnlineID,
|
||||
SelectedMods = { BindTarget = mods },
|
||||
}), loadedScores =>
|
||||
{
|
||||
int delay = 200;
|
||||
int i = 0;
|
||||
|
||||
foreach (var d in loadedScores)
|
||||
{
|
||||
d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i;
|
||||
|
||||
// This is a bit of a weird one. We're already in a sheared state and don't want top-level
|
||||
// shear applied, but still need the `BeatmapLeadeboardScore` to be in "sheared" mode (see ctor).
|
||||
d.Shear = Vector2.Zero;
|
||||
|
||||
scoresContainer.Add(d);
|
||||
|
||||
d.FadeOut()
|
||||
.MoveToX(-20f)
|
||||
.Delay(delay)
|
||||
.FadeIn(300, Easing.OutQuint)
|
||||
.MoveToX(0f, 300, Easing.OutQuint);
|
||||
|
||||
delay += 30;
|
||||
i++;
|
||||
}
|
||||
}, cancellation: cancellationTokenSource.Token);
|
||||
|
||||
if (userScore != null)
|
||||
{
|
||||
personalBestDisplay.MoveToX(0, 600, Easing.OutQuint);
|
||||
personalBestDisplay.FadeIn(600, Easing.OutQuint);
|
||||
personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore)
|
||||
{
|
||||
IsPersonalBest = true,
|
||||
Rank = userScore.Position,
|
||||
SelectedMods = { BindTarget = mods },
|
||||
};
|
||||
|
||||
scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = personal_best_height }, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearScores()
|
||||
{
|
||||
float delay = 0;
|
||||
|
||||
foreach (var d in scoresContainer)
|
||||
{
|
||||
// Avoid applying animations a second time to drawables which are already fading out.
|
||||
if (d.LifetimeEnd != double.MaxValue)
|
||||
continue;
|
||||
|
||||
d.Delay(delay)
|
||||
.MoveToX(-10f, 120, Easing.Out)
|
||||
.FadeOut(120, Easing.Out)
|
||||
.Expire();
|
||||
|
||||
delay += 20;
|
||||
}
|
||||
|
||||
personalBestDisplay.MoveToX(-100, 300, Easing.OutQuint);
|
||||
personalBestDisplay.FadeOut(300, Easing.OutQuint);
|
||||
scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding(), 300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private LeaderboardState displayedState;
|
||||
|
||||
protected void SetState(LeaderboardState state)
|
||||
{
|
||||
if (state == displayedState)
|
||||
return;
|
||||
|
||||
if (state == LeaderboardState.Retrieving)
|
||||
loading.Show();
|
||||
else
|
||||
loading.Hide();
|
||||
|
||||
displayedState = state;
|
||||
|
||||
placeholder?.FadeOut(150, Easing.OutQuint).Expire();
|
||||
placeholder = getPlaceholderFor(state);
|
||||
|
||||
if (placeholder == null)
|
||||
return;
|
||||
|
||||
clearScores();
|
||||
|
||||
placeholderContainer.Child = placeholder;
|
||||
|
||||
placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 900, Easing.OutQuint);
|
||||
placeholder.FadeInFromZero(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Placeholder? getPlaceholderFor(LeaderboardState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case LeaderboardState.NetworkFailure:
|
||||
return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync)
|
||||
{
|
||||
Action = refetchScores
|
||||
};
|
||||
|
||||
case LeaderboardState.NoneSelected:
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap);
|
||||
|
||||
case LeaderboardState.RulesetUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset);
|
||||
|
||||
case LeaderboardState.BeatmapUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap);
|
||||
|
||||
case LeaderboardState.NoScores:
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet);
|
||||
|
||||
case LeaderboardState.NotLoggedIn:
|
||||
return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards);
|
||||
|
||||
case LeaderboardState.NotSupporter:
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard);
|
||||
|
||||
case LeaderboardState.NoTeam:
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoTeam);
|
||||
|
||||
case LeaderboardState.Retrieving:
|
||||
return null;
|
||||
|
||||
case LeaderboardState.Success:
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -23,7 +24,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
private MetadataDisplay source = null!;
|
||||
private MetadataDisplay genre = null!;
|
||||
private MetadataDisplay language = null!;
|
||||
private MetadataDisplay tag = null!;
|
||||
private MetadataDisplay userTags = null!;
|
||||
private MetadataDisplay mapperTags = null!;
|
||||
private MetadataDisplay submitted = null!;
|
||||
private MetadataDisplay ranked = null!;
|
||||
|
||||
@@ -35,6 +37,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
private Drawable failRetryWedge = null!;
|
||||
private FailRetryDisplay failRetryDisplay = null!;
|
||||
|
||||
public bool RatingsVisible => ratingsWedge.Alpha > 0;
|
||||
public bool FailRetryVisible => failRetryWedge.Alpha > 0;
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
[Resolved]
|
||||
@@ -92,6 +97,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
AutoSizeDuration = (float)transition_duration / 3,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
@@ -148,7 +155,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
},
|
||||
},
|
||||
tag = new MetadataDisplay("Tags"),
|
||||
userTags = new MetadataDisplay("User Tags")
|
||||
{
|
||||
Alpha = 0,
|
||||
},
|
||||
mapperTags = new MetadataDisplay("Mapper Tags"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -250,7 +261,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
// We could consider hiding individual wedges based on zero data in the future.
|
||||
// Needs some experimentation on what looks good.
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null)
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var currentOnlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmap != null)
|
||||
{
|
||||
ratingsWedge.FadeIn(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(0, transition_duration, Easing.OutQuint);
|
||||
@@ -282,7 +296,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
else
|
||||
source.Data = ("-", null);
|
||||
|
||||
tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
|
||||
mapperTags.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
|
||||
submitted.Date = beatmapSetInfo.DateSubmitted;
|
||||
ranked.Date = beatmapSetInfo.DateRanked;
|
||||
|
||||
@@ -351,7 +365,34 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
updateUserTags();
|
||||
updateSubWedgeVisibility();
|
||||
}
|
||||
|
||||
private void updateUserTags()
|
||||
{
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = onlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
||||
|
||||
if (onlineBeatmap?.TopTags == null || onlineBeatmap.TopTags.Length == 0 || onlineBeatmapSet?.RelatedTags == null)
|
||||
{
|
||||
userTags.FadeOut(transition_duration, Easing.OutQuint);
|
||||
return;
|
||||
}
|
||||
|
||||
var tagsById = onlineBeatmapSet.RelatedTags.ToDictionary(t => t.Id);
|
||||
string[] userTagsArray = onlineBeatmap.TopTags
|
||||
.Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
|
||||
.Where(t => t.relatedTag != null)
|
||||
// see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
|
||||
.OrderByDescending(t => t.topTag.VoteCount)
|
||||
.ThenBy(t => t.relatedTag!.Name)
|
||||
.Select(t => t.relatedTag!.Name)
|
||||
.ToArray();
|
||||
|
||||
userTags.FadeIn(transition_duration, Easing.OutQuint);
|
||||
userTags.Tags = (userTagsArray, t => songSelect?.Search(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Layout;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
@@ -163,7 +164,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
Text = "...",
|
||||
Colour = colourProvider.Background4,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
}
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Button),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@@ -33,7 +35,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private const float corner_radius = 10;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
private IBindable<WorkingBeatmap> working { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
@@ -84,7 +86,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Masking = true;
|
||||
CornerRadius = corner_radius;
|
||||
|
||||
@@ -185,7 +186,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.BindValueChanged(_ => updateDisplay());
|
||||
working.BindValueChanged(_ => updateDisplay());
|
||||
ruleset.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
mods.BindValueChanged(m =>
|
||||
@@ -225,9 +226,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
var metadata = beatmap.Value.Metadata;
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
var metadata = working.Value.Metadata;
|
||||
var beatmapInfo = working.Value.BeatmapInfo;
|
||||
var beatmapSetInfo = working.Value.BeatmapSetInfo;
|
||||
|
||||
statusPill.Status = beatmapInfo.Status;
|
||||
|
||||
@@ -247,30 +248,48 @@ namespace osu.Game.Screens.SelectV2
|
||||
updateOnlineDisplay();
|
||||
}
|
||||
|
||||
private CancellationTokenSource? lengthBpmCancellationSource;
|
||||
|
||||
private void updateLengthAndBpmStatistics()
|
||||
{
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
lengthBpmCancellationSource?.Cancel();
|
||||
lengthBpmCancellationSource = new CancellationTokenSource();
|
||||
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
var token = lengthBpmCancellationSource.Token;
|
||||
|
||||
int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||
int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate);
|
||||
int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate);
|
||||
Task.Run(() =>
|
||||
{
|
||||
var beatmapInfo = working.Value.BeatmapInfo;
|
||||
// This can take time as it is a synchronous task.
|
||||
var beatmap = working.Value.Beatmap;
|
||||
|
||||
double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmapInfo.Length / rate);
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
|
||||
lengthStatistic.Text = hitLength.ToFormattedDuration();
|
||||
lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration());
|
||||
int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||
int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate);
|
||||
int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.GetMostCommonBeatLength(), rate);
|
||||
|
||||
bpmStatistic.Text = bpmMin == bpmMax
|
||||
? $"{bpmMin}"
|
||||
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||
double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmapInfo.Length / rate);
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
lengthStatistic.Text = hitLength.ToFormattedDuration();
|
||||
lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration());
|
||||
|
||||
bpmStatistic.Text = bpmMin == bpmMax
|
||||
? $"{bpmMin}"
|
||||
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||
});
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void refetchBeatmapSet()
|
||||
{
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
var beatmapSetInfo = working.Value.BeatmapSetInfo;
|
||||
|
||||
currentRequest?.Cancel();
|
||||
currentRequest = null;
|
||||
@@ -306,20 +325,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
else
|
||||
{
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID);
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID);
|
||||
|
||||
if (onlineBeatmap != null)
|
||||
{
|
||||
playCount.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
playCount.FadeOut(300, Easing.OutQuint);
|
||||
playCount.Value = null;
|
||||
}
|
||||
|
||||
favouritesStatistic.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1);
|
||||
favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -241,8 +242,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
cancellationSource?.Cancel();
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
|
||||
computeStarDifficulty(cancellationSource.Token);
|
||||
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
ratingAndNameContainer.FadeOut(300, Easing.OutQuint);
|
||||
@@ -254,17 +253,55 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName;
|
||||
mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author));
|
||||
mapperText.Text = beatmap.Value.Metadata.Author.Username;
|
||||
|
||||
var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value);
|
||||
|
||||
countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics()
|
||||
.Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
updateStarDifficulty(cancellationSource.Token);
|
||||
updateCountStatistics(cancellationSource.Token);
|
||||
updateDifficultyStatistics();
|
||||
}
|
||||
|
||||
private void updateStarDifficulty(CancellationToken cancellationToken)
|
||||
{
|
||||
difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
starRatingDisplay.Current.Value = task.GetResultSafely() ?? default;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private void updateCountStatistics(CancellationToken cancellationToken)
|
||||
{
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
countStatisticsDisplay.Statistics = Array.Empty<StatisticDifficulty.Data>();
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
// This can take time as it is a synchronous task.
|
||||
// TODO: We're calling `GetPlayableBeatmap` multiple times every map load at song select.
|
||||
var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value);
|
||||
var statistics = playableBeatmap.GetStatistics()
|
||||
.Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content))
|
||||
.ToList();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
countStatisticsDisplay.Statistics = statistics;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private void updateDifficultyStatistics() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
if (beatmap.IsDefault)
|
||||
@@ -321,21 +358,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
};
|
||||
});
|
||||
|
||||
private void computeStarDifficulty(CancellationToken cancellationToken)
|
||||
{
|
||||
difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
starRatingDisplay.Current.Value = task.GetResultSafely() ?? default;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// 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.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public 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 = 8;
|
||||
|
||||
private SongSelectSearchTextBox searchTextBox = null!;
|
||||
private ShearedToggleButton showConvertedBeatmapsButton = null!;
|
||||
private DifficultyRangeSlider difficultyRangeSlider = null!;
|
||||
private ShearedDropdown<SortMode> sortDropdown = null!;
|
||||
private ShearedDropdown<GroupMode> groupDropdown = null!;
|
||||
private CollectionDropdown collectionDropdown = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
public LocalisableString StatusText
|
||||
{
|
||||
get => searchTextBox.StatusText;
|
||||
set => searchTextBox.StatusText = value;
|
||||
}
|
||||
|
||||
public event Action<FilterCriteria>? CriteriaChanged;
|
||||
|
||||
private FilterCriteria currentCriteria = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Shear = OsuGame.SHEAR;
|
||||
Margin = new MarginPadding { Top = -corner_radius, Right = -40 };
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = corner_radius,
|
||||
Masking = true,
|
||||
Child = new WedgeBackground
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Scale = new Vector2(-1, 1),
|
||||
}
|
||||
},
|
||||
new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Padding = new MarginPadding { Top = corner_radius + 5, Bottom = 2, Right = 40f, Left = 2f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Child = searchTextBox = new SongSelectSearchTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
HoldFocus = true,
|
||||
},
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute), // can probably be removed?
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
difficultyRangeSlider = new DifficultyRangeSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
MinRange = 0.1f,
|
||||
},
|
||||
Empty(),
|
||||
showConvertedBeatmapsButton = new ShearedToggleButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = UserInterfaceStrings.ShowConverts,
|
||||
Height = 30f,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(maxSize: 210),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(maxSize: 230),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
sortDropdown = new ShearedDropdown<SortMode>(SortStrings.Default)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = Enum.GetValues<SortMode>(),
|
||||
},
|
||||
Empty(),
|
||||
// todo: pending localisation
|
||||
groupDropdown = new ShearedDropdown<GroupMode>("Group by")
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = Enum.GetValues<GroupMode>(),
|
||||
},
|
||||
Empty(),
|
||||
collectionDropdown = new CollectionDropdown
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
difficultyRangeSlider.LowerBound = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum);
|
||||
difficultyRangeSlider.UpperBound = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum);
|
||||
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmapsButton.Active);
|
||||
config.BindWith(OsuSetting.SongSelectSortingMode, sortDropdown.Current);
|
||||
config.BindWith(OsuSetting.SongSelectGroupingMode, groupDropdown.Current);
|
||||
|
||||
ruleset.BindValueChanged(_ => updateCriteria());
|
||||
mods.BindValueChanged(m =>
|
||||
{
|
||||
// The following is a note carried from old song select and may not be a valid reason anymore:
|
||||
// // Mods are updated once by the mod select overlay when song select is entered,
|
||||
// // regardless of if there are any mods or any changes have taken place.
|
||||
// // Updating the criteria here so early triggers a re-ordering of panels on song select, via... some mechanism.
|
||||
// // Todo: Investigate/fix and potentially remove this.
|
||||
// TODO: this might be simply removable with the new song select & carousel code.
|
||||
if (m.NewValue.SequenceEqual(m.OldValue))
|
||||
return;
|
||||
|
||||
var rulesetCriteria = currentCriteria.RulesetCriteria;
|
||||
if (rulesetCriteria?.FilterMayChangeFromMods(m) == true)
|
||||
updateCriteria();
|
||||
});
|
||||
|
||||
searchTextBox.Current.BindValueChanged(_ => updateCriteria());
|
||||
difficultyRangeSlider.LowerBound.BindValueChanged(_ => updateCriteria());
|
||||
difficultyRangeSlider.UpperBound.BindValueChanged(_ => updateCriteria());
|
||||
showConvertedBeatmapsButton.Active.BindValueChanged(_ => updateCriteria());
|
||||
sortDropdown.Current.BindValueChanged(_ => updateCriteria());
|
||||
groupDropdown.Current.BindValueChanged(_ => updateCriteria());
|
||||
collectionDropdown.Current.BindValueChanged(_ => updateCriteria());
|
||||
updateCriteria();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="FilterCriteria"/> based on the current state of the controls.
|
||||
/// </summary>
|
||||
public FilterCriteria CreateCriteria()
|
||||
{
|
||||
string query = searchTextBox.Current.Value;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Sort = sortDropdown.Current.Value,
|
||||
Group = groupDropdown.Current.Value,
|
||||
AllowConvertedBeatmaps = showConvertedBeatmapsButton.Active.Value,
|
||||
Ruleset = ruleset.Value,
|
||||
Mods = mods.Value,
|
||||
CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes).ToImmutableHashSet()
|
||||
};
|
||||
|
||||
if (!difficultyRangeSlider.LowerBound.IsDefault)
|
||||
criteria.UserStarDifficulty.Min = difficultyRangeSlider.LowerBound.Value;
|
||||
|
||||
if (!difficultyRangeSlider.UpperBound.IsDefault)
|
||||
criteria.UserStarDifficulty.Max = difficultyRangeSlider.UpperBound.Value;
|
||||
|
||||
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
return criteria;
|
||||
}
|
||||
|
||||
private void updateCriteria()
|
||||
{
|
||||
currentCriteria = CreateCriteria();
|
||||
CriteriaChanged?.Invoke(currentCriteria);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the query to the search text box.
|
||||
/// </summary>
|
||||
/// <param name="query">The string to search.</param>
|
||||
public void Search(string query)
|
||||
{
|
||||
searchTextBox.Current.Value = query;
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
private partial class SongSelectSearchTextBox : ShearedFilterTextBox
|
||||
{
|
||||
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox();
|
||||
|
||||
private partial class InnerTextBox : InnerFilterTextBox
|
||||
{
|
||||
public override bool HandleLeftRightArrows => false;
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
// the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action.
|
||||
if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete))
|
||||
return false;
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class FilterControl
|
||||
{
|
||||
public partial class DifficultyRangeSlider : ShearedRangeSlider
|
||||
{
|
||||
private Container borderContainer = null!;
|
||||
|
||||
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
private static readonly (float, Color4)[] spectrum = OsuColour.STAR_DIFFICULTY_SPECTRUM
|
||||
.Skip(1)
|
||||
.Prepend((0.0f, OsuColour.STAR_DIFFICULTY_SPECTRUM.ElementAt(1).Item2)).ToArray();
|
||||
|
||||
public DifficultyRangeSlider()
|
||||
: base("Star Rating")
|
||||
{
|
||||
NubWidth = ShearedNub.HEIGHT * 1.16f;
|
||||
TooltipSuffix = "stars";
|
||||
DefaultStringLowerBound = "0.0";
|
||||
DefaultStringUpperBound = "∞";
|
||||
DefaultTooltipUpperBound = UserInterfaceStrings.NoLimit;
|
||||
|
||||
AddLayout(drawSizeLayout);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
{
|
||||
SliderContainer.AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Depth = 1,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = 5f,
|
||||
Masking = true,
|
||||
ChildrenEnumerable = spectrum.Zip(spectrum.Skip(1))
|
||||
.Select(p => new Box
|
||||
{
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = p.First.Item1 / 10f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = (p.Second.Item1 - p.First.Item1) / 10f,
|
||||
Colour = ColourInfo.GradientHorizontal(p.First.Item2, p.Second.Item2),
|
||||
}),
|
||||
},
|
||||
borderContainer = new Container
|
||||
{
|
||||
Depth = -1,
|
||||
RelativePositionAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BorderColour = colourProvider.Highlight1,
|
||||
BorderThickness = 2,
|
||||
Masking = true,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = 5f,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.Transparent,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LowerBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false);
|
||||
UpperBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false);
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
updateBorderDisplay(true);
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBorderDisplay(bool instant)
|
||||
{
|
||||
float borderStart = LowerBoundSlider.NormalizedValue * LowerBoundSlider.UsableWidth / LowerBoundSlider.DrawWidth;
|
||||
float borderEnd = UpperBoundSlider.NormalizedValue * UpperBoundSlider.UsableWidth / UpperBoundSlider.DrawWidth;
|
||||
borderEnd += UpperBoundSlider.NubWidth / UpperBoundSlider.DrawWidth;
|
||||
|
||||
borderContainer.MoveToX(borderStart, instant ? 0 : 250, Easing.OutQuint);
|
||||
borderContainer.ResizeWidthTo(borderEnd - borderStart, instant ? 0 : 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override BoundSliderBar CreateBoundSlider(bool isUpper) => new DifficultyBoundSliderBar(this, isUpper);
|
||||
|
||||
private partial class DifficultyBoundSliderBar : BoundSliderBar
|
||||
{
|
||||
private readonly bool isUpper;
|
||||
|
||||
protected override bool FocusIndicator => false;
|
||||
|
||||
public DifficultyBoundSliderBar(ShearedRangeSlider slider, bool isUpper)
|
||||
: base(slider, isUpper)
|
||||
{
|
||||
this.isUpper = isUpper;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (isUpper)
|
||||
{
|
||||
LeftBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f);
|
||||
RightBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f);
|
||||
}
|
||||
else
|
||||
{
|
||||
LeftBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f);
|
||||
RightBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateDisplay(double value)
|
||||
{
|
||||
Colour4 nubColour = ColourUtils.SampleFromLinearGradient(spectrum, (float)Math.Round(value, 2, MidpointRounding.AwayFromZero));
|
||||
nubColour = nubColour.Lighten(0.4f);
|
||||
|
||||
if (value >= 8.0)
|
||||
nubColour = colours.Gray4;
|
||||
|
||||
Nub.AccentColour = nubColour;
|
||||
Nub.GlowingAccentColour = nubColour.Lighten(0.2f);
|
||||
Nub.ShadowColour = Color4.Black.Opacity(0.2f);
|
||||
NubText.Colour = OsuColour.ForegroundTextColourFor(nubColour);
|
||||
|
||||
base.UpdateDisplay(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Depth = float.MaxValue,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = Y_OFFSET,
|
||||
Size = new Vector2(BUTTON_WIDTH, bar_height),
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
@@ -115,7 +115,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
new Container
|
||||
{
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = Y_OFFSET,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = mod_display_portion,
|
||||
Masking = true,
|
||||
@@ -264,7 +264,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
CornerRadius = Y_OFFSET;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Depth = float.MaxValue;
|
||||
Origin = Anchor.BottomLeft;
|
||||
Shear = OsuGame.SHEAR;
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
CornerRadius = Y_OFFSET;
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = bar_height;
|
||||
Masking = true;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user