1
0
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:
Bartłomiej Dach
2025-05-09 10:02:18 +02:00
Unverified
106 changed files with 6550 additions and 1106 deletions
@@ -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));
+16 -2
View File
@@ -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
+7 -6
View File
@@ -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));
}
}
}
+1
View File
@@ -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"));
}
}
}
@@ -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;
}
}
}
+52 -44
View File
@@ -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;
+12 -1
View File
@@ -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);
+15 -3
View File
@@ -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 };
}
}
}
}
+74 -42
View File
@@ -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>
+7 -1
View File
@@ -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();
});
+39 -25
View File
@@ -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
View File
@@ -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.
+2 -3
View File
@@ -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))]
+1 -1
View File
@@ -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 = "+";
+18 -4
View File
@@ -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
{
+13 -9
View File
@@ -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);
+2 -3
View File
@@ -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,
+30
View File
@@ -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;
}
}
}
}
+37 -1
View File
@@ -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!");
+4 -4
View File
@@ -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));
+91 -48
View File
@@ -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!");
}
}
+25 -3
View File
@@ -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),
};
}
+40 -32
View File
@@ -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();
+292
View File
@@ -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