同步更新,整理资源

This commit is contained in:
LA
2025-05-02 16:01:38 +08:00
parent b24d1b5eff
commit 854c7b75dd
74 changed files with 2545 additions and 1643 deletions

View File

@@ -21,3 +21,7 @@ M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberiz
M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.

View File

@@ -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

View File

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

View File

@@ -297,34 +297,202 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { 3.1f, -123d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_hard_rock_test_cases =
{
// OD = 5 test cases.
// This leads to "effective" OD of 7.
// PERFECT hit window is [-11ms, 11ms]
// GREAT hit window is [-35ms, 35ms]
// GOOD hit window is [-58ms, 58ms]
// OK hit window is [-80ms, 80ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-97ms, ----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -10d, HitResult.Perfect },
new object[] { 5f, -11d, HitResult.Perfect },
new object[] { 5f, -12d, HitResult.Great },
new object[] { 5f, -13d, HitResult.Great },
new object[] { 5f, -34d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Great },
new object[] { 5f, -36d, HitResult.Good },
new object[] { 5f, -37d, HitResult.Good },
new object[] { 5f, -57d, HitResult.Good },
new object[] { 5f, -58d, HitResult.Good },
new object[] { 5f, -59d, HitResult.Ok },
new object[] { 5f, -60d, HitResult.Ok },
new object[] { 5f, -79d, HitResult.Ok },
new object[] { 5f, -80d, HitResult.Ok },
new object[] { 5f, -81d, HitResult.Meh },
new object[] { 5f, -82d, HitResult.Meh },
new object[] { 5f, -96d, HitResult.Meh },
new object[] { 5f, -97d, HitResult.Meh },
new object[] { 5f, -98d, HitResult.Miss },
new object[] { 5f, -99d, HitResult.Miss },
new object[] { 5f, 79d, HitResult.Ok },
new object[] { 5f, 80d, HitResult.Miss },
new object[] { 5f, 81d, HitResult.Miss },
new object[] { 5f, 82d, HitResult.Miss },
new object[] { 5f, 96d, HitResult.Miss },
new object[] { 5f, 97d, HitResult.Miss },
new object[] { 5f, 98d, HitResult.Miss },
new object[] { 5f, 99d, HitResult.Miss },
// OD = 9.3 test cases.
// This leads to "effective" OD of 13.02.
// Note that contrary to other rulesets this does NOT cap out to OD 10!
// PERFECT hit window is [-11ms, 11ms]
// GREAT hit window is [-25ms, 25ms]
// GOOD hit window is [-49ms, 49ms]
// OK hit window is [-70ms, 70ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-87ms, ----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 9.3f, 10d, HitResult.Perfect },
new object[] { 9.3f, 11d, HitResult.Perfect },
new object[] { 9.3f, 12d, HitResult.Great },
new object[] { 9.3f, 13d, HitResult.Great },
new object[] { 9.3f, 24d, HitResult.Great },
new object[] { 9.3f, 25d, HitResult.Great },
new object[] { 9.3f, 26d, HitResult.Good },
new object[] { 9.3f, 27d, HitResult.Good },
new object[] { 9.3f, 48d, HitResult.Good },
new object[] { 9.3f, 49d, HitResult.Good },
new object[] { 9.3f, 50d, HitResult.Ok },
new object[] { 9.3f, 51d, HitResult.Ok },
new object[] { 9.3f, 69d, HitResult.Ok },
new object[] { 9.3f, 70d, HitResult.Miss },
new object[] { 9.3f, 71d, HitResult.Miss },
new object[] { 9.3f, 72d, HitResult.Miss },
new object[] { 9.3f, 86d, HitResult.Miss },
new object[] { 9.3f, 87d, HitResult.Miss },
new object[] { 9.3f, 88d, HitResult.Miss },
new object[] { 9.3f, 89d, HitResult.Miss },
new object[] { 9.3f, -69d, HitResult.Ok },
new object[] { 9.3f, -70d, HitResult.Ok },
new object[] { 9.3f, -71d, HitResult.Meh },
new object[] { 9.3f, -72d, HitResult.Meh },
new object[] { 9.3f, -86d, HitResult.Meh },
new object[] { 9.3f, -87d, HitResult.Meh },
new object[] { 9.3f, -88d, HitResult.Miss },
new object[] { 9.3f, -89d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_easy_test_cases =
{
// Assume OD = 5 (other values are not tested, even OD 5 is enough to exercise the flooring logic).
// PERFECT hit window is [ -22ms, 22ms]
// GREAT hit window is [ -68ms, 68ms]
// GOOD hit window is [-114ms, 114ms]
// OK hit window is [-156ms, 156ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-190ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -21d, HitResult.Perfect },
new object[] { 5f, -22d, HitResult.Perfect },
new object[] { 5f, -23d, HitResult.Great },
new object[] { 5f, -24d, HitResult.Great },
new object[] { 5f, -67d, HitResult.Great },
new object[] { 5f, -68d, HitResult.Great },
new object[] { 5f, -69d, HitResult.Good },
new object[] { 5f, -70d, HitResult.Good },
new object[] { 5f, -113d, HitResult.Good },
new object[] { 5f, -114d, HitResult.Good },
new object[] { 5f, -115d, HitResult.Ok },
new object[] { 5f, -116d, HitResult.Ok },
new object[] { 5f, -155d, HitResult.Ok },
new object[] { 5f, -156d, HitResult.Ok },
new object[] { 5f, -157d, HitResult.Meh },
new object[] { 5f, -158d, HitResult.Meh },
new object[] { 5f, -189d, HitResult.Meh },
new object[] { 5f, -190d, HitResult.Meh },
new object[] { 5f, -191d, HitResult.Miss },
new object[] { 5f, -192d, HitResult.Miss },
new object[] { 5f, 155d, HitResult.Ok },
new object[] { 5f, 156d, HitResult.Miss },
new object[] { 5f, 157d, HitResult.Miss },
new object[] { 5f, 158d, HitResult.Miss },
new object[] { 5f, 189d, HitResult.Miss },
new object[] { 5f, 190d, HitResult.Miss },
new object[] { 5f, 191d, HitResult.Miss },
new object[] { 5f, 192d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_double_time_test_cases =
{
// Assume OD = 5 (other values are not tested, even OD 5 is enough to exercise the flooring logic).
// PERFECT hit window is [ -24ms, 24ms]
// GREAT hit window is [ -73ms, 73ms]
// GOOD hit window is [-123ms, 123ms]
// OK hit window is [-168ms, 168ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-204ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -23d, HitResult.Perfect },
new object[] { 5f, -24d, HitResult.Perfect },
new object[] { 5f, -25d, HitResult.Great },
new object[] { 5f, -26d, HitResult.Great },
new object[] { 5f, -72d, HitResult.Great },
new object[] { 5f, -73d, HitResult.Great },
new object[] { 5f, -74d, HitResult.Good },
new object[] { 5f, -75d, HitResult.Good },
new object[] { 5f, -122d, HitResult.Good },
new object[] { 5f, -123d, HitResult.Good },
new object[] { 5f, -124d, HitResult.Ok },
new object[] { 5f, -125d, HitResult.Ok },
new object[] { 5f, -167d, HitResult.Ok },
new object[] { 5f, -168d, HitResult.Ok },
new object[] { 5f, -169d, HitResult.Meh },
new object[] { 5f, -170d, HitResult.Meh },
new object[] { 5f, -203d, HitResult.Meh },
new object[] { 5f, -204d, HitResult.Meh },
new object[] { 5f, -205d, HitResult.Miss },
new object[] { 5f, -206d, HitResult.Miss },
new object[] { 5f, 167d, HitResult.Ok },
new object[] { 5f, 168d, HitResult.Miss },
new object[] { 5f, 169d, HitResult.Miss },
new object[] { 5f, 170d, HitResult.Miss },
new object[] { 5f, 203d, HitResult.Miss },
new object[] { 5f, 204d, HitResult.Miss },
new object[] { 5f, 205d, HitResult.Miss },
new object[] { 5f, 206d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_half_time_test_cases =
{
// Assume OD = 5 (other values are not tested, even OD 5 is enough to exercise the flooring logic).
// PERFECT hit window is [ -12ms, 12ms]
// GREAT hit window is [ -36ms, 36ms]
// GOOD hit window is [ -61ms, 61ms]
// OK hit window is [ -84ms, 84ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-102ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -11d, HitResult.Perfect },
new object[] { 5f, -12d, HitResult.Perfect },
new object[] { 5f, -13d, HitResult.Great },
new object[] { 5f, -14d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Great },
new object[] { 5f, -36d, HitResult.Great },
new object[] { 5f, -37d, HitResult.Good },
new object[] { 5f, -38d, HitResult.Good },
new object[] { 5f, -60d, HitResult.Good },
new object[] { 5f, -61d, HitResult.Good },
new object[] { 5f, -62d, HitResult.Ok },
new object[] { 5f, -63d, HitResult.Ok },
new object[] { 5f, -83d, HitResult.Ok },
new object[] { 5f, -84d, HitResult.Ok },
new object[] { 5f, -85d, HitResult.Meh },
new object[] { 5f, -86d, HitResult.Meh },
new object[] { 5f, -101d, HitResult.Meh },
new object[] { 5f, -102d, HitResult.Meh },
new object[] { 5f, -103d, HitResult.Miss },
new object[] { 5f, -104d, HitResult.Miss },
new object[] { 5f, 83d, HitResult.Ok },
new object[] { 5f, 84d, HitResult.Miss },
new object[] { 5f, 85d, HitResult.Miss },
new object[] { 5f, 86d, HitResult.Miss },
new object[] { 5f, 101d, HitResult.Miss },
new object[] { 5f, 102d, HitResult.Miss },
new object[] { 5f, 103d, HitResult.Miss },
new object[] { 5f, 104d, HitResult.Miss },
};
private const double note_time = 300;
[TestCaseSource(nameof(score_v2_test_cases))]
public void TestHitWindowTreatmentWithScoreV2(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 300;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note
{
StartTime = note_time,
Column = 0,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
CircleSize = 1,
},
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var beatmap = createNonConvertBeatmap(overallDifficulty);
var replay = new Replay
{
@@ -352,31 +520,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCaseSource(nameof(score_v1_non_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 300;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note
{
StartTime = note_time,
Column = 0,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
CircleSize = 1,
},
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var beatmap = createNonConvertBeatmap(overallDifficulty);
var replay = new Replay
{
@@ -403,29 +547,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCaseSource(nameof(score_v1_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 300;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new Beatmap
{
HitObjects =
{
new FakeCircle
{
StartTime = note_time,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
},
BeatmapInfo =
{
Ruleset = new RulesetInfo { OnlineID = 0 }
},
ControlPointInfo = cpi,
};
var beatmap = createConvertBeatmap(overallDifficulty);
var replay = new Replay
{
@@ -450,6 +572,172 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(score_v1_non_convert_hard_rock_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndHardRockNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
var beatmap = createNonConvertBeatmap(overallDifficulty);
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ManiaModHardRock()],
}
};
RunTest($@"SV1+HR single note @ OD{overallDifficulty}", beatmap, $@"SV1+HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(score_v1_non_convert_easy_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndEasyNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
var beatmap = createNonConvertBeatmap(overallDifficulty);
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ManiaModEasy()],
}
};
RunTest($@"SV1+EZ single note @ OD{overallDifficulty}", beatmap, $@"SV1+EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(score_v1_non_convert_double_time_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndDoubleTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
var beatmap = createNonConvertBeatmap(overallDifficulty);
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ManiaModDoubleTime()],
}
};
RunTest($@"SV1+DT single note @ OD{overallDifficulty}", beatmap, $@"SV1+DT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(score_v1_non_convert_half_time_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndHalfTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
var beatmap = createNonConvertBeatmap(overallDifficulty);
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ManiaModHalfTime()],
}
};
RunTest($@"SV1+HT single note @ OD{overallDifficulty}", beatmap, $@"SV1+HT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
private static ManiaBeatmap createNonConvertBeatmap(float overallDifficulty)
{
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note
{
StartTime = note_time,
Column = 0,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
CircleSize = 1,
},
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
return beatmap;
}
private static Beatmap createConvertBeatmap(float overallDifficulty)
{
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new Beatmap
{
HitObjects =
{
new FakeCircle
{
StartTime = note_time,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
},
BeatmapInfo =
{
Ruleset = new RulesetInfo { OnlineID = 0 }
},
ControlPointInfo = cpi,
};
return beatmap;
}
private class FakeCircle : HitObject, IHasPosition
{
public float X

View File

@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania
},
new SettingsEnumDropdown<ManiaScrollingStyle>
{
LabelText = RulesetSettingsStrings.ScrollingStyle,
LabelText = "Scrolling style",
Current = config.GetBindable<ManiaScrollingStyle>(ManiaRulesetSetting.ScrollStyle)
},
new SettingsSlider<double, ManiaScrollSlider>
@@ -65,13 +65,13 @@ namespace osu.Game.Rulesets.Mania
},
new SettingsSlider<double, ManiaScrollBaseSpeedSlider>
{
LabelText = RulesetSettingsStrings.ScrollBaseSpeed,
LabelText = "Scroll Base MS (when 200 Speed)",
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollBaseSpeed),
KeyboardStep = 1,
},
new SettingsSlider<double, ManiaScrollTimePerSpeedSlider>
{
LabelText = RulesetSettingsStrings.ScrollTimePrecision,
LabelText = "MS / Scroll Speed",
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTimePerSpeed),
KeyboardStep = 1,
},

View File

@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
public partial class EzComComboCounter : ComboCounter
{
[SettingSource("Font", "Font", SettingControlType = typeof(OffsetNumberNameSelector))]
public Bindable<string> NameDropdown { get; } = new Bindable<string>("EZ2DJ-4th");
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
[SettingSource("Effect Type", "Effect Type")]
public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);

View File

@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
public partial class EzComComboSprite : HitErrorMeter
{
[SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(OffsetNumberNameSelector))]
public Bindable<string> NameDropdown { get; } = new Bindable<string>("EZ2DJ-4th");
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
[SettingSource("Effect Type", "Effect Type")]
public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);

View File

@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2f),
Scale = new Vector2(1.5f),
// Y = -20,
Alpha = 0, // 初始隐藏
Texture = textures.Get("Gameplay/AllCombo/ALL-COMBO2"), // 替换为你的贴图路径

View File

@@ -18,10 +18,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
public partial class EzComHitTiming : HitErrorMeter
{
[SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(OffsetNumberNameSelector))]
public Bindable<string> NumberNameDropdown { get; } = new Bindable<string>("Tomato");
public Bindable<OffsetNumberName> NumberNameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.Tomato);
[SettingSource("Offset Text Font", "Offset Text Font", SettingControlType = typeof(OffsetTextNameSelector))]
public Bindable<string> TextNameDropdown { get; } = new Bindable<string>("Tomato");
public Bindable<OffsetNumberName> TextNameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.Tomato);
[SettingSource("AloneShow", "Show only Early or: Late separately")]
public Bindable<AloneShowMenu> AloneShow { get; } = new Bindable<AloneShowMenu>(AloneShowMenu.None);
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
public BindableNumber<double> DisplayDuration { get; } = new BindableNumber<double>(300)
{
MinValue = 10,
MaxValue = 1000, // 最大持续时间
MaxValue = 10000, // 最大持续时间
Precision = 1, // 精度
};
@@ -101,6 +101,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
// Spacing = new Vector2(SymmetryOffset.Value),
Children = new Drawable[]
{
@@ -133,6 +134,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1.2f),
Text = "±000",
},
}

View File

@@ -4,14 +4,17 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osuTK;
using osuTK.Graphics;
using EffectType = osu.Game.Skinning.Components.EffectType;
namespace osu.Game.Rulesets.Mania.Skinning.SbI
{
@@ -38,6 +41,29 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var hitTiming = container.ChildrenOfType<EzComHitTiming>().ToArray();
if (hitTiming.Length >= 2)
{
var hitTiming1 = hitTiming[0];
var hitTiming2 = hitTiming[1];
const float mirror_x = 350;
hitTiming1.Anchor = Anchor.Centre;
hitTiming1.Origin = Anchor.Centre;
hitTiming1.DisplayDuration.Value = hitTiming1.DisplayDuration.MinValue;
hitTiming1.X = -mirror_x;
// hitTiming1.Scale = new Vector2(2);
hitTiming1.AloneShow.Value = AloneShowMenu.Early;
hitTiming2.Anchor = Anchor.Centre;
hitTiming2.Origin = Anchor.Centre;
hitTiming2.DisplayDuration.Value = hitTiming2.DisplayDuration.MinValue;
hitTiming2.X = mirror_x;
// hitTiming2.Scale = new Vector2(2);
hitTiming2.AloneShow.Value = AloneShowMenu.Late;
}
var combo1 = container.OfType<EzComComboCounter>().FirstOrDefault();
if (combo1 != null)
@@ -45,25 +71,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
combo1.Anchor = Anchor.TopCentre;
combo1.Origin = Anchor.Centre;
combo1.Y = 200;
combo1.Alpha = 1f;
combo1.Current.BindValueChanged(changedEvent =>
{
bool wasIncrease = changedEvent.NewValue > changedEvent.OldValue;
bool wasMiss = changedEvent.OldValue > 1 && changedEvent.NewValue == 0;
if (wasIncrease)
{
combo1.Text
.ScaleTo(new Vector2(1.5f, 1.5f), 10, Easing.OutQuint)
.Then()
.ScaleTo(Vector2.One, 500, Easing.OutQuint);
}
if (wasMiss)
{
combo1.Text.FlashColour(Color4.Red, 500, Easing.OutQuint);
}
});
combo1.Effect.Value = EffectType.None;
combo1.NameDropdown.Value = OffsetNumberName.EZ2AC_EVOLVE;
}
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
@@ -76,13 +85,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
hitErrorMeter.Position = new Vector2(0, -15);
hitErrorMeter.Scale = new Vector2(1.4f, 1.4f);
hitErrorMeter.JudgementLineThickness.Value = 2;
hitErrorMeter.ShowMovingAverage.Value = true;
hitErrorMeter.JudgementFadeOutDuration.Value = hitErrorMeter.JudgementFadeOutDuration.MinValue;
hitErrorMeter.ShowMovingAverage.Value = false;
hitErrorMeter.ColourBarVisibility.Value = false;
hitErrorMeter.CentreMarkerStyle.Value = BarHitErrorMeter.CentreMarkerStyles.Circle;
hitErrorMeter.CentreMarkerStyle.Value = BarHitErrorMeter.CentreMarkerStyles.Line;
hitErrorMeter.LabelStyle.Value = BarHitErrorMeter.LabelStyles.None;
}
})
{
new EzComHitTiming(),
new EzComHitTiming(),
new EzComComboCounter(),
new BarHitErrorMeter(),
};
@@ -90,11 +102,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
return null;
case SkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult>:
// if (Skin is SbISkin && resultComponent.Component >= HitResult.Great)
// return Drawable.Empty();
// return new EzComJudgementTexture(resultComponent.Component);
return new SbIJudgementPiece(resultComponent.Component);
// return new SbIJudgementPiece(resultComponent.Component);
return Drawable.Empty();
case ManiaSkinComponentLookup maniaComponent:
switch (maniaComponent.Component)

