1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

Merge pull request #25711 from smoogipoo/mania-convert-song-select-keycount

Display osu!mania keycount in song select carousel panels and details
This commit is contained in:
Dean Herbert 2023-12-13 16:31:25 +09:00 committed by GitHub
commit fdcf87569c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 204 additions and 41 deletions

View File

@ -57,13 +57,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
{
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
return GetColumnCountForNonConvert(difficulty);
double roundedCircleSize = Math.Round(difficulty.CircleSize);
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
return (int)Math.Max(1, roundedCircleSize);
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
// optimisations, it actually ends up happening on doubles.
@ -79,12 +80,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty)
{
double roundedCircleSize = Math.Round(difficulty.CircleSize);
return (int)Math.Max(1, roundedCircleSize);
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)

View File

@ -4,6 +4,7 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mania
public bool Matches(BeatmapInfo beatmapInfo)
{
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty)));
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)));
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)

View File

@ -420,6 +420,9 @@ namespace osu.Game.Rulesets.Mania
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
public int GetKeyCount(IBeatmapInfo beatmapInfo)
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo));
}
public enum PlayfieldType

View File

@ -14,7 +14,9 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Details;
using osuTK.Graphics;
@ -38,6 +40,12 @@ namespace osu.Game.Tests.Visual.SongSelect
Width = 500
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset game ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
}
private BeatmapInfo exampleBeatmapInfo => new BeatmapInfo
{
Ruleset = rulesets.AvailableRulesets.First(),
@ -66,8 +74,10 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
public void TestManiaFirstBarText()
public void TestManiaFirstBarTextManiaBeatmap()
{
AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
{
Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException("osu!mania ruleset not found"),
@ -84,6 +94,27 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
}
[Test]
public void TestManiaFirstBarTextConvert()
{
AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
{
Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty
{
CircleSize = 5,
DrainRate = 4.3f,
OverallDifficulty = 4.5f,
ApproachRate = 3.1f
},
StarRating = 8
});
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
}
[Test]
public void TestEasyMod()
{

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -21,6 +20,7 @@ using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;
using Realms;
namespace osu.Game
{
@ -68,6 +68,7 @@ namespace osu.Game
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
processBeatmapsWithMissingObjectCounts();
processScoresWithMissingStatistics();
convertLegacyTotalScoreToStandardised();
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
@ -134,19 +135,13 @@ namespace osu.Game
// of other possible ways), but for now avoid queueing if the user isn't logged in at startup.
if (api.IsLoggedIn)
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)))
{
Debug.Assert(b.BeatmapSet != null);
beatmapSetIds.Add(b.BeatmapSet.ID);
}
foreach (var b in r.All<BeatmapInfo>().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && b.BeatmapSet != null))
beatmapSetIds.Add(b.BeatmapSet!.ID);
}
else
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0))
{
Debug.Assert(b.BeatmapSet != null);
beatmapSetIds.Add(b.BeatmapSet.ID);
}
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 && b.BeatmapSet != null))
beatmapSetIds.Add(b.BeatmapSet!.ID);
}
});
@ -178,6 +173,42 @@ namespace osu.Game
}
}
private void processBeatmapsWithMissingObjectCounts()
{
Logger.Log("Querying for beatmaps with missing hitobject counts to reprocess...");
HashSet<Guid> beatmapIds = realmAccess.Run(r => new HashSet<Guid>(r.All<BeatmapInfo>()
.Filter($"{nameof(BeatmapInfo.Difficulty)}.{nameof(BeatmapDifficulty.TotalObjectCount)} == 0")
.AsEnumerable().Select(b => b.ID)));
Logger.Log($"Found {beatmapIds.Count} beatmaps which require reprocessing.");
int i = 0;
foreach (var id in beatmapIds)
{
sleepIfRequired();
realmAccess.Run(r =>
{
var beatmap = r.Find<BeatmapInfo>(id);
if (beatmap != null)
{
try
{
Logger.Log($"Background processing {beatmap} ({++i} / {beatmapIds.Count})");
beatmapUpdater.ProcessObjectCounts(beatmap);
}
catch (Exception e)
{
Logger.Log($"Background processing failed on {beatmap}: {e}");
}
}
});
}
}
private void processScoresWithMissingStatistics()
{
HashSet<Guid> scoreIds = new HashSet<Guid>();

View File

@ -21,6 +21,9 @@ namespace osu.Game.Beatmaps
public double SliderMultiplier { get; set; } = 1.4;
public double SliderTickRate { get; set; } = 1;
public int EndTimeObjectCount { get; set; }
public int TotalObjectCount { get; set; }
public BeatmapDifficulty()
{
}
@ -44,6 +47,9 @@ namespace osu.Game.Beatmaps
difficulty.SliderMultiplier = SliderMultiplier;
difficulty.SliderTickRate = SliderTickRate;
difficulty.EndTimeObjectCount = EndTimeObjectCount;
difficulty.TotalObjectCount = TotalObjectCount;
}
public virtual void CopyFrom(IBeatmapDifficultyInfo other)
@ -55,6 +61,9 @@ namespace osu.Game.Beatmaps
SliderMultiplier = other.SliderMultiplier;
SliderTickRate = other.SliderTickRate;
EndTimeObjectCount = other.EndTimeObjectCount;
TotalObjectCount = other.TotalObjectCount;
}
}
}

