diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
index 312d3d5e9a..2178941fd8 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
@@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.EmptyFreeform
         {
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
-            return new DifficultyAttributes(mods, 0);
+            return new IDifficultyAttributes(mods, 0);
         }
 
         protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index f6addab279..55cf4fb42d 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.Pippidon
         {
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
-            return new DifficultyAttributes(mods, 0);
+            return new IDifficultyAttributes(mods, 0);
         }
 
         protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
index a4dc1762d5..47215ce7d9 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
@@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.EmptyScrolling
         {
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
-            return new DifficultyAttributes(mods, 0);
+            return new IDifficultyAttributes(mods, 0);
         }
 
         protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index f6addab279..55cf4fb42d 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.Pippidon
         {
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
-            return new DifficultyAttributes(mods, 0);
+            return new IDifficultyAttributes(mods, 0);
         }
 
         protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index 5c64643fd4..3ff1c0dd01 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -1,15 +1,28 @@
 // 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 Newtonsoft.Json;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Catch.Difficulty
 {
-    public class CatchDifficultyAttributes : DifficultyAttributes
+    public struct CatchDifficultyAttributes : IDifficultyAttributes
     {
+        public CatchDifficultyAttributes() { }
+
+        /// <inheritdoc/>
+        public Mod[] Mods { get; set; } = Array.Empty<Mod>();
+
+        /// <inheritdoc/>
+        public double StarRating { get; set; }
+
+        /// <inheritdoc/>
+        public int MaxCombo { get; set; }
+
         /// <summary>
         /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
         /// </summary>
@@ -19,22 +32,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
         [JsonProperty("approach_rate")]
         public double ApproachRate { get; set; }
 
-        public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
+        public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
         {
-            foreach (var v in base.ToDatabaseAttributes())
-                yield return v;
-
+            yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo);
             // Todo: osu!catch should not output star rating in the 'aim' attribute.
-            yield return (ATTRIB_ID_AIM, StarRating);
-            yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
+            yield return (IDifficultyAttributes.ATTRIB_ID_AIM, StarRating);
+            yield return (IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE, ApproachRate);
         }
 
-        public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
+        public void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
         {
-            base.FromDatabaseAttributes(values, onlineInfo);
-
-            StarRating = values[ATTRIB_ID_AIM];
-            ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
+            MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO];
+            StarRating = values[IDifficultyAttributes.ATTRIB_ID_AIM];
+            ApproachRate = values[IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE];
         }
     }
 }
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 7d21409ee8..4637fd3c0a 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
         {
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
             if (beatmap.HitObjects.Count == 0)
                 return new CatchDifficultyAttributes { Mods = mods };
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index efca1e5e77..a8a0e3c444 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
         {
         }
 
-        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
+        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes)
         {
             var catchAttributes = (CatchDifficultyAttributes)attributes;
 
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index db60e757e1..8244334748 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -1,15 +1,28 @@
 // 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 Newtonsoft.Json;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Mania.Difficulty
 {
-    public class ManiaDifficultyAttributes : DifficultyAttributes
+    public struct ManiaDifficultyAttributes : IDifficultyAttributes
     {
+        public ManiaDifficultyAttributes() { }
+
+        /// <inheritdoc/>
+        public Mod[] Mods { get; set; } = Array.Empty<Mod>();
+
+        /// <inheritdoc/>
+        public double StarRating { get; set; }
+
+        /// <inheritdoc/>
+        public int MaxCombo { get; set; }
+
         /// <summary>
         /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
         /// </summary>
@@ -19,21 +32,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty
         [JsonProperty("great_hit_window")]
         public double GreatHitWindow { get; set; }
 
-        public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
+        public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
         {
-            foreach (var v in base.ToDatabaseAttributes())
-                yield return v;
-
-            yield return (ATTRIB_ID_DIFFICULTY, StarRating);
-            yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
+            yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo);
+            yield return (IDifficultyAttributes.ATTRIB_ID_DIFFICULTY, StarRating);
+            yield return (IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
         }
 
-        public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
+        public void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
         {
-            base.FromDatabaseAttributes(values, onlineInfo);
-
-            StarRating = values[ATTRIB_ID_DIFFICULTY];
-            GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
+            MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO];
+            StarRating = values[IDifficultyAttributes.ATTRIB_ID_DIFFICULTY];
+            GreatHitWindow = values[IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW];
         }
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index ff9aa4aa7b..ae49c495c5 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
             originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
             if (beatmap.HitObjects.Count == 0)
                 return new ManiaDifficultyAttributes { Mods = mods };
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index 4d1b9b5766..b01bcc2d04 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
         {
         }
 
-        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
+        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes)
         {
             var maniaAttributes = (ManiaDifficultyAttributes)attributes;
 
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index a3c0209a08..315b7b5fbc 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -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.Collections.Generic;
 using System.Linq;
 using JetBrains.Annotations;
@@ -11,8 +12,19 @@ using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Osu.Difficulty
 {
-    public class OsuDifficultyAttributes : DifficultyAttributes
+    public struct OsuDifficultyAttributes : IDifficultyAttributes
     {
+        public OsuDifficultyAttributes() { }
+
+        /// <inheritdoc/>
+        public Mod[] Mods { get; set; } = Array.Empty<Mod>();
+
+        /// <inheritdoc/>
+        public double StarRating { get; set; }
+
+        /// <inheritdoc/>
+        public int MaxCombo { get; set; }
+
         /// <summary>
         /// The difficulty corresponding to the aim skill.
         /// </summary>
@@ -90,41 +102,38 @@ namespace osu.Game.Rulesets.Osu.Difficulty
         /// </summary>
         public int SpinnerCount { get; set; }
 
-        public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
+        public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
         {
-            foreach (var v in base.ToDatabaseAttributes())
-                yield return v;
-
-            yield return (ATTRIB_ID_AIM, AimDifficulty);
-            yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
-            yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
-            yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
-            yield return (ATTRIB_ID_DIFFICULTY, StarRating);
+            yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo);
+            yield return (IDifficultyAttributes.ATTRIB_ID_AIM, AimDifficulty);
+            yield return (IDifficultyAttributes.ATTRIB_ID_SPEED, SpeedDifficulty);
+            yield return (IDifficultyAttributes.ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
+            yield return (IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE, ApproachRate);
+            yield return (IDifficultyAttributes.ATTRIB_ID_DIFFICULTY, StarRating);
 
             if (ShouldSerializeFlashlightDifficulty())
-                yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
+                yield return (IDifficultyAttributes.ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
 
-            yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
+            yield return (IDifficultyAttributes.ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
 
-            yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
-            yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
-            yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
+            yield return (IDifficultyAttributes.ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
+            yield return (IDifficultyAttributes.ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
+            yield return (IDifficultyAttributes.ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
         }
 
-        public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
+        public void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
         {
-            base.FromDatabaseAttributes(values, onlineInfo);
-
-            AimDifficulty = values[ATTRIB_ID_AIM];
-            SpeedDifficulty = values[ATTRIB_ID_SPEED];
-            OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
-            ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
-            StarRating = values[ATTRIB_ID_DIFFICULTY];
-            FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
-            SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
-            AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
-            SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
-            SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
+            MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO];
+            AimDifficulty = values[IDifficultyAttributes.ATTRIB_ID_AIM];
+            SpeedDifficulty = values[IDifficultyAttributes.ATTRIB_ID_SPEED];
+            OverallDifficulty = values[IDifficultyAttributes.ATTRIB_ID_OVERALL_DIFFICULTY];
+            ApproachRate = values[IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE];
+            StarRating = values[IDifficultyAttributes.ATTRIB_ID_DIFFICULTY];
+            FlashlightDifficulty = values.GetValueOrDefault(IDifficultyAttributes.ATTRIB_ID_FLASHLIGHT);
+            SliderFactor = values[IDifficultyAttributes.ATTRIB_ID_SLIDER_FACTOR];
+            AimDifficultStrainCount = values[IDifficultyAttributes.ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
+            SpeedDifficultStrainCount = values[IDifficultyAttributes.ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
+            SpeedNoteCount = values[IDifficultyAttributes.ATTRIB_ID_SPEED_NOTE_COUNT];
             DrainRate = onlineInfo.DrainRate;
             HitCircleCount = onlineInfo.CircleCount;
             SliderCount = onlineInfo.SliderCount;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index acf01b2a83..97603bb16a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
         {
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
             if (beatmap.HitObjects.Count == 0)
                 return new OsuDifficultyAttributes { Mods = mods };
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 8425f437cc..b79a8feb96 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
         {
         }
 
-        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
+        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes)
         {
             var osuAttributes = (OsuDifficultyAttributes)attributes;
 
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 451aed183d..337e1c1488 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -1,15 +1,29 @@
 // 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 Newtonsoft.Json;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Taiko.Difficulty
 {
-    public class TaikoDifficultyAttributes : DifficultyAttributes
+    public struct TaikoDifficultyAttributes : IDifficultyAttributes
     {
+        public TaikoDifficultyAttributes() { }
+
+        /// <inheritdoc/>
+        public Mod[] Mods { get; set; } = Array.Empty<Mod>();
+
+        /// <inheritdoc/>
+        public double StarRating { get; set; }
+
+        /// <inheritdoc/>
+        [JsonProperty("max_combo", Order = -2)]
+        public int MaxCombo { get; set; }
+
         /// <summary>
         /// The difficulty corresponding to the stamina skill.
         /// </summary>
@@ -52,23 +66,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
         [JsonProperty("ok_hit_window")]
         public double OkHitWindow { get; set; }
 
-        public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
+        public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
         {
-            foreach (var v in base.ToDatabaseAttributes())
-                yield return v;
-
-            yield return (ATTRIB_ID_DIFFICULTY, StarRating);
-            yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
-            yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
+            yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo);
+            yield return (IDifficultyAttributes.ATTRIB_ID_DIFFICULTY, StarRating);
+            yield return (IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
+            yield return (IDifficultyAttributes.ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
         }
 
-        public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
+        public void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
         {
-            base.FromDatabaseAttributes(values, onlineInfo);
-
-            StarRating = values[ATTRIB_ID_DIFFICULTY];
-            GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
-            OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
+            MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO];
+            StarRating = values[IDifficultyAttributes.ATTRIB_ID_DIFFICULTY];
+            GreatHitWindow = values[IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW];
+            OkHitWindow = values[IDifficultyAttributes.ATTRIB_ID_OK_HIT_WINDOW];
         }
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 18223e74fa..4b86620b39 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
             return difficultyHitObjects;
         }
 
-        protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+        protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
         {
             if (beatmap.HitObjects.Count == 0)
                 return new TaikoDifficultyAttributes { Mods = mods };
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 29eadf417f..7373993629 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
         {
         }
 
-        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
+        protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes)
         {
             var taikoAttributes = (TaikoDifficultyAttributes)attributes;
 
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 6b1b883ce7..7c63bbd9a3 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -222,7 +222,7 @@ namespace osu.Game.Tests.NonVisual
 
             protected override Mod[] DifficultyAdjustmentMods { get; }
 
-            protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+            protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
             {
                 throw new NotImplementedException();
             }
diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs
index f860cd097a..5db37dd810 100644
--- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs
+++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs
@@ -172,7 +172,7 @@ namespace osu.Game.Tests.NonVisual
             {
             }
 
-            protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+            protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
                 => new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() };
 
             protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
@@ -208,7 +208,7 @@ namespace osu.Game.Tests.NonVisual
             }
         }
 
-        private class TestDifficultyAttributes : DifficultyAttributes
+        private class TestDifficultyAttributes : IDifficultyAttributes
         {
             public HitObject[] Objects = Array.Empty<HitObject>();
         }
diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs
index 6aac275a6a..e0a7e724a3 100644
--- a/osu.Game/Beatmaps/StarDifficulty.cs
+++ b/osu.Game/Beatmaps/StarDifficulty.cs
@@ -26,13 +26,13 @@ namespace osu.Game.Beatmaps
         /// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
         /// </summary>
         [CanBeNull]
-        public readonly DifficultyAttributes Attributes;
+        public readonly IDifficultyAttributes Attributes;
 
         /// <summary>
-        /// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed
+        /// Creates a <see cref="StarDifficulty"/> structure based on <see cref="IDifficultyAttributes"/> computed
         /// by a <see cref="DifficultyCalculator"/>.
         /// </summary>
-        public StarDifficulty([NotNull] DifficultyAttributes attributes)
+        public StarDifficulty([NotNull] IDifficultyAttributes attributes)
         {
             Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0;
             MaxCombo = attributes.MaxCombo;
@@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps
 
         /// <summary>
         /// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo
-        /// in scenarios where computing <see cref="DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
+        /// in scenarios where computing <see cref="IDifficultyAttributes"/> is not feasible (i.e. when working with online sources).
         /// </summary>
         public StarDifficulty(double starDifficulty, int maxCombo)
         {
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index 63b27243d0..39340776f0 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Difficulty
         /// </summary>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>A structure describing the difficulty of the beatmap.</returns>
-        public DifficultyAttributes Calculate(CancellationToken cancellationToken = default)
+        public IDifficultyAttributes Calculate(CancellationToken cancellationToken = default)
             => Calculate(Array.Empty<Mod>(), cancellationToken);
 
         /// <summary>
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Difficulty
         /// <param name="mods">The mods that should be applied to the beatmap.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>A structure describing the difficulty of the beatmap.</returns>
-        public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
+        public IDifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
         {
             cancellationToken.ThrowIfCancellationRequested();
             preProcess(mods, cancellationToken);
@@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Difficulty
         /// This can only be used to compute difficulties for legacy mod combinations.
         /// </remarks>
         /// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
-        public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
+        public IEnumerable<IDifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
         {
             var rulesetInstance = ruleset.CreateInstance();
 
@@ -263,14 +263,14 @@ namespace osu.Game.Rulesets.Difficulty
         protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
 
         /// <summary>
-        /// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
+        /// Creates <see cref="IDifficultyAttributes"/> to describe beatmap's calculated difficulty.
         /// </summary>
         /// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.
         /// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
         /// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
         /// <param name="skills">The skills which processed the beatmap.</param>
         /// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
-        protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
+        protected abstract IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
 
         /// <summary>
         /// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/IDifficultyAttributes.cs
similarity index 65%
rename from osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
rename to osu.Game/Rulesets/Difficulty/IDifficultyAttributes.cs
index ae4239c148..b951b5fb11 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/IDifficultyAttributes.cs
@@ -1,7 +1,6 @@
 // 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 Newtonsoft.Json;
 using osu.Game.Beatmaps;
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Difficulty
     /// Describes the difficulty of a beatmap, as output by a <see cref="DifficultyCalculator"/>.
     /// </summary>
     [JsonObject(MemberSerialization.OptIn)]
-    public class DifficultyAttributes
+    public interface IDifficultyAttributes
     {
         protected const int ATTRIB_ID_AIM = 1;
         protected const int ATTRIB_ID_SPEED = 3;
@@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Difficulty
         /// <summary>
         /// The mods which were applied to the beatmap.
         /// </summary>
-        public Mod[] Mods { get; set; } = Array.Empty<Mod>();
+        public Mod[] Mods { get; set; }
 
         /// <summary>
         /// The combined star rating of all skills.
@@ -48,42 +47,18 @@ namespace osu.Game.Rulesets.Difficulty
         public int MaxCombo { get; set; }
 
         /// <summary>
-        /// Creates new <see cref="DifficultyAttributes"/>.
-        /// </summary>
-        public DifficultyAttributes()
-        {
-        }
-
-        /// <summary>
-        /// Creates new <see cref="DifficultyAttributes"/>.
-        /// </summary>
-        /// <param name="mods">The mods which were applied to the beatmap.</param>
-        /// <param name="starRating">The combined star rating of all skills.</param>
-        public DifficultyAttributes(Mod[] mods, double starRating)
-        {
-            Mods = mods;
-            StarRating = starRating;
-        }
-
-        /// <summary>
-        /// Converts this <see cref="DifficultyAttributes"/> to osu-web compatible database attribute mappings.
+        /// Converts this <see cref="IDifficultyAttributes"/> to osu-web compatible database attribute mappings.
         /// </summary>
         /// <remarks>
         /// See: osu_difficulty_attribs table.
         /// </remarks>
-        public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
-        {
-            yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
-        }
+        public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes();
 
         /// <summary>
-        /// Reads osu-web database attribute mappings into this <see cref="DifficultyAttributes"/> object.
+        /// Reads osu-web database attribute mappings into this <see cref="IDifficultyAttributes"/> object.
         /// </summary>
         /// <param name="values">The attribute mappings.</param>
         /// <param name="onlineInfo">The <see cref="IBeatmapOnlineInfo"/> where more information about the beatmap may be extracted from (such as AR/CS/OD/etc).</param>
-        public virtual void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
-        {
-            MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
-        }
+        public void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo);
     }
 }
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/IPerformanceAttributes.cs
similarity index 100%
rename from osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs
rename to osu.Game/Rulesets/Difficulty/IPerformanceAttributes.cs
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
index ee7f02f9be..b7ff4b9812 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
@@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Difficulty
             Ruleset = ruleset;
         }
 
-        public Task<IPerformanceAttributes> CalculateAsync(ScoreInfo score, DifficultyAttributes attributes, CancellationToken cancellationToken)
+        public Task<IPerformanceAttributes> CalculateAsync(ScoreInfo score, IDifficultyAttributes attributes, CancellationToken cancellationToken)
             => Task.Run(() => CreatePerformanceAttributes(score, attributes), cancellationToken);
 
-        public IPerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes)
+        public IPerformanceAttributes Calculate(ScoreInfo score, IDifficultyAttributes attributes)
             => CreatePerformanceAttributes(score, attributes);
 
         public IPerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap)
@@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Difficulty
         /// </summary>
         /// <param name="score">The score to create the attributes for.</param>
         /// <param name="attributes">The difficulty attributes for the beatmap relating to the score.</param>
-        protected abstract IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes);
+        protected abstract IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes);
     }
 }
diff --git a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
index a07827d50b..b6a7aa26da 100644
--- a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
@@ -8,7 +8,7 @@ using System;
 namespace osu.Game.Rulesets.Difficulty
 {
     /// <summary>
-    /// Wraps a <see cref="DifficultyAttributes"/> object and adds a time value for which the attribute is valid.
+    /// Wraps a <see cref="IDifficultyAttributes"/> object and adds a time value for which the attribute is valid.
     /// Output by DifficultyCalculator.CalculateTimed methods.
     /// </summary>
     public class TimedDifficultyAttributes : IComparable<TimedDifficultyAttributes>
@@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Difficulty
         /// <summary>
         /// The attributes.
         /// </summary>
-        public readonly DifficultyAttributes Attributes;
+        public readonly IDifficultyAttributes Attributes;
 
         /// <summary>
         /// Creates new <see cref="TimedDifficultyAttributes"/>.
         /// </summary>
         /// <param name="time">The non-clock-adjusted time value at which the attributes take effect.</param>
         /// <param name="attributes">The attributes.</param>
-        public TimedDifficultyAttributes(double time, DifficultyAttributes attributes)
+        public TimedDifficultyAttributes(double time, IDifficultyAttributes attributes)
         {
             Time = time;
             Attributes = attributes;
diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
index 25c1387220..a48124fb68 100644
--- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
+++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Screens.Play.HUD
         }
 
         [CanBeNull]
-        private DifficultyAttributes getAttributeAtTime(JudgementResult judgement)
+        private IDifficultyAttributes getAttributeAtTime(JudgementResult judgement)
         {
             if (timedAttributes == null || timedAttributes.Count == 0)
                 return null;