View File

@@ -3,7 +3,9 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
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.Input.Bindings;
@@ -20,14 +22,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
// private Box background = null!;
private Color4 brightColour;
private Color4 dimColour;
private Box backgroundOverlay = null!;
// private Box background = null!;
// private Box? separator;
[Resolved]
private Column column { get; set; } = null!;
// private Bindable<Color4> accentColour = null!;
// private readonly Bindable<float> overlayHeight = new Bindable<float>(0f);
private readonly Bindable<float> overlayHeight = new Bindable<float>(0f);
public SbIColumnBackground()
{
@@ -46,19 +52,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
Name = "Background",
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 1,
},
backgroundOverlay = new Box
{
Name = "Background Gradient Overlay",
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Height = 0.1f,
Blending = BlendingParameters.Additive,
Alpha = 1,
Colour = Color4.Black,
Alpha = 0,
Colour = Color4.White,
},
};
// overlayHeight.BindValueChanged(height => backgroundOverlay.Height = height.NewValue, true);
overlayHeight.BindValueChanged(height => backgroundOverlay.Height = height.NewValue, true);
// accentColour.BindValueChanged(colour =>
// {
// var newColour = colour.NewValue.Darken(3);
@@ -92,20 +99,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
public bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
{
// if (e.Action == column.Action.Value)
// {
// var noteColour = column.AccentColour.Value;
// brightColour = noteColour.Opacity(1f);
// dimColour = noteColour.Opacity(0);
//
// backgroundOverlay.Colour = direction.Value == ScrollingDirection.Up
// ? ColourInfo.GradientVertical(brightColour, dimColour)
// : ColourInfo.GradientVertical(dimColour, brightColour);
//
// overlayHeight.Value = 0.75f;
//
// backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
// }
if (e.Action == column.Action.Value)
{
var noteColour = column.AccentColour.Value;
brightColour = noteColour.Opacity(1f);
dimColour = noteColour.Opacity(0);
backgroundOverlay.Colour = direction.Value == ScrollingDirection.Up
? ColourInfo.GradientVertical(brightColour, dimColour)
: ColourInfo.GradientVertical(dimColour, brightColour);
overlayHeight.Value = 0.1f;
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
}
return false;
}

View File

@@ -2,24 +2,28 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.SbI
{
public partial class SbIHoldBodyPiece : CompositeDrawable, IHoldNoteBody
{
// private readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
// private Drawable background = null!;
public SbIHoldBodyPiece()
{
RelativeSizeAxes = Axes.Both;
// Without this, the width of the body will be slightly larger than the head/tail.
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Masking = true;
CornerRadius = 0;
}
@@ -27,23 +31,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
[BackgroundDependencyLoader(true)]
private void load(DrawableHitObject? drawableObject)
{
// InternalChildren = new[]
// {
// background = new Box
// {
// RelativeSizeAxes = Axes.Both,
// Colour = Colour4.White,
// },
// };
InternalChildren = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White,
},
};
// if (drawableObject != null)
// {
// var holdNote = (DrawableHoldNote)drawableObject;
//
// // AccentColour.BindTo(holdNote.AccentColour);
// // hittingLayer.AccentColour.BindTo(holdNote.AccentColour);
// // ((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNote.IsHitting);
// }
if (drawableObject != null)
{
var holdNote = (DrawableHoldNote)drawableObject;
accentColour.BindTo(holdNote.AccentColour);
// hittingLayer.AccentColour.BindTo(holdNote.AccentColour);
// ((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNote.IsHitting);
}
// AccentColour.BindValueChanged(colour =>
// {

View File

@@ -12,10 +12,10 @@ namespace osu.Game.Rulesets.Mania.UI
ScrollSpeedStyle,
// [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionDown))]
[Description("ms值 通配速度风格")]
[Description("ms值 恒定速度")]
ScrollTimeStyle,
[Description("ms值 恒定反应时间")]
[Description("ms值 恒定时间")]
ScrollTimeStyleFixed,
}
}

View File

@@ -15,8 +15,4 @@
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Skinning\Ez2\Ez2HUD\EzComComboSprite.txt" />
</ItemGroup>
</Project>

View File

@@ -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

View File