View File

@ -20,6 +20,7 @@ using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects.Types;
using Realms;
namespace osu.Game.Beatmaps
@ -388,6 +389,8 @@ namespace osu.Game.Beatmaps
ApproachRate = decodedDifficulty.ApproachRate,
SliderMultiplier = decodedDifficulty.SliderMultiplier,
SliderTickRate = decodedDifficulty.SliderTickRate,
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = decoded.HitObjects.Count
};
var metadata = new BeatmapMetadata

View File

@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
@ -10,6 +11,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps
{
@ -44,7 +46,8 @@ namespace osu.Game.Beatmaps
public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously,
updateScheduler);
}
/// <summary>
@ -80,6 +83,21 @@ namespace osu.Game.Beatmaps
workingBeatmapCache.Invalidate(beatmapSet);
});
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ =>
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapInfo);
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
var beatmap = working.Beatmap;
beatmapInfo.Difficulty.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
beatmapInfo.Difficulty.TotalObjectCount = beatmap.HitObjects.Count;
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
workingBeatmapCache.Invalidate(beatmapInfo);
});
#region Implementation of IDisposable
public void Dispose()

View File

@ -44,6 +44,19 @@ namespace osu.Game.Beatmaps
/// </summary>
double SliderTickRate { get; }
/// <summary>
/// The number of hitobjects in the beatmap with a distinct end time.
/// </summary>
/// <remarks>
/// Canonically, these are hitobjects are either sliders or spinners.
/// </remarks>
int EndTimeObjectCount { get; }
/// <summary>
/// The total number of hitobjects in the beatmap.
/// </summary>
int TotalObjectCount { get; }
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>

View File

@ -88,8 +88,9 @@ namespace osu.Game.Database
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
/// 37 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapDifficulty.
/// </summary>
private const int schema_version = 36;
private const int schema_version = 37;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -109,6 +109,8 @@ namespace osu.Game.Online.API.Requests.Responses
CircleSize = CircleSize,
ApproachRate = ApproachRate,
OverallDifficulty = OverallDifficulty,
EndTimeObjectCount = SliderCount + SpinnerCount,
TotalObjectCount = CircleCount + SliderCount + SpinnerCount
};
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring.Legacy;
namespace osu.Game.Rulesets
@ -14,6 +15,12 @@ namespace osu.Game.Rulesets
/// </summary>
int LegacyID { get; }
/// <summary>
/// Retrieves the number of mania keys required to play the beatmap.
/// </summary>
/// <returns></returns>
int GetKeyCount(IBeatmapInfo beatmapInfo) => 0;
ILegacyScoreSimulator CreateLegacyScoreSimulator();
}
}

View File

