mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 05:53:10 +08:00
Add MaximumStatistics upgrade for databased scores
This commit is contained in:
parent
9aab502adc
commit
731d3f3b63
@ -7,6 +7,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -14,6 +15,7 @@ using osu.Framework.Logging;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game
|
namespace osu.Game
|
||||||
@ -23,6 +25,9 @@ namespace osu.Game
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesetStore { get; set; } = null!;
|
private RulesetStore rulesetStore { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScoreManager scoreManager { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RealmAccess realmAccess { get; set; } = null!;
|
private RealmAccess realmAccess { get; set; } = null!;
|
||||||
|
|
||||||
@ -41,11 +46,12 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Logger.Log("Beginning background beatmap processing..");
|
Logger.Log("Beginning background beatmap processing..");
|
||||||
checkForOutdatedStarRatings();
|
checkForOutdatedStarRatings();
|
||||||
processBeatmapSetsWithMissingMetrics();
|
processBeatmapSetsWithMissingMetrics();
|
||||||
|
await processScoresWithMissingStatistics();
|
||||||
}).ContinueWith(t =>
|
}).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.Exception?.InnerException is ObjectDisposedException)
|
if (t.Exception?.InnerException is ObjectDisposedException)
|
||||||
@ -140,5 +146,52 @@ namespace osu.Game
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task processScoresWithMissingStatistics()
|
||||||
|
{
|
||||||
|
HashSet<Guid> scoreIds = new HashSet<Guid>();
|
||||||
|
|
||||||
|
Logger.Log("Querying for scores to reprocess...");
|
||||||
|
|
||||||
|
realmAccess.Run(r =>
|
||||||
|
{
|
||||||
|
foreach (var score in r.All<ScoreInfo>())
|
||||||
|
{
|
||||||
|
if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0)
|
||||||
|
scoreIds.Add(score.ID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Log($"Found {scoreIds.Count} scores which require reprocessing.");
|
||||||
|
|
||||||
|
foreach (var id in scoreIds)
|
||||||
|
{
|
||||||
|
while (localUserPlayInfo?.IsPlaying.Value == true)
|
||||||
|
{
|
||||||
|
Logger.Log("Background processing sleeping due to active gameplay...");
|
||||||
|
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var score = scoreManager.Query(s => s.ID == id);
|
||||||
|
|
||||||
|
await scoreManager.PopulateMaximumStatistics(score);
|
||||||
|
|
||||||
|
// Can't use async overload because we're not on the update thread.
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realmAccess.Write(r =>
|
||||||
|
{
|
||||||
|
r.Find<ScoreInfo>(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Log($"Populated maximum statistics for score {id}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,7 +279,7 @@ namespace osu.Game
|
|||||||
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
|
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
|
||||||
|
|
||||||
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
|
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
|
||||||
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig));
|
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig, difficultyCache));
|
||||||
|
|
||||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
|
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -16,6 +18,8 @@ using osu.Game.Scoring.Legacy;
|
|||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Scoring
|
namespace osu.Game.Scoring
|
||||||
@ -28,15 +32,17 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
private readonly RulesetStore rulesets;
|
private readonly RulesetStore rulesets;
|
||||||
private readonly Func<BeatmapManager> beatmaps;
|
private readonly Func<BeatmapManager> beatmaps;
|
||||||
|
private readonly BeatmapDifficultyCache? difficultyCache;
|
||||||
|
|
||||||
private readonly IAPIProvider api;
|
private readonly IAPIProvider api;
|
||||||
|
|
||||||
public ScoreImporter(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api)
|
public ScoreImporter(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api, BeatmapDifficultyCache? difficultyCache = null)
|
||||||
: base(storage, realm)
|
: base(storage, realm)
|
||||||
{
|
{
|
||||||
this.rulesets = rulesets;
|
this.rulesets = rulesets;
|
||||||
this.beatmaps = beatmaps;
|
this.beatmaps = beatmaps;
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
this.difficultyCache = difficultyCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ScoreInfo? CreateModel(ArchiveReader archive)
|
protected override ScoreInfo? CreateModel(ArchiveReader archive)
|
||||||
@ -71,6 +77,8 @@ namespace osu.Game.Scoring
|
|||||||
if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo));
|
if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo));
|
||||||
if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset));
|
if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset));
|
||||||
|
|
||||||
|
PopulateMaximumStatistics(model).WaitSafely();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(model.StatisticsJson))
|
if (string.IsNullOrEmpty(model.StatisticsJson))
|
||||||
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
|
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
|
||||||
|
|
||||||
@ -78,6 +86,69 @@ namespace osu.Game.Scoring
|
|||||||
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
|
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The score to populate the statistics of.</param>
|
||||||
|
public async Task PopulateMaximumStatistics(ScoreInfo score)
|
||||||
|
{
|
||||||
|
if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var beatmap = score.BeatmapInfo.Detach();
|
||||||
|
var ruleset = score.Ruleset.Detach();
|
||||||
|
|
||||||
|
// Populate the maximum statistics.
|
||||||
|
HitResult maxBasicResult = ruleset.CreateInstance().GetHitResults()
|
||||||
|
.Select(h => h.result)
|
||||||
|
.Where(h => h.IsBasic())
|
||||||
|
.OrderByDescending(Judgement.ToNumericResult).First();
|
||||||
|
|
||||||
|
foreach ((HitResult result, int count) in score.Statistics)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.LargeTickHit:
|
||||||
|
case HitResult.LargeTickMiss:
|
||||||
|
score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.SmallTickHit:
|
||||||
|
case HitResult.SmallTickMiss:
|
||||||
|
score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.IgnoreHit:
|
||||||
|
case HitResult.IgnoreMiss:
|
||||||
|
case HitResult.SmallBonus:
|
||||||
|
case HitResult.LargeBonus:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!score.IsLegacyScore)
|
||||||
|
return;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
if (difficultyCache == null)
|
||||||
|
throw new InvalidOperationException($"Cannot populate legacy score statistics without a {nameof(BeatmapDifficultyCache)}.");
|
||||||
|
|
||||||
|
// In osu! and osu!mania, some judgements affect combo but aren't stored to scores.
|
||||||
|
// A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes.
|
||||||
|
StarDifficulty? difficulty = await difficultyCache.GetDifficultyAsync(beatmap, ruleset, score.Mods);
|
||||||
|
if (difficulty == null)
|
||||||
|
throw new InvalidOperationException("Failed to populate maximum statistics due to missing difficulty attributes.");
|
||||||
|
|
||||||
|
int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum();
|
||||||
|
if (difficulty.Value.MaxCombo > maxComboFromStatistics)
|
||||||
|
score.MaximumStatistics[HitResult.LegacyComboIncrease] = difficulty.Value.MaxCombo - maxComboFromStatistics;
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
}
|
||||||
|
|
||||||
protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport)
|
protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport)
|
||||||
{
|
{
|
||||||
base.PostImport(model, realm, batchImport);
|
base.PostImport(model, realm, batchImport);
|
||||||
|
@ -28,12 +28,13 @@ namespace osu.Game.Scoring
|
|||||||
private readonly OsuConfigManager configManager;
|
private readonly OsuConfigManager configManager;
|
||||||
private readonly ScoreImporter scoreImporter;
|
private readonly ScoreImporter scoreImporter;
|
||||||
|
|
||||||
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api, OsuConfigManager configManager = null)
|
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api,
|
||||||
|
OsuConfigManager configManager = null, BeatmapDifficultyCache difficultyCache = null)
|
||||||
: base(storage, realm)
|
: base(storage, realm)
|
||||||
{
|
{
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
|
|
||||||
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
|
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api, difficultyCache)
|
||||||
{
|
{
|
||||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||||
};
|
};
|
||||||
@ -178,6 +179,12 @@ namespace osu.Game.Scoring
|
|||||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||||
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
|
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The score to populate the statistics of.</param>
|
||||||
|
public Task PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score);
|
||||||
|
|
||||||
#region Implementation of IPresentImports<ScoreInfo>
|
#region Implementation of IPresentImports<ScoreInfo>
|
||||||
|
|
||||||
public Action<IEnumerable<Live<ScoreInfo>>> PresentImport
|
public Action<IEnumerable<Live<ScoreInfo>>> PresentImport
|
||||||
|
Loading…
Reference in New Issue
Block a user