@@ -7,6 +7,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override Ruleset CreateRuleset() => new TaikoRuleset();
private static readonly object[][] test_cases =
private static readonly object[][] no_mod_test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
@@ -52,30 +53,58 @@ namespace osu.Game.Rulesets.Taiko.Tests
new object[] { 7.8f, -64d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
private static readonly object[][] hard_rock_test_cases =
{
// OD = 5 test cases.
// This leads to "effective" OD of 7.
// GREAT hit window is (-29ms, 29ms)
// OK hit window is (-68ms, 68ms)
new object[] { 5f, -27d, HitResult.Great },
new object[] { 5f, -28d, HitResult.Great },
new object[] { 5f, -29d, HitResult.Ok },
new object[] { 5f, -30d, HitResult.Ok },
new object[] { 5f, -66d, HitResult.Ok },
new object[] { 5f, -67d, HitResult.Ok },
new object[] { 5f, -68d, HitResult.Miss },
new object[] { 5f, -69d, HitResult.Miss },
// OD = 7.8 test cases.
// This would lead to "effective" OD of 10.92,
// but the effects are capped to OD 10.
// GREAT hit window is (-20ms, 20ms)
// OK hit window is (-50ms, 50ms)
new object[] { 7.8f, -18d, HitResult.Great },
new object[] { 7.8f, -19d, HitResult.Great },
new object[] { 7.8f, -20d, HitResult.Ok },
new object[] { 7.8f, -21d, HitResult.Ok },
new object[] { 7.8f, -48d, HitResult.Ok },
new object[] { 7.8f, -49d, HitResult.Ok },
new object[] { 7.8f, -50d, HitResult.Miss },
new object[] { 7.8f, -51d, HitResult.Miss },
};
private static readonly object[][] easy_test_cases =
{
// OD = 5 test cases.
// This leads to "effective" OD of 2.5.
// GREAT hit window is ( -42ms, 42ms)
// OK hit window is (-100ms, 100ms)
new object[] { 5f, -40d, HitResult.Great },
new object[] { 5f, -41d, HitResult.Great },
new object[] { 5f, -42d, HitResult.Ok },
new object[] { 5f, -43d, HitResult.Ok },
new object[] { 5f, -98d, HitResult.Ok },
new object[] { 5f, -99d, HitResult.Ok },
new object[] { 5f, -100d, HitResult.Miss },
new object[] { 5f, -101d, HitResult.Miss },
};
private const double hit_time = 100;
[TestCaseSource(nameof(no_mod_test_cases))]
public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double hit_time = 100;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit
{
StartTime = hit_time,
Type = HitType.Centre,
}
},
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
BeatmapInfo =
{
Ruleset = new TaikoRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var beatmap = createBeatmap(overallDifficulty);
var replay = new Replay
{
@@ -98,5 +127,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
RunTest($@"single hit @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(hard_rock_test_cases))]
public void TestHitWindowTreatmentWithHardRock(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
var beatmap = createBeatmap(overallDifficulty);
var replay = new Replay
{
Frames =
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new TaikoModHardRock()]
}
};
RunTest($@"HR single hit @ OD{overallDifficulty}", beatmap, $@"HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(easy_test_cases))]
public void TestHitWindowTreatmentWithEasy(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
var beatmap = createBeatmap(overallDifficulty);
var replay = new Replay
{
Frames =
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new TaikoModHardRock()]
}
};
RunTest($@"EZ single hit @ OD{overallDifficulty}", beatmap, $@"EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
private static TaikoBeatmap createBeatmap(float overallDifficulty)
{
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit
{
StartTime = hit_time,
Type = HitType.Centre,
}
},
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
BeatmapInfo =
{
Ruleset = new TaikoRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
return beatmap;
}
}
}

View File

@@ -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

View File

@@ -77,33 +77,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
}
[Test]
public void TestPlayerScore()
{
createLeaderboard();
addLocalPlayer();
var player2Score = new BindableLong(1234567);
var player3Score = new BindableLong(1111111);
AddStep("add player 2", () => createLeaderboardScore(player2Score, new APIUser { Username = "Player 2" }));
AddStep("add player 3", () => createLeaderboardScore(player3Score, new APIUser { Username = "Player 3" }));
AddUntilStep("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
AddUntilStep("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500);
AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
AddUntilStep("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2));
AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456);
AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
AddUntilStep("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2));
AddUntilStep("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
}
[Test]
public void TestRandomScores()
{
@@ -183,30 +156,6 @@ namespace osu.Game.Tests.Visual.Gameplay
() => Does.Contain("#FF549A"));
}
[Test]
public void TestTrackedScorePosition([Values] bool partial)
{
createLeaderboard(partial);
AddStep("add many scores in one go", () =>
{
for (int i = 0; i < 49; i++)
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
// Add player at end to force an animation down the whole list.
playerScore.Value = 0;
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
});
if (partial)
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
else
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000);
AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null));
}
private void addLocalPlayer()
{
AddStep("add local player", () =>
@@ -216,12 +165,11 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
private void createLeaderboard(bool partial = false)
private void createLeaderboard()
{
AddStep("create leaderboard", () =>
{
leaderboardProvider.Scores.Clear();
leaderboardProvider.IsPartial = partial;
Child = leaderboard = new TestDrawableGameplayLeaderboard
{
Anchor = Anchor.Centre,
@@ -243,24 +191,14 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public float Spacing => Flow.Spacing.Y;
public bool CheckPositionByUsername(string username, int? expectedPosition)
{
var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username);
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
}
public IEnumerable<DrawableGameplayLeaderboardScore> GetAllScoresForUsername(string username)
=> Flow.Where(i => i.User?.Username == username);
public IEnumerable<DrawableGameplayLeaderboardScore> AllScores => Flow;
}
private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
{
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
public bool IsPartial { get; set; }
}
}
}

View File

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

View File

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

View File

@@ -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;

View File

@@ -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 System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -10,6 +11,9 @@ 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.Osu;
@@ -26,6 +30,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,6 +42,24 @@ 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
@@ -115,11 +139,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);
@@ -155,5 +213,29 @@ 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);
}
}
}

View File

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

View File

@@ -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,
},
}
}

View File

@@ -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,
FilterText = "12345 matches",
},
}
}
},
};
}
}

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface
Scheduler.AddOnce(resolvePlayback);
}
private void playClickSample() => sampleClick?.Play();
private void playClickSample() => sampleClick.Play();
private void resolvePlayback()
{

View File

@@ -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[]
{

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

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -109,15 +109,6 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString LandscapeTouchOverlay => new TranslatableString(getKey(@"landscape_touch_overlay"), @"Landscape (touch overlay)");
/// <summary>
/// "Scrolling style"
/// </summary>
public static LocalisableString ScrollingStyle => new TranslatableString(getKey(@"scrolling_style"), @"Scrolling style");
public static LocalisableString ScrollBaseSpeed => new TranslatableString(getKey(@"scroll_base_speed"), @"Scroll Base MS (when 200 Speed)");
public static LocalisableString ScrollTimePrecision => new TranslatableString(getKey(@"scrolling_time_precision"), @"Scroll MS per 1 Speed");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -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>

View File

@@ -125,7 +125,14 @@ namespace osu.Game.Online.Leaderboards
var result = LeaderboardScores.Success
(
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(),
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap))
.OrderByTotalScore()
.Select((s, idx) =>
{
s.Position = idx + 1;
return s;
})
.ToArray(),
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
);
inFlightOnlineRequest = null;

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 = "+";

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

View File

@@ -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
{

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,

View File

@@ -136,7 +136,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
LabelText = "Scaling To Game Mode",
Current = osuConfig.GetBindable<ScalingGameMode>(OsuSetting.ScalingGameMode),
Keywords = new[] { "scale", "letterbox" },
},
scalingSettings = new FillFlowContainer<SettingsSlider<float>>
{

View File

@@ -31,6 +31,9 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; }
[Resolved]
private EzSkinEditorOverlay? ezSkinEditorOverlay { get; set; }
public SkinEditorSceneLibrary()
{
Height = HEIGHT;
@@ -88,6 +91,17 @@ namespace osu.Game.Overlays.SkinEditor
Origin = Anchor.CentreLeft,
Action = () => skinEditorOverlay?.PresentGameplay(),
},
new SceneButton
{
Text = "Skin.ini(Null)",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Action = () =>
{
skinEditorOverlay?.PresentGameplay();
ezSkinEditorOverlay?.PopulateSettings();
},
},
}
},
}

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,

View File

@@ -6,9 +6,9 @@ using System.Collections.Generic;
using System.IO;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.IO.FileAbstraction;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Storyboards;
using osu.Game.Utils;
using TagLib;
using File = TagLib.File;
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Edit.Checks
{
// We use TagLib here for platform invariance; BASS cannot detect audio presence on Linux.
using (Stream data = context.WorkingBeatmap.GetStream(storagePath))
using (File tagFile = File.Create(new StreamFileAbstraction(filename, data)))
using (File tagFile = TagLibUtils.GetTagLibFile(filename, data))
{
if (tagFile.Properties.AudioChannels == 0)
continue;

View File

@@ -6,9 +6,9 @@ using System.Collections.Generic;
using System.IO;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.IO.FileAbstraction;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Storyboards;
using osu.Game.Utils;
using TagLib;
using File = TagLib.File;
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Edit.Checks
try
{
using (Stream data = context.WorkingBeatmap.GetStream(storagePath))
using (File tagFile = File.Create(new StreamFileAbstraction(filename, data)))
using (File tagFile = TagLibUtils.GetTagLibFile(filename, data))
{
int height = tagFile.Properties.VideoHeight;
int width = tagFile.Properties.VideoWidth;

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

View File

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

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

View File

@@ -25,8 +25,12 @@ namespace osu.Game.Screens.Backgrounds
var video = new Video(videoPath)
{
RelativeSizeAxes = Axes.Both,
Loop = true
Loop = true,
};
video.FillMode = FillMode.Fill;
video.FillAspectRatio = 1.0f * video.DrawSize.X / video.DrawSize.Y;
AddInternal(video);
GlobalConfigStore.Config = config;

View File

@@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Setup
private FormTextBox sourceTextBox = null!;
private FormTextBox tagsTextBox = null!;
private bool reloading;
private bool dirty;
public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
@@ -105,6 +106,8 @@ namespace osu.Game.Screens.Edit.Setup
private void reloadMetadata()
{
reloading = true;
var metadata = Beatmap.Metadata;
RomanisedArtistTextBox.ReadOnly = false;
@@ -120,10 +123,15 @@ namespace osu.Game.Screens.Edit.Setup
tagsTextBox.Current.Value = metadata.Tags;
updateReadOnlyState();
reloading = false;
}
private void applyMetadata()
{
if (reloading)
return;
Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value;
Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value;
Beatmap.Metadata.TitleUnicode = TitleTextBox.Current.Value;

View File

@@ -10,9 +10,9 @@ using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Localisation;
using osu.Game.Models;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Utils;
@@ -98,11 +98,16 @@ namespace osu.Game.Screens.Edit.Setup
if (!source.Exists)
return false;
TagLib.File? tagSource;
string artist;
string title;
try
{
tagSource = TagLib.File.Create(source.FullName);
using (var tagSource = TagLibUtils.GetTagLibFile(source.FullName))
{
artist = tagSource.Tag.JoinedAlbumArtists ?? tagSource.Tag.JoinedPerformers;
title = tagSource.Tag.Title;
}
}
catch (Exception e)
{
@@ -116,16 +121,12 @@ namespace osu.Game.Screens.Edit.Setup
{
metadata.AudioFile = name;
string artist = tagSource.Tag.JoinedAlbumArtists;
if (!string.IsNullOrWhiteSpace(artist))
{
metadata.ArtistUnicode = artist;
metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
}
string title = tagSource.Tag.Title;
if (!string.IsNullOrEmpty(title))
{
metadata.TitleUnicode = title;

View File

@@ -0,0 +1,389 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Screens
{
/// <summary>
/// A container which handles loading a skin editor on user request for a specified target.
/// This also handles the scaling / positioning adjustment of the target.
/// </summary>
public partial class EzSkinEditorOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
{
private readonly ScalingContainer scalingContainer;
protected override bool BlockNonPositionalInput => true;
private SkinEditor? skinEditor;
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[Resolved]
private OsuGame game { get; set; } = null!;
[Resolved]
private MusicController music { get; set; } = null!;
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private OsuScreen? lastTargetScreen;
private InvokeOnDisposal? nestedInputManagerDisable;
private readonly LayoutValue drawSizeLayout;
public EzSkinEditorOverlay(ScalingContainer scalingContainer)
{
this.scalingContainer = scalingContainer;
RelativeSizeAxes = Axes.Both;
AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize));
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Back:
if (skinEditor?.State.Value != Visibility.Visible)
break;
Hide();
return true;
}
return false;
}
protected override void PopIn()
{
globallyDisableBeatmapSkinSetting();
if (skinEditor != null)
{
disableNestedInputManagers();
skinEditor.Show();
if (lastTargetScreen is MainMenu)
PresentGameplay();
PopulateSettings();
return;
}
var editor = new SkinEditor();
editor.State.BindValueChanged(_ => updateComponentVisibility());
skinEditor = editor;
LoadComponentAsync(editor, _ =>
{
if (editor != skinEditor)
return;
AddInternal(editor);
if (lastTargetScreen is MainMenu)
PresentGameplay();
Debug.Assert(lastTargetScreen != null);
SetTarget(lastTargetScreen);
PopulateSettings();
});
}
protected override void PopOut()
{
skinEditor?.Save(false);
skinEditor?.Hide();
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
globallyReenableBeatmapSkinSetting();
}
private EditorSidebar settingsSidebar = null!;
private EzSkinSettings ezSkinSettings = null!;
public void PopulateSettings()
{
settingsSidebar.Clear();
settingsSidebar.Add(new SkinSettingsToolbox(ezSkinSettings));
}
public void PresentGameplay() => presentGameplay(false);
private void presentGameplay(bool attemptedBeatmapSwitch)
{
performer?.PerformFromScreen(screen =>
{
if (State.Value != Visibility.Visible)
return;
if (beatmap.Value is DummyWorkingBeatmap)
{
// presume we don't have anything good to play and just bail.
return;
}
// If we're playing the intro, switch away to another beatmap.
if (beatmap.Value.BeatmapSetInfo.Protected)
{
if (!attemptedBeatmapSwitch)
{
music.NextTrack();
Schedule(() => presentGameplay(true));
}
return;
}
if (screen is Player)
return;
// the validity of the current game-wide beatmap + ruleset combination is enforced by song select.
// if we're anywhere else, the state is unknown and may not make sense, so forcibly set something that does.
if (screen is not PlaySongSelect)
ruleset.Value = beatmap.Value.BeatmapInfo.Ruleset;
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
IReadOnlyList<Mod> usableMods = mods.Value;
if (replayGeneratingMod != null)
usableMods = usableMods.Append(replayGeneratingMod).ToArray();
if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid))
mods.Value = mods.Value.Except(invalid).ToArray();
if (replayGeneratingMod != null)
screen.Push(new EndlessPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)));
}, new[] { typeof(Player), typeof(PlaySongSelect) });
}
protected override void Update()
{
base.Update();
if (!drawSizeLayout.IsValid)
{
updateScreenSizing();
drawSizeLayout.Validate();
}
}
private void updateScreenSizing()
{
if (skinEditor?.State.Value != Visibility.Visible) return;
const float padding = 10;
float relativeSidebarWidth = (EditorSidebar.WIDTH + padding) / DrawWidth;
float relativeToolbarHeight = (SkinEditorSceneLibrary.HEIGHT + SkinEditor.MENU_HEIGHT + padding) / DrawHeight;
var rect = new RectangleF(
relativeSidebarWidth,
relativeToolbarHeight,
1 - relativeSidebarWidth * 2,
1f - relativeToolbarHeight - padding / DrawHeight);
scalingContainer.SetCustomRect(rect, true);
}
private void updateComponentVisibility()
{
Debug.Assert(skinEditor != null);
if (skinEditor.State.Value == Visibility.Visible)
{
Scheduler.AddOnce(updateScreenSizing);
game.Toolbar.Hide();
game.CloseAllOverlays();
}
else
{
scalingContainer.SetCustomRect(null);
if (lastTargetScreen?.HideOverlaysOnEnter != true)
game.Toolbar.Show();
}
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
/// <summary>
/// Set a new target screen which will be used to find skinnable components.
/// </summary>
public void SetTarget(OsuScreen screen)
{
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
lastTargetScreen = screen;
if (skinEditor == null) return;
// ensure the toolbar is re-hidden even if a new screen decides to try and show it.
updateComponentVisibility();
// AddOnce with parameter will ensure the newest target is loaded if there is any overlap.
Scheduler.AddOnce(setTarget, screen);
}
private void setTarget(OsuScreen? target)
{
if (target == null)
return;
Debug.Assert(skinEditor != null);
if (!target.IsLoaded || !skinEditor.IsLoaded)
{
Scheduler.AddOnce(setTarget, target);
return;
}
if (skinEditor.State.Value == Visibility.Visible)
{
skinEditor.Save(false);
skinEditor.UpdateTargetScreen(target);
disableNestedInputManagers();
}
else
{
skinEditor.Hide();
skinEditor.Expire();
skinEditor = null;
}
}
private void disableNestedInputManagers()
{
if (lastTargetScreen == null)
return;
var nestedInputManagers = lastTargetScreen.ChildrenOfType<PassThroughInputManager>().Where(manager => manager.UseParentInput).ToArray();
foreach (var inputManager in nestedInputManagers)
inputManager.UseParentInput = false;
nestedInputManagerDisable = new InvokeOnDisposal(() =>
{
foreach (var inputManager in nestedInputManagers)
inputManager.UseParentInput = true;
});
}
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private LeasedBindable<bool>? leasedBeatmapSkins;
private void globallyDisableBeatmapSkinSetting()
{
if (beatmapSkins.Disabled)
return;
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
// To keep things simple, disable the setting game-wide while using the skin editor.
//
// This causes a full reload of the skin, which is pretty ugly.
// TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap.
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
leasedBeatmapSkins.Value = false;
}
private void globallyReenableBeatmapSkinSetting()
{
leasedBeatmapSkins?.Return();
leasedBeatmapSkins = null;
}
private partial class EndlessPlayer : ReplayPlayer
{
protected override UserActivity? InitialActivity => null;
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool? AllowGlobalTrackControl => false;
public EndlessPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore)
: base(createScore, new PlayerConfiguration
{
ShowResults = false,
AutomaticallySkipIntro = true,
})
{
}
protected override void LoadComplete()
{
base.LoadComplete();
if (!LoadedBeatmapSuccessfully)
Scheduler.AddDelayed(this.Exit, 1000);
}
protected override void Update()
{
base.Update();
if (!LoadedBeatmapSuccessfully)
return;
if (GameplayState.HasPassed)
GameplayClockContainer.Seek(0);
}
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens
{
public partial class EzSkinSettings : CompositeDrawable
{
[SettingSource("Column Width", "Column Width")]
public BindableNumber<float> ColumnWidth { get; } = new BindableNumber<float>(50f)
{
MinValue = 9f,
MaxValue = 90f,
Precision = 1f,
};
[SettingSource("Special Column Width Factor", "Special Column Width Factor")]
public BindableNumber<float> SpecialFactor { get; } = new BindableNumber<float>(1f)
{
MinValue = 0.1f,
MaxValue = 2f,
Precision = 0.1f,
};
[BackgroundDependencyLoader]
private void load(ScoreProcessor scoreProcessor)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
}
}
}

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,

View File

@@ -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,

View File

@@ -5,7 +5,6 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -20,8 +19,6 @@ namespace osu.Game.Screens.Play.HUD
{
public partial class DrawableGameplayLeaderboard : CompositeDrawable
{
private readonly Cached sorting = new Cached();
public Bindable<bool> Expanded = new Bindable<bool>();
protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;
@@ -87,7 +84,6 @@ namespace osu.Game.Screens.Play.HUD
}, true);
}
Scheduler.AddDelayed(sort, 1000, true);
configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true);
}
@@ -109,8 +105,8 @@ namespace osu.Game.Screens.Play.HUD
drawable.Expanded.BindTo(Expanded);
Flow.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort));
drawable.DisplayOrder.BindValueChanged(_ => Scheduler.AddOnce(sort), true);
int displayCount = Math.Min(Flow.Count, max_panels);
Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
@@ -179,22 +175,8 @@ namespace osu.Game.Screens.Play.HUD
private void sort()
{
if (sorting.IsValid)
return;
var orderedByScore = Flow
.OrderByDescending(i => i.TotalScore.Value)
.ThenBy(i => i.DisplayOrder.Value)
.ToList();
for (int i = 0; i < Flow.Count; i++)
{
var score = orderedByScore[i];
Flow.SetLayoutPosition(score, i);
score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1;
}
sorting.Validate();
foreach (var score in Flow.ToArray())
Flow.SetLayoutPosition(score, score.DisplayOrder.Value);
}
private partial class InputDisabledScrollContainer : OsuScrollContainer