@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Scoring.Legacy
public float OverallDifficulty { get; set; }
/// <summary>
/// The count of hitcircles in the beatmap.
/// The number of hitobjects in the beatmap with a distinct end time.
/// </summary>
/// <remarks>
/// When converting from osu! ruleset beatmaps, this is equivalent to the sum of sliders and spinners in the beatmap.
/// Canonically, these are hitobjects are either sliders or spinners.
/// </remarks>
public int CircleCount { get; set; }
public int EndTimeObjectCount { get; set; }
/// <summary>
/// The total count of hitobjects in the beatmap.
@ -46,22 +46,24 @@ namespace osu.Game.Rulesets.Scoring.Legacy
double IBeatmapDifficultyInfo.SliderMultiplier => 0;
double IBeatmapDifficultyInfo.SliderTickRate => 0;
public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => new LegacyBeatmapConversionDifficultyInfo
{
SourceRuleset = apiBeatmap.Ruleset,
CircleSize = apiBeatmap.CircleSize,
OverallDifficulty = apiBeatmap.OverallDifficulty,
CircleCount = apiBeatmap.CircleCount,
TotalObjectCount = apiBeatmap.SliderCount + apiBeatmap.SpinnerCount + apiBeatmap.CircleCount
};
public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => FromBeatmapInfo(apiBeatmap);
public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo
{
SourceRuleset = beatmap.BeatmapInfo.Ruleset,
CircleSize = beatmap.Difficulty.CircleSize,
OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
CircleCount = beatmap.HitObjects.Count(h => h is not IHasDuration),
EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = beatmap.HitObjects.Count
};
public static LegacyBeatmapConversionDifficultyInfo FromBeatmapInfo(IBeatmapInfo beatmapInfo) => new LegacyBeatmapConversionDifficultyInfo
{
SourceRuleset = beatmapInfo.Ruleset,
CircleSize = beatmapInfo.Difficulty.CircleSize,
OverallDifficulty = beatmapInfo.Difficulty.OverallDifficulty,
EndTimeObjectCount = beatmapInfo.Difficulty.EndTimeObjectCount,
TotalObjectCount = beatmapInfo.Difficulty.TotalObjectCount
};
}
}

View File

@ -27,6 +27,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@ -57,6 +58,8 @@ namespace osu.Game.Screens.Select.Carousel
private StarCounter starCounter = null!;
private DifficultyIcon difficultyIcon = null!;
private OsuSpriteText keyCountText = null!;
[Resolved]
private BeatmapSetOverlay? beatmapOverlay { get; set; }
@ -69,6 +72,9 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private IBindable<StarDifficulty?> starDifficultyBindable = null!;
private CancellationTokenSource? starDifficultyCancellationSource;
@ -133,6 +139,13 @@ namespace osu.Game.Screens.Select.Carousel
AutoSizeAxes = Axes.Both,
Children = new[]
{
keyCountText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 20),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Alpha = 0,
},
new OsuSpriteText
{
Text = beatmapInfo.DifficultyName,
@ -167,6 +180,13 @@ namespace osu.Game.Screens.Select.Carousel
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateKeyCount());
}
protected override void Selected()
{
base.Selected();
@ -216,11 +236,28 @@ namespace osu.Game.Screens.Select.Carousel
if (d.NewValue != null)
difficultyIcon.Current.Value = d.NewValue.Value;
}, true);
updateKeyCount();
}
base.ApplyState();
}
private void updateKeyCount()
{
if (ruleset.Value.OnlineID == 3)
{
// Account for mania differences locally for now.
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
keyCountText.Alpha = 1;
keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo)}K]";
}
else
keyCountText.Alpha = 0;
}
public MenuItem[] ContextMenuItems
{
get

View File

@ -126,13 +126,23 @@ namespace osu.Game.Screens.Select.Details
mod.ApplyToDifficulty(adjustedDifficulty);
}
switch (BeatmapInfo?.Ruleset.OnlineID)
IRulesetInfo ruleset = gameRuleset?.Value ?? beatmapInfo.Ruleset;
switch (ruleset.OnlineID)
{
case 3:
// Account for mania differences locally for now
// Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes
// Account for mania differences locally for now.
// Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes.
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.CreateInstance();
// For the time being, the key count is static no matter what, because:
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo);
FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania;
FirstValue.Value = (baseDifficulty?.CircleSize ?? 0, null);
FirstValue.Value = (keyCount, keyCount);
break;
default: