Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2563d86
enforce realtime difficulty calculation when using non-default config…
tsunyoku Jul 15, 2024
9d2cd87
add additional support for realtime calculations on non-legacy mods
tsunyoku Jul 15, 2024
3b159d5
line endings
tsunyoku Jul 15, 2024
435a1f5
remove unneeded interpolation
tsunyoku Jul 15, 2024
749f505
line endings
tsunyoku Jul 15, 2024
8aae8a2
fix CL edge case
tsunyoku Jul 15, 2024
927af4e
rename `isLegacyMod` to `isRankedLegacyMod` and add missing mania mods
tsunyoku Jul 16, 2024
59ca627
line endings (sigh)
tsunyoku Jul 16, 2024
9835e52
add `OsuModSpunOut`
tsunyoku Jul 16, 2024
8480823
handle hard rock being unranked for mania
tsunyoku Jul 16, 2024
b540e7b
use `ALWAYS_REALTIME_DIFFICULTY` in `DatabaseTest`
tsunyoku Jul 16, 2024
5e1b1d9
line endings
tsunyoku Jul 16, 2024
f514a3c
add test
tsunyoku Jul 16, 2024
26636d0
line endings
tsunyoku Jul 16, 2024
0ec8c96
enforce realtime difficulty calculation when using non-default config…
tsunyoku Jul 15, 2024
12217e7
remove unneeded interpolation
tsunyoku Jul 15, 2024
be5a334
fix CL edge case
tsunyoku Jul 15, 2024
c03acf1
rename `isLegacyMod` to `isRankedLegacyMod` and add missing mania mods
tsunyoku Jul 16, 2024
ff2aa59
add `OsuModSpunOut`
tsunyoku Jul 16, 2024
7bbb452
handle hard rock being unranked for mania
tsunyoku Jul 16, 2024
b3ed260
use `ALWAYS_REALTIME_DIFFICULTY` in `DatabaseTest`
tsunyoku Jul 16, 2024
fc5e32a
add test
tsunyoku Jul 16, 2024
ffb1b48
Merge branch 'partial-real-time-difficulty' of https://github.com/tsu…
tsunyoku Jul 17, 2024
2f562cd
rename env var to `PREFER_REALTIME_DIFFICULTY`
tsunyoku Jul 17, 2024
808dc5f
fix README grammar
tsunyoku Jul 17, 2024
19f72cb
move prefer naming back to always
tsunyoku Jul 22, 2024
2abc9b2
rename to ALWAYS_USE_REALTIME_DIFFICULTY
tsunyoku Jul 22, 2024
09efc44
more precise legacy mod checking
tsunyoku Aug 1, 2024
327669a
Add preventative test coverage guarding against accidental inheritance
bdach Aug 7, 2024
8b57913
Remove space in envvar name
bdach Aug 7, 2024
68f0ce1
Merge branch 'master' into partial-real-time-difficulty
bdach Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ Whether to process user total stats. Set to `0` to disable processing.

Default is unset (processing enabled).

### REALTIME_DIFFICULTY
### ALWAYS_USE_REALTIME_DIFFICULTY

Whether to use realtime processing (download beatmaps and compute their difficulty attributes on every processed score), or to rely on database data. Set to `0` to disable processing.
Whether to always use realtime processing (download beatmaps and compute their difficulty attributes on every processed score), or to rely on database data when possible. Set to `0` to disable processing.

Default is unset (processing enabled).

Expand Down
105 changes: 105 additions & 0 deletions osu.Server.Queues.ScoreStatisticsProcessor.Tests/BeatmapStoreTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Server.Queues.ScoreStatisticsProcessor.Stores;
using Xunit;