View File

@@ -56,6 +56,7 @@ namespace osu.Game.Screens.Play.HUD
public BindableDouble Accuracy { get; } = new BindableDouble(1);
public BindableInt Combo { get; } = new BindableInt();
public BindableBool HasQuit { get; } = new BindableBool();
public Bindable<int?> ScorePosition { get; } = new Bindable<int?>();
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
private Func<ScoringMode, long>? getDisplayScoreFunction;
@@ -69,28 +70,6 @@ namespace osu.Game.Screens.Play.HUD
public Color4? TextColour { get; set; }
private int? scorePosition;
private bool scorePositionIsSet;
public int? ScorePosition
{
get => scorePosition;
set
{
// We always want to run once, as the incoming value may be null and require a visual update to "-".
if (value == scorePosition && scorePositionIsSet)
return;
scorePosition = value;
positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-";
scorePositionIsSet = true;
updateState();
}
}
public IUser? User { get; }
/// <summary>
@@ -123,6 +102,7 @@ namespace osu.Game.Screens.Play.HUD
Accuracy.BindTo(score.Accuracy);
Combo.BindTo(score.Combo);
HasQuit.BindTo(score.HasQuit);
ScorePosition.BindTo(score.Position);
DisplayOrder.BindTo(score.DisplayOrder);
GetDisplayScore = score.GetDisplayScore;
@@ -334,6 +314,7 @@ namespace osu.Game.Screens.Play.HUD
updateState();
Expanded.BindValueChanged(changeExpandedState, true);
ScorePosition.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
@@ -392,7 +373,9 @@ namespace osu.Game.Screens.Play.HUD
return;
}
if (scorePosition == 1)
positionText.Text = ScorePosition.Value.HasValue ? $"#{ScorePosition.Value.Value.FormatRank()}" : "-";
if (ScorePosition.Value == 1)
{
widthExtension = true;
panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");

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

View File

@@ -5,36 +5,22 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
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.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
namespace osu.Game.Screens.Ranking
@@ -286,180 +272,6 @@ namespace osu.Game.Screens.Ranking
protected override bool OnClick(ClickEvent e) => true;
private partial class DrawableUserTag : OsuAnimatedButton
{
public readonly UserTag UserTag;
public Action<UserTag>? OnSelected { get; set; }
private readonly Bindable<int> voteCount = new Bindable<int>();
private readonly BindableBool voted = new BindableBool();
private readonly Bindable<bool> confirmed = new BindableBool();
private readonly BindableBool updating = new BindableBool();
protected Box MainBackground { get; private set; } = null!;
private Box voteBackground = null!;
protected OsuSpriteText TagCategoryText { get; private set; } = null!;
protected OsuSpriteText TagNameText { get; private set; } = null!;
protected VoteCountText VoteCountText { get; private set; } = null!;
private readonly bool showVoteCount;
private LoadingLayer loadingLayer = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public DrawableUserTag(UserTag userTag, bool showVoteCount = true)
{
UserTag = userTag;
this.showVoteCount = showVoteCount;
voteCount.BindTo(userTag.VoteCount);
updating.BindTo(userTag.Updating);
voted.BindTo(userTag.Voted);
AutoSizeAxes = Axes.Both;
ScaleOnMouseDown = 0.95f;
}
[BackgroundDependencyLoader]
private void load()
{
CornerRadius = 5;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Colour = colours.Lime1,
Radius = 6,
Type = EdgeEffectType.Glow,
};
Content.AddRange(new Drawable[]
{
MainBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new[]
{
TagCategoryText = new OsuSpriteText
{
Alpha = UserTag.GroupName != null ? 0.6f : 0,
Text = UserTag.GroupName ?? default(LocalisableString),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Horizontal = 6 }
},
new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
Blending = BlendingParameters.Additive,
},
TagNameText = new OsuSpriteText
{
Text = UserTag.DisplayName,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Horizontal = 6, Vertical = 3, },
},
}
},
showVoteCount
? new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
voteBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
VoteCountText = new VoteCountText(voteCount)
{
Margin = new MarginPadding { Horizontal = 6 },
},
}
}
: Empty(),
}
},
loadingLayer = new LoadingLayer(dimBackground: true),
});
TooltipText = UserTag.Description;
}
protected override void LoadComplete()
{
base.LoadComplete();
const double transition_duration = 300;
updating.BindValueChanged(u => loadingLayer.State.Value = u.NewValue ? Visibility.Visible : Visibility.Hidden);
if (showVoteCount)
{
voteCount.BindValueChanged(_ =>
{
confirmed.Value = voteCount.Value >= 10;
}, true);
voted.BindValueChanged(v =>
{
if (v.NewValue)
{
voteBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint);
VoteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
}
else
{
voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint);
VoteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
}
}, true);
confirmed.BindValueChanged(c =>
{
if (c.NewValue)
{
MainBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint);
TagCategoryText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
TagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
FadeEdgeEffectTo(0.3f, transition_duration, Easing.OutQuint);
}
else
{
MainBackground.FadeColour(colours.Gray6, transition_duration, Easing.OutQuint);
TagCategoryText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
TagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint);
}
}, true);
}
FinishTransforms(true);
Action = () => OnSelected?.Invoke(UserTag);
}
}
private partial class AddNewTagUserTag : DrawableUserTag, IHasPopover
{
public BindableDictionary<long, UserTag> AvailableTags { get; } = new BindableDictionary<long, UserTag>();
@@ -493,292 +305,5 @@ namespace osu.Game.Screens.Ranking
OnSelected = OnTagSelected,
};
}
private partial class AddTagsPopover : OsuPopover
{
private SearchTextBox searchBox = null!;
private SearchContainer searchContainer = null!;
public BindableDictionary<long, UserTag> AvailableTags { get; } = new BindableDictionary<long, UserTag>();
public Action<UserTag>? OnSelected { get; set; }
private CancellationTokenSource? loadCancellationTokenSource;
[BackgroundDependencyLoader]
private void load()
{
AllowableAnchors = new[]
{
Anchor.TopCentre,
Anchor.BottomCentre,
};
Children = new Drawable[]
{
new Container
{
Size = new Vector2(400, 300),
Children = new Drawable[]
{
searchBox = new SearchTextBox
{
HoldFocus = true,
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.X,
Y = 40,
Height = 260,
ScrollbarOverlapsContent = false,
Child = searchContainer = new SearchContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 5, Bottom = 10 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
}
}
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
AvailableTags.BindCollectionChanged((_, _) =>
{
loadCancellationTokenSource?.Cancel();
loadCancellationTokenSource = new CancellationTokenSource();
LoadComponentsAsync(createItems(AvailableTags.Values), loaded =>
{
searchContainer.Clear();
searchContainer.AddRange(loaded);
}, loadCancellationTokenSource.Token);
}, true);
searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true);
}
private IEnumerable<Drawable> createItems(IEnumerable<UserTag> tags)
{
var grouped = tags.GroupBy(tag => tag.GroupName).OrderBy(group => group.Key);
foreach (var group in grouped)
{
var drawableGroup = new GroupFlow(group.Key);
foreach (var tag in group.OrderBy(t => t.FullName))
drawableGroup.Add(new DrawableAddableTag(tag) { Action = () => OnSelected?.Invoke(tag) });
yield return drawableGroup;
}
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Select && !e.Repeat)
{
attemptSelect();
return true;
}
return false;
}
private void attemptSelect()
{
var visibleItems = searchContainer.ChildrenOfType<DrawableAddableTag>().Where(d => d.IsPresent).ToArray();
if (visibleItems.Length == 1)
OnSelected?.Invoke(visibleItems.Single().Tag);
}
private partial class GroupFlow : FillFlowContainer, IFilterable
{
public IEnumerable<LocalisableString> FilterTerms { get; }
public bool MatchingFilter
{
set => Alpha = value ? 1 : 0;
}
public bool FilteringActive { set { } }
public GroupFlow(string? name)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(5);
Add(new StatisticItemHeader { Text = name ?? "uncategorised" });
FilterTerms = name == null ? [] : [name];
}
}
private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable
{
public readonly UserTag Tag;
private Box votedBackground = null!;
private SpriteIcon votedIcon = null!;
private readonly Bindable<bool> voted = new Bindable<bool>();
private readonly BindableBool updating = new BindableBool();
private LoadingLayer loadingLayer = null!;
public DrawableAddableTag(UserTag tag)
{
Tag = tag;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
ScaleOnMouseDown = 0.95f;
voted.BindTo(Tag.Voted);
updating.BindTo(Tag.Updating);
}
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Content.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray7,
Depth = float.MaxValue,
},
new Container
{
RelativeSizeAxes = Axes.Y,
Width = 30,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Depth = float.MaxValue,
Children = new Drawable[]
{
votedBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
votedIcon = new SpriteIcon
{
Size = new Vector2(16),
Icon = FontAwesome.Solid.ThumbsUp,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Padding = new MarginPadding(5) { Right = 35 },
Children = new Drawable[]
{
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = Tag.DisplayName,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = Tag.Description,
}
}
},
loadingLayer = new LoadingLayer(dimBackground: true),
});
}
public IEnumerable<LocalisableString> FilterTerms => [Tag.FullName, Tag.Description];
public bool MatchingFilter { set => Alpha = value ? 1 : 0; }
public bool FilteringActive { set { } }
protected override void LoadComplete()
{
base.LoadComplete();
voted.BindValueChanged(_ =>
{
votedBackground.FadeColour(voted.Value ? colours.Lime2 : colours.Gray2, 250, Easing.OutQuint);
votedIcon.FadeColour(voted.Value ? Colour4.Black : Colour4.White, 250, Easing.OutQuint);
}, true);
FinishTransforms(true);
updating.BindValueChanged(u => loadingLayer.State.Value = u.NewValue ? Visibility.Visible : Visibility.Hidden);
}
}
}
private partial class VoteCountText : CompositeDrawable
{
private OsuSpriteText? text;
private readonly Bindable<int> voteCount;
public VoteCountText(Bindable<int> voteCount)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
this.voteCount = voteCount.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
voteCount.BindValueChanged(count =>
{
OsuSpriteText? previousText = text;
AddInternal(text = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
Text = voteCount.Value.ToLocalisableString(),
});
if (previousText != null)
{
const double transition_duration = 500;
bool isIncrease = count.NewValue > count.OldValue;
text.MoveToY(isIncrease ? 20 : -20)
.MoveToY(0, transition_duration, Easing.OutExpo);
previousText.BypassAutoSizeAxes = Axes.Both;
previousText.MoveToY(isIncrease ? -20 : 20, transition_duration, Easing.OutExpo).Expire();
AutoSizeDuration = 300;
AutoSizeEasing = Easing.OutQuint;
}
}, true);
}
}
}
}

