diff --git a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/DailySproutTest.cs b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/DailySproutTest.cs new file mode 100644 index 00000000..cb833aa7 --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/DailySproutTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Dapper; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; +using Xunit; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Tests +{ + public class DailySproutTest : MedalAwarderTest + { + private readonly Beatmap beatmap; + + public DailySproutTest() + { + beatmap = AddBeatmap(); + AddMedal(336); + } + + [Fact] + public void MedalNotAwardedIfNoDailyChallengesOnRecord() + { + SetScoreForBeatmap(beatmap.beatmap_id); + AssertNoMedalsAwarded(); + } + + [Fact] + public void MedalAwardedIfAtLeastOneDailyChallengeOnRecord() + { + using (var db = Processor.GetDatabaseConnection()) + db.Execute("INSERT INTO `daily_challenge_user_stats` (`user_id`, `daily_streak_best`) VALUES (2, 1)"); + SetScoreForBeatmap(beatmap.beatmap_id); + AssertSingleMedalAwarded(336); + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/GameNightTest.cs b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/GameNightTest.cs new file mode 100644 index 00000000..c6945647 --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/GameNightTest.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API; +using osu.Game.Rulesets.Osu.Mods; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; +using Xunit; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Tests +{ + public class GameNightTest : MedalAwarderTest + { + private readonly Beatmap beatmap; + + public GameNightTest() + { + AddMedal(340); + beatmap = AddBeatmap(); + } + + [Fact] + public void TestMedalAwarded() + { + SetScoreForBeatmap(beatmap.beatmap_id, s => s.Score.ScoreData.Mods = [new APIMod(new OsuModBubbles())]); + AssertSingleMedalAwarded(340); + } + + [Fact] + public void TestMedalAwardedWithExtraMods() + { + SetScoreForBeatmap(beatmap.beatmap_id, s => s.Score.ScoreData.Mods = [new APIMod(new OsuModBubbles()), new APIMod(new OsuModClassic())]); + AssertSingleMedalAwarded(340); + } + + [Fact] + public void TestMedalNotAwardedIfFunModsMissing() + { + SetScoreForBeatmap(beatmap.beatmap_id, s => s.Score.ScoreData.Mods = [new APIMod(new OsuModClassic())]); + AssertNoMedalsAwarded(); + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/GearShiftTest.cs b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/GearShiftTest.cs new file mode 100644 index 00000000..b6a14223 --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/GearShiftTest.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API; +using osu.Game.Rulesets.Osu.Mods; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; +using Xunit; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Tests +{ + public class GearShiftTest : MedalAwarderTest + { + private readonly Beatmap beatmap; + + public GearShiftTest() + { + AddMedal(339); + beatmap = AddBeatmap(); + } + + [Fact] + public void TestMedalAwarded() + { + SetScoreForBeatmap(beatmap.beatmap_id, s => s.Score.ScoreData.Mods = [new APIMod(new OsuModAlternate())]); + AssertMedalAwarded(339); + } + + [Fact] + public void TestMedalAwardedWithExtraMods() + { + SetScoreForBeatmap(beatmap.beatmap_id, s => s.Score.ScoreData.Mods = [new APIMod(new OsuModAlternate()), new APIMod(new OsuModDoubleTime())]); + AssertMedalAwarded(339); + } + + [Fact] + public void TestMedalNotAwardedIfConversionModsMissing() + { + SetScoreForBeatmap(beatmap.beatmap_id, s => s.Score.ScoreData.Mods = [new APIMod(new OsuModFreezeFrame())]); + AssertNoMedalsAwarded(); + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MedalAwarderTest.cs b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MedalAwarderTest.cs index 4ccd29df..f6924b9a 100644 --- a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MedalAwarderTest.cs +++ b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MedalAwarderTest.cs @@ -27,6 +27,8 @@ protected MedalAwarderTest(AssemblyName[]? externalProcessorAssemblies = null) db.Execute("TRUNCATE TABLE osu_beatmappacks"); db.Execute("TRUNCATE TABLE osu_beatmappacks_items"); + + db.Execute("TRUNCATE TABLE daily_challenge_user_stats"); } } diff --git a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MonthlyShrubTest.cs b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MonthlyShrubTest.cs new file mode 100644 index 00000000..94dae3d3 --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/MonthlyShrubTest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Dapper; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; +using Xunit; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Tests +{ + public class MonthlyShrubTest : MedalAwarderTest + { + private readonly Beatmap beatmap; + + public MonthlyShrubTest() + { + beatmap = AddBeatmap(); + AddMedal(338); + } + + [Theory] + [InlineData(0)] + [InlineData(9)] + [InlineData(26)] + public void MedalNotAwardedIfNotEnoughDailyChallengesOnRecord(int dailyChallengeCount) + { + using (var db = Processor.GetDatabaseConnection()) + db.Execute($"INSERT INTO `daily_challenge_user_stats` (`user_id`, `daily_streak_best`) VALUES (2, {dailyChallengeCount})"); + SetScoreForBeatmap(beatmap.beatmap_id); + AssertNoMedalsAwarded(); + } + + [Fact] + public void MedalAwardedIfAtLeastThirtyDailyChallengesOnRecord() + { + using (var db = Processor.GetDatabaseConnection()) + db.Execute("INSERT INTO `daily_challenge_user_stats` (`user_id`, `daily_streak_best`) VALUES (2, 30)"); + SetScoreForBeatmap(beatmap.beatmap_id); + AssertSingleMedalAwarded(338); + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor.Tests/WeeklySaplingTest.cs b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/WeeklySaplingTest.cs new file mode 100644 index 00000000..5a31284e --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor.Tests/WeeklySaplingTest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Dapper; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; +using Xunit; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Tests +{ + public class WeeklySaplingTest : MedalAwarderTest + { + private readonly Beatmap beatmap; + + public WeeklySaplingTest() + { + beatmap = AddBeatmap(); + AddMedal(337); + } + + [Theory] + [InlineData(0)] + [InlineData(4)] + [InlineData(6)] + public void MedalNotAwardedIfNotEnoughDailyChallengesOnRecord(int dailyChallengeCount) + { + using (var db = Processor.GetDatabaseConnection()) + db.Execute($"INSERT INTO `daily_challenge_user_stats` (`user_id`, `daily_streak_best`) VALUES (2, {dailyChallengeCount})"); + SetScoreForBeatmap(beatmap.beatmap_id); + AssertNoMedalsAwarded(); + } + + [Fact] + public void MedalAwardedIfAtLeastSevenDailyChallengesOnRecord() + { + using (var db = Processor.GetDatabaseConnection()) + db.Execute("INSERT INTO `daily_challenge_user_stats` (`user_id`, `daily_streak_best`) VALUES (2, 7)"); + SetScoreForBeatmap(beatmap.beatmap_id); + AssertSingleMedalAwarded(337); + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor/Models/DailyChallengeUserStats.cs b/osu.Server.Queues.ScoreStatisticsProcessor/Models/DailyChallengeUserStats.cs new file mode 100644 index 00000000..0e31a3c9 --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor/Models/DailyChallengeUserStats.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using Dapper.Contrib.Extensions; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Models +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + [Serializable] + [Table("daily_challenge_user_stats")] + public record DailyChallengeUserStats + { + public uint daily_streak_current { get; set; } + public uint daily_streak_best { get; set; } + public uint playcount { get; set; } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor/Models/MedalAwarderContext.cs b/osu.Server.Queues.ScoreStatisticsProcessor/Models/MedalAwarderContext.cs index 4c446d92..2768bfbb 100644 --- a/osu.Server.Queues.ScoreStatisticsProcessor/Models/MedalAwarderContext.cs +++ b/osu.Server.Queues.ScoreStatisticsProcessor/Models/MedalAwarderContext.cs @@ -12,12 +12,14 @@ namespace osu.Server.Queues.ScoreStatisticsProcessor.Models /// /// The score to check for medals. /// The calculated user statistics after . + /// The user's daily challenge stats after . /// Allows retrieval of s from database. /// MySQL connection for manual retrieval from database. /// MySQL transaction for manual retrieval from database. public record MedalAwarderContext( SoloScore Score, UserStats UserStats, + DailyChallengeUserStats DailyChallengeUserStats, BeatmapStore BeatmapStore, MySqlConnection Connection, MySqlTransaction Transaction); diff --git a/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalAwarders/DailyChallengeMedalAwarder.cs b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalAwarders/DailyChallengeMedalAwarder.cs new file mode 100644 index 00000000..245496b2 --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalAwarders/DailyChallengeMedalAwarder.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using JetBrains.Annotations; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Processors.MedalAwarders +{ + [UsedImplicitly] + public class DailyChallengeMedalAwarder : IMedalAwarder + { + public bool RunOnFailedScores => false; + public bool RunOnLegacyScores => false; + + public IEnumerable Check(IEnumerable medals, MedalAwarderContext context) + { + foreach (var medal in medals) + { + switch (medal.achievement_id) + { + case 336: + { + if (context.DailyChallengeUserStats.daily_streak_best >= 1) + yield return medal; + + break; + } + + case 337: + { + if (context.DailyChallengeUserStats.daily_streak_best >= 7) + yield return medal; + + break; + } + + case 338: + { + if (context.DailyChallengeUserStats.daily_streak_best >= 30) + yield return medal; + + break; + } + } + } + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalAwarders/LazerModIntroductionMedalAwarder.cs b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalAwarders/LazerModIntroductionMedalAwarder.cs new file mode 100644 index 00000000..e372c28c --- /dev/null +++ b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalAwarders/LazerModIntroductionMedalAwarder.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . 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 JetBrains.Annotations; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Server.Queues.ScoreStatisticsProcessor.Helpers; +using osu.Server.Queues.ScoreStatisticsProcessor.Models; + +namespace osu.Server.Queues.ScoreStatisticsProcessor.Processors.MedalAwarders +{ + [UsedImplicitly] + public class LazerModIntroductionMedalAwarder : IMedalAwarder + { + public bool RunOnFailedScores => false; + public bool RunOnLegacyScores => false; + + public IEnumerable Check(IEnumerable medals, MedalAwarderContext context) + { + Ruleset ruleset = LegacyRulesetHelper.GetRulesetFromLegacyId(context.Score.ruleset_id); + Mod[] mods = context.Score.ScoreData.Mods.Select(m => m.ToMod(ruleset)).ToArray(); + + foreach (var medal in medals) + { + switch (medal.achievement_id) + { + // Gear Shift + case 339: + { + if (mods.Any(m => m.Type == ModType.Conversion)) + yield return medal; + + break; + } + + // Game Night + case 340: + { + if (mods.Any(m => m.Type == ModType.Fun)) + yield return medal; + + break; + } + } + } + } + } +} diff --git a/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalProcessor.cs b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalProcessor.cs index d2b9662f..7beb310a 100644 --- a/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalProcessor.cs +++ b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/MedalProcessor.cs @@ -70,8 +70,16 @@ public void ApplyToUserStats(SoloScore score, UserStats userStats, MySqlConnecti .Where(m => !alreadyAchieved.Contains(m.achievement_id)) .ToArray(); + var dailyChallengeUserStats = conn.QuerySingleOrDefault( + @"SELECT * FROM `daily_challenge_user_stats` WHERE `user_id` = @user_id", + new + { + user_id = userStats.user_id + }, + transaction) ?? new DailyChallengeUserStats(); + var beatmapStore = BeatmapStore.CreateAsync(conn, transaction).Result; - var context = new MedalAwarderContext(score, userStats, beatmapStore, conn, transaction); + var context = new MedalAwarderContext(score, userStats, dailyChallengeUserStats, beatmapStore, conn, transaction); foreach (var awarder in medal_awarders) {