namespace osu.Server.Queues.ScoreStatisticsProcessor.Tests
{
public class BeatmapStoreTest
{
private static readonly HashSet<Type> osu_ranked_mods = new HashSet<Type>
{
typeof(OsuModNoFail),
typeof(OsuModEasy),
typeof(OsuModTouchDevice),
typeof(OsuModHidden),
typeof(OsuModHardRock),
typeof(OsuModSuddenDeath),
typeof(OsuModDoubleTime),
typeof(OsuModHalfTime),
typeof(OsuModNightcore),
typeof(OsuModFlashlight),
typeof(OsuModSpunOut),
typeof(OsuModPerfect),
};

private static readonly HashSet<Type> taiko_ranked_mods = new HashSet<Type>
{
typeof(TaikoModNoFail),
typeof(TaikoModEasy),
typeof(TaikoModHidden),
typeof(TaikoModHardRock),
typeof(TaikoModSuddenDeath),
typeof(TaikoModDoubleTime),
typeof(TaikoModHalfTime),
typeof(TaikoModNightcore),
typeof(TaikoModFlashlight),
typeof(TaikoModPerfect),
};

private static readonly HashSet<Type> catch_ranked_mods = new HashSet<Type>
{
typeof(CatchModNoFail),
typeof(CatchModEasy),
typeof(CatchModHidden),
typeof(CatchModHardRock),
typeof(CatchModSuddenDeath),
typeof(CatchModDoubleTime),
typeof(CatchModHalfTime),
typeof(CatchModNightcore),
typeof(CatchModFlashlight),
typeof(CatchModPerfect),
};

private static readonly HashSet<Type> mania_ranked_mods = new HashSet<Type>
{
typeof(ManiaModNoFail),
typeof(ManiaModEasy),
typeof(ManiaModHidden),
typeof(ManiaModSuddenDeath),
typeof(ManiaModDoubleTime),
typeof(ManiaModHalfTime),
typeof(ManiaModNightcore),
typeof(ManiaModFlashlight),
typeof(ManiaModPerfect),
typeof(ManiaModKey4),
typeof(ManiaModKey5),
typeof(ManiaModKey6),
typeof(ManiaModKey7),
typeof(ManiaModKey8),
typeof(ManiaModFadeIn),
typeof(ManiaModKey9),
typeof(ManiaModMirror),
};

public static readonly object[][] RANKED_TEST_DATA =
[
[new OsuRuleset(), osu_ranked_mods],
[new TaikoRuleset(), taiko_ranked_mods],
[new CatchRuleset(), catch_ranked_mods],
[new ManiaRuleset(), mania_ranked_mods],
];

[Theory]
[MemberData(nameof(RANKED_TEST_DATA))]
public void TestLegacyModsMarkedAsRankedCorrectly(Ruleset ruleset, HashSet<Type> legacyModTypes)
{
var rulesetMods = ruleset.CreateAllMods();

foreach (var mod in rulesetMods)
Assert.Equal(legacyModTypes.Contains(mod.GetType()), BeatmapStore.IsRankedLegacyMod(mod));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected DatabaseTest(AssemblyName[]? externalProcessorAssemblies = null)
? new CancellationTokenSource()
: new CancellationTokenSource(20000);

Environment.SetEnvironmentVariable("REALTIME_DIFFICULTY", "0");
Environment.SetEnvironmentVariable("ALWAYS_USE_REALTIME_DIFFICULTY", "0");

Processor = new ScoreStatisticsQueueProcessor(externalProcessorAssemblies: externalProcessorAssemblies);
Processor.Error += processorOnError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,37 @@ public async Task UserDailyRankUpdates()
});
}

[Fact]
public void EnforcedRealtimeDifficultyModsAwardPP()
{
AddBeatmap(b => b.beatmap_id = 315);

// difficulty attributes are intentionally not added to the database
// so if pp was populated, it had to go through the realtime calculations

ScoreItem score;

using (MySqlConnection conn = Processor.GetDatabaseConnection())
{
score = CreateTestScore(rulesetId: 0, beatmapId: 315);

score.Score.ScoreData.Statistics[HitResult.Great] = 100;
score.Score.max_combo = 100;
score.Score.accuracy = 1;
score.Score.build_id = TestBuildID;
score.Score.ScoreData.Mods = new[] { new APIMod(new OsuModMuted()), new APIMod(new OsuModTraceable()) };
score.Score.preserve = true;

conn.Insert(score.Score);
PushToQueueAndWaitForProcess(score);
}

WaitForDatabaseState("SELECT COUNT(*) FROM scores WHERE id = @ScoreId AND pp IS NOT NULL", 1, CancellationToken, new
{
ScoreId = score.Score.id
});
}

private class InvalidMod : Mod
{
public override string Name => "Invalid";
Expand Down
64 changes: 60 additions & 4 deletions osu.Server.Queues.ScoreStatisticsProcessor/Stores/BeatmapStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -13,10 +14,15 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Server.Queues.ScoreStatisticsProcessor.Helpers;
using osu.Server.Queues.ScoreStatisticsProcessor.Models;
using StatsdClient;
using Beatmap = osu.Server.Queues.ScoreStatisticsProcessor.Models.Beatmap;

namespace osu.Server.Queues.ScoreStatisticsProcessor.Stores
Expand All @@ -26,7 +32,7 @@ namespace osu.Server.Queues.ScoreStatisticsProcessor.Stores
/// </summary>
public class BeatmapStore
{
private static readonly bool use_realtime_difficulty_calculation = Environment.GetEnvironmentVariable("REALTIME_DIFFICULTY") != "0";
private static readonly bool always_use_realtime_difficulty_calculation = Environment.GetEnvironmentVariable("ALWAYS_USE_REALTIME_DIFFICULTY") != "0";
private static readonly string beatmap_download_path = Environment.GetEnvironmentVariable("BEATMAP_DOWNLOAD_PATH") ?? "https://osu.ppy.sh/osu/{0}";

private readonly ConcurrentDictionary<uint, Beatmap?> beatmapCache = new ConcurrentDictionary<uint, Beatmap?>();
Expand Down Expand Up @@ -65,8 +71,16 @@ public static async Task<BeatmapStore> CreateAsync(MySqlConnection connection, M
/// <returns>The difficulty attributes or <c>null</c> if not existing.</returns>
public async Task<DifficultyAttributes?> GetDifficultyAttributesAsync(Beatmap beatmap, Ruleset ruleset, Mod[] mods, MySqlConnection connection, MySqlTransaction? transaction = null)
{
if (use_realtime_difficulty_calculation)
// database attributes are stored using the default mod configurations
// if we want to support mods with non-default configurations (i.e non-1.5x rates on DT/NC)
// or non-legacy mods which aren't populated into the database (with exception to CL)
// then we must calculate difficulty attributes in real-time.
bool mustUseRealtimeDifficulty = mods.Any(m => !m.UsesDefaultConfiguration || (!IsRankedLegacyMod(m) && m is not ModClassic));

if (always_use_realtime_difficulty_calculation || mustUseRealtimeDifficulty)
{
var stopwatch = Stopwatch.StartNew();

using var req = new WebRequest(string.Format(beatmap_download_path, beatmap.beatmap_id));

req.AllowInsecureRequests = true;
Expand All @@ -79,7 +93,17 @@ public static async Task<BeatmapStore> CreateAsync(MySqlConnection connection, M
var workingBeatmap = new StreamedWorkingBeatmap(req.ResponseStream);
var calculator = ruleset.CreateDifficultyCalculator(workingBeatmap);

return calculator.Calculate(mods);
var attributes = calculator.Calculate(mods);

string[] tags =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have to make sure to turn off indexing on these as soon as it goes live or we'll rack up a $1000 bill in no time

{
$"ruleset:{ruleset.RulesetInfo.OnlineID}",
$"mods:{string.Join("", mods.Select(x => x.Acronym))}"
};

DogStatsd.Timer("calculate-realtime-difficulty-attributes", stopwatch.ElapsedMilliseconds, tags: tags);

return attributes;
}

BeatmapDifficultyAttribute[]? rawDifficultyAttributes;
Expand Down Expand Up @@ -107,12 +131,44 @@ public static async Task<BeatmapStore> CreateAsync(MySqlConnection connection, M
return difficultyAttributes;
}

/// <remarks>
/// This method attempts to create a simple solution to deciding if a <see cref="Mod"/> can be considered a ranked "legacy" mod.
/// Used by <see cref="GetDifficultyAttributesAsync"/> to decide if the current mod combination's difficulty attributes
/// can be fetched from the database.
/// </remarks>
public static bool IsRankedLegacyMod(Mod mod) =>
mod is ModNoFail
or ModEasy
or ModPerfect
or ModSuddenDeath
or ModNightcore
or ModDoubleTime
or ModHalfTime
or ModFlashlight
or ModTouchDevice
or OsuModHardRock
or OsuModSpunOut
or OsuModHidden
or TaikoModHardRock
or TaikoModHidden
or CatchModHardRock
or CatchModHidden
or ManiaModKey4
or ManiaModKey5
or ManiaModKey6
or ManiaModKey7
or ManiaModKey8
or ManiaModKey9
or ManiaModMirror
or ManiaModHidden
or ManiaModFadeIn;

/// <remarks>
/// This method attempts to choose the best possible set of <see cref="LegacyMods"/> to use for looking up stored difficulty attributes.
/// The match is not always exact; for some mods that award pp but do not exist in stable
/// (such as <see cref="ModHalfTime"/>) the closest available approximation is used.
/// Moreover, the set of <see cref="LegacyMods"/> returned is constrained to mods that actually affect difficulty in the legacy sense.
/// The entirety of this workaround is not used / unnecessary if <see cref="use_realtime_difficulty_calculation"/> is <see langword="true"/>.
/// The entirety of this workaround is not used / unnecessary if <see cref="always_use_realtime_difficulty_calculation"/> is <see langword="true"/>.
/// </remarks>
private static LegacyMods getLegacyModsForAttributeLookup(Beatmap beatmap, Ruleset ruleset, Mod[] mods)
{
Expand Down