View File

@@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select.Leaderboards
// 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, filterMods ? mods.Value.ToArray() : null), forceRefresh: true);
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.Where(m => m.UserPlayable).ToArray() : null), forceRefresh: true);
if (!initialFetchComplete)
{

View File

@@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select.Leaderboards
/// An optional value to guarantee stable ordering.
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
/// </summary>
public Bindable<long> DisplayOrder { get; } = new BindableLong();
public long TotalScoreTiebreaker { get; init; }
/// <summary>
/// A custom function which handles converting a score to a display score using a provided <see cref="ScoringMode"/>.
@@ -68,6 +68,25 @@ namespace osu.Game.Screens.Select.Leaderboards
/// </summary>
public Colour4? TeamColour { get; init; }
/// <summary>
/// The initial position of the score on the leaderboard.
/// Mostly used for cases like the local user's best score on the global leaderboard (which will not be contiguous with the other scores).
/// </summary>
public int? InitialPosition { get; init; }
/// <summary>
/// The displayed rank of the score on the leaderboard.
/// </summary>
public Bindable<int?> Position { get; } = new Bindable<int?>();
/// <summary>
/// The index of the score on the leaderboard.
/// This differs from <see cref="Position"/> in that it is required (must always be known)
/// and that it doesn't represent the score's position on global leaderboards.
/// It's a property completely local to and relative to all scores provided by the managing <see cref="IGameplayLeaderboardProvider"/>.
/// </summary>
public Bindable<long> DisplayOrder { get; } = new BindableLong();
public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked)
{
User = user;
@@ -95,8 +114,9 @@ namespace osu.Game.Screens.Select.Leaderboards
TotalScore.Value = scoreInfo.TotalScore;
Accuracy.Value = scoreInfo.Accuracy;
Combo.Value = scoreInfo.Combo;
DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
GetDisplayScore = scoreInfo.GetDisplayScore;
InitialPosition = scoreInfo.Position;
}
/// <remarks>

View File

@@ -14,14 +14,5 @@ namespace osu.Game.Screens.Select.Leaderboards
/// List of all scores to display on the leaderboard.
/// </summary>
public IBindableList<GameplayLeaderboardScore> Scores { get; }
/// <summary>
/// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores),
/// or is a full leaderboard (contains all scores that there will ever be).
/// </summary>
/// <remarks>
/// If this is <see langword="true"/> and a tracked score is last on the leaderboard, it will show an "unknown" score position.
/// </remarks>
bool IsPartial { get; }
}
}

View File

@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
@@ -55,6 +56,8 @@ namespace osu.Game.Screens.Select.Leaderboards
[Resolved]
private OsuColour colours { get; set; } = null!;
private readonly Cached sorting = new Cached();
public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users)
{
this.users = users;
@@ -101,6 +104,8 @@ namespace osu.Game.Screens.Select.Leaderboards
HasQuit = { BindTarget = trackedUser.UserQuit },
TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null,
};
leaderboardScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
leaderboardScore.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
scores.Add(leaderboardScore);
}
});
@@ -124,6 +129,8 @@ namespace osu.Game.Screens.Select.Leaderboards
// new players are not supported.
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUserIds.BindCollectionChanged(playingUsersChanged);
Scheduler.AddDelayed(sort, 1000, true);
}
private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
@@ -174,6 +181,26 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
private void sort()
{
if (sorting.IsValid)
return;
var orderedByScore = scores
.OrderByDescending(i => i.TotalScore.Value)
.ThenBy(i => i.TotalScoreTiebreaker)
.ToList();
for (int i = 0; i < orderedByScore.Count; i++)
{
var score = orderedByScore[i];
score.DisplayOrder.Value = i;
score.Position.Value = i + 1;
}
sorting.Validate();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@@ -1,8 +1,10 @@
// 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.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
@@ -12,8 +14,6 @@ namespace osu.Game.Screens.Select.Leaderboards
{
public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider
{
public bool IsPartial { get; private set; }
public IBindableList<GameplayLeaderboardScore> Scores => scores;
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
@@ -23,13 +23,16 @@ namespace osu.Game.Screens.Select.Leaderboards
[Resolved]
private GameplayState? gameplayState { get; set; }
private readonly Cached sorting = new Cached();
private bool isPartial;
protected override void LoadComplete()
{
base.LoadComplete();
var globalScores = leaderboardManager?.Scores.Value;
IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
if (globalScores != null)
{
@@ -39,12 +42,73 @@ namespace osu.Game.Screens.Select.Leaderboards
if (gameplayState != null)
{
scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
var localScore = new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
{
// Local score should always show lower than any existing scores in cases of ties.
DisplayOrder = { Value = long.MaxValue }
});
TotalScoreTiebreaker = long.MaxValue
};
localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
scores.Add(localScore);
}
Scheduler.AddDelayed(sort, 1000, true);
}
private void sort()
{
if (sorting.IsValid)
return;
var orderedByScore = scores
.OrderByDescending(i => i.TotalScore.Value)
.ThenBy(i => i.TotalScoreTiebreaker)
.ToList();
int delta = 0;
for (int i = 0; i < orderedByScore.Count; i++)
{
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.
if (!isPartial)
score.Position.Value = i + 1;
else
{
// we have a partial leaderboard, with potential gaps.
// we have initial score positions which were valid at the point of starting play.
// the assumption here is that non-tracked scores here cannot move around, only tracked ones can.
if (score.Tracked)
{
int? previousScorePosition = i > 0 ? orderedByScore[i - 1].InitialPosition : 0;
int? nextScorePosition = i < orderedByScore.Count - 1 ? orderedByScore[i + 1].InitialPosition : null;
// if the tracked score is perfectly between two scores which have known neighbouring initial positions,
// we can assign it the position of the previous score plus one...
if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition)
{
score.Position.Value = previousScorePosition + 1;
// but we also need to ensure all subsequent scores get shifted down one position, too.
delta++;
}
// conversely, if the tracked score is not between neighbouring two scores and the leaderboard is partial,
// we can't really assign a valid position at all. it could be any number between the two neighbours.
else
score.Position.Value = null;
}
// for non-tracked scores, we just need to apply any delta that might have come from the tracked scores
// which might have been encountered and assigned a position earlier.
else
score.Position.Value = score.InitialPosition + delta;
}
}
sorting.Validate();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
@@ -261,12 +275,12 @@ namespace osu.Game.Screens.SelectV2
}
else
{
ratingsWedge.FadeOut(transition_duration, Easing.OutQuint)
.MoveToX(-50, transition_duration, Easing.OutQuint);
failRetryWedge.Delay(100)
.FadeOut(transition_duration, Easing.OutQuint)
failRetryWedge.FadeOut(transition_duration, Easing.OutQuint)
.MoveToX(-50, transition_duration, Easing.OutQuint);
ratingsWedge.Delay(100)
.FadeOut(transition_duration, Easing.OutQuint)
.MoveToX(-50, 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));
}
}
}

View File

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

View File

@@ -308,18 +308,7 @@ namespace osu.Game.Screens.SelectV2
var onlineBeatmapSet = currentOnlineBeatmapSet;
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.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");
}
}

View File

@@ -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;

View File

@@ -16,7 +16,7 @@ namespace osu.Game.Skinning.Components
protected override double RollingDuration => 250;
[SettingSource("Font", "Font", SettingControlType = typeof(OffsetNumberNameSelector))]
public Bindable<string> FontNameDropdown { get; } = new Bindable<string>("NIGHTwhite");
public Bindable<OffsetNumberName> FontNameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.NIGHTWhite);
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
public Bindable<bool> ShowLabel { get; } = new BindableBool();

View File

@@ -13,7 +13,7 @@ namespace osu.Game.Skinning.Components
public partial class EzCounterText : CompositeDrawable, IHasText
{
public readonly EzGetTexture TextPart;
public Bindable<string> FontName { get; } = new Bindable<string>("EZ2DJ-4th");
public Bindable<OffsetNumberName> FontName { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
public FillFlowContainer TextContainer { get; private set; }
@@ -27,14 +27,20 @@ namespace osu.Game.Skinning.Components
// public object Spacing { get; set; }
public EzCounterText(Bindable<string>? externalFontName = null)
public EzCounterText(Bindable<OffsetNumberName>? externalFontName = null)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
if (externalFontName is not null)
FontName.BindTo(externalFontName);
TextPart = new EzGetTexture(textLookup, FontName);
var fontNameString = new Bindable<string>();
FontName.BindValueChanged(e => fontNameString.Value = e.NewValue.ToString(), true);
TextPart = new EzGetTexture(textLookup, fontNameString);
InternalChildren = new Drawable[]
{
TextContainer = new FillFlowContainer
@@ -76,12 +82,11 @@ namespace osu.Game.Skinning.Components
base.LoadComplete();
float scale = calculateScale(TextPart.Height);
if (scale > 0)
TextPart.Scale = new Vector2(scale);
TextPart.Scale = new Vector2(scale);
FontName.BindValueChanged(e =>
{
TextPart.FontName.Value = e.NewValue;
TextPart.FontName.Value = e.NewValue.ToString();
// textPart.LoadAsync(); // **强制重新加载字体**
TextPart.Invalidate(); // **确保 UI 立即刷新**
}, true);
@@ -89,6 +94,9 @@ namespace osu.Game.Skinning.Components
private float calculateScale(float textureHeight, float targetHeight = 25f)
{
if (textureHeight <= 0)
return 1;
return targetHeight / textureHeight;
}
}

View File

@@ -72,23 +72,28 @@ namespace osu.Game.Skinning.Components
string lookup = getLookup(character);
TexturedCharacterGlyph? glyph = null;
string[] possiblePaths = new[]
if (textureName != null)
{
$"Gameplay/Combo/{textureName}/{textureName}-counter-{lookup}",
$"Gameplay/Combo/{textureName}/{textureName}-Title-{lookup}",
$"Gameplay/EarlyOrLate/{textureName}-{lookup}",
$"Gameplay/HitResult/{textureName}-{lookup}"
};
string textureNameReplace = textureName.Replace(" ", "_");
foreach (string path in possiblePaths)
{
var texture = textures.Get(path);
if (texture != null)
string[] possiblePaths = new[]
{
glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null),
texture, 0.125f);
break;
$"Gameplay/Combo/{textureNameReplace}/{textureNameReplace}-counter-{lookup}",
$"Gameplay/Combo/{textureNameReplace}/{textureNameReplace}-Title-{lookup}",
$"Gameplay/EarlyOrLate/{textureNameReplace}-{lookup}",
$"Gameplay/HitResult/{textureNameReplace}-{lookup}"
};
foreach (string path in possiblePaths)
{
var texture = textures.Get(path);
if (texture != null)
{
glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null),
texture, 0.125f);
break;
}
}
}

View File

@@ -1,10 +1,10 @@
// 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 OpenTabletDriver.Plugin;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -61,62 +61,66 @@ namespace osu.Game.Skinning.Components
}
}
public partial class OffsetNumberNameSelector : SettingsDropdown<string>
public enum OffsetNumberName
{
// ReSharper disable InconsistentNaming
Air,
Aqua,
Cricket,
D2D,
DarkConcert,
DJMAX,
EMOTIONAL,
ENDLESS,
EZ2AC_AE,
EZ2AC_CV,
EZ2AC_EVOLVE,
EZ2AC_TT,
EZ2DJ_1thSE,
EZ2DJ_2nd,
EZ2DJ_4th,
EZ2DJ_6th,
EZ2DJ_7th,
EZ2DJ_Platinum,
EZ2ON,
F3_ANCIENT,
F3_CONTEMP,
F3_FUTURE,
F3_MODERN,
FiND_A_WAY,
FORTRESS2,
GC_TYPE2,
Gem,
Gem2,
Gold,
HX_STD,
HX_STD_Yellow,
HX_1121,
Kings,
M250,
NIGHTFALL,
NIGHTWhite,
QTZ_02,
REBOOT,
REBOOT_GOLD,
SG_701,
SH_512,
Star,
TCEZ_001,
TECHNIKA,
Tomato,
VariousWays
// ReSharper restore InconsistentNaming
}
public partial class OffsetNumberNameSelector : SettingsDropdown<OffsetNumberName>
{
protected override void LoadComplete()
{
base.LoadComplete();
Items = new List<string>
{
"Air",
"Aqua",
"Cricket",
"D2D",
"DarkConcert",
"DJMAX",
"EMOTIONAL",
"ENDLESS",
"EZ2AC-AE",
"EZ2AC-CV",
"EZ2AC-EVOLVE",
"EZ2AC-TT",
"EZ2DJ-1thSE",
"EZ2DJ-2nd",
"EZ2DJ-4th",
"EZ2DJ-6th",
"EZ2DJ-7th",
"EZ2DJ-Platinum",
"EZ2ON",
"F3-ANCIENT",
"F3-CONTEMP",
"F3-FUTURE",
"F3-MODERN",
"FiND A WAY",
"FORTRESS2",
"GC-TYPE2",
"Gem",
"Gem2",
"Gold",
"HX STD",
"HX STD黄色",
"HX-1121",
"Kings",
"M250",
"NIGHTFALL",
"NIGHTwhite",
"QTZ-02",
"REBOOT",
"REBOOT GOLD",
"SG-701",
"SH-512",
"Star",
"TCEZ-001",
"TECHNIKA",
"Tomato",
"VariousWays",
};
Log.Debug("Items", Items.ToString());
Items = Enum.GetValues(typeof(OffsetNumberName)).Cast<OffsetNumberName>().ToList();
}
}
}

View File

@@ -68,10 +68,6 @@ namespace osu.Game.Skinning
switch (lookup)
{
case GlobalSkinnableContainerLookup containerLookup:
// Only handle global level defaults for now.
if (containerLookup.Ruleset != null)
return null;
switch (containerLookup.Lookup)
{
case GlobalSkinnableContainers.SongSelect:
@@ -83,6 +79,11 @@ namespace osu.Game.Skinning
return songSelectComponents;
case GlobalSkinnableContainers.MainHUDComponents:
if (containerLookup.Ruleset != null)
{
return new DefaultSkinComponentsContainer(_ => { });
}
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
{
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();

View File

@@ -59,11 +59,8 @@ namespace osu.Game.Utils
/// <summary>
/// Applies rounding to the given BPM value.
/// </summary>
/// <remarks>
/// Double-rounding is applied intentionally (see https://github.com/ppy/osu/pull/18345#issue-1243311382 for rationale).
/// </remarks>
/// <param name="baseBpm">The base BPM to round.</param>
/// <param name="rate">Rate adjustment, if applicable.</param>
public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(Math.Round(baseBpm) * rate);
public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(baseBpm * rate);
}
}