diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 440aa8a7fc..aa00e182a9 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Difficulty; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Catch { @@ -119,6 +120,8 @@ namespace osu.Game.Rulesets.Catch public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score); + public override int? LegacyID => 2; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs new file mode 100644 index 0000000000..5a640f6d1a --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -0,0 +1,104 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + public class CatchPerformanceCalculator : PerformanceCalculator + { + protected new CatchDifficultyAttributes Attributes => (CatchDifficultyAttributes)base.Attributes; + + private Mod[] mods; + + private int fruitsHit; + private int ticksHit; + private int tinyTicksHit; + private int tinyTicksMissed; + private int misses; + + public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) + : base(ruleset, beatmap, score) + { + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + mods = Score.Mods; + + var legacyScore = Score as LegacyScoreInfo; + + fruitsHit = legacyScore?.Count300 ?? Score.Statistics[HitResult.Perfect]; + ticksHit = legacyScore?.Count100 ?? 0; + tinyTicksHit = legacyScore?.Count50 ?? 0; + tinyTicksMissed = legacyScore?.CountKatu ?? 0; + misses = Score.Statistics[HitResult.Miss]; + + // Don't count scores made with supposedly unranked mods + if (mods.Any(m => !m.Ranked)) + return 0; + + // We are heavily relying on aim in catch the beat + double value = Math.Pow(5.0f * Math.Max(1.0f, Attributes.StarRating / 0.0049f) - 4.0f, 2.0f) / 100000.0f; + + // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo + int numTotalHits = totalComboHits(); + + // Longer maps are worth more + float lengthBonus = + 0.95f + 0.4f * Math.Min(1.0f, numTotalHits / 3000.0f) + + (numTotalHits > 3000 ? (float)Math.Log10(numTotalHits / 3000.0f) * 0.5f : 0.0f); + + // Longer maps are worth more + value *= lengthBonus; + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + value *= Math.Pow(0.97f, misses); + + // Combo scaling + float beatmapMaxCombo = Attributes.MaxCombo; + if (beatmapMaxCombo > 0) + value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); + + float approachRate = (float)Attributes.ApproachRate; + float approachRateFactor = 1.0f; + if (approachRate > 9.0f) + approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9 + else if (approachRate < 8.0f) + approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8 + + value *= approachRateFactor; + + if (mods.Any(m => m is ModHidden)) + // Hiddens gives nothing on max approach rate, and more the lower it is + value *= 1.05f + 0.075f * (10.0f - Math.Min(10.0f, approachRate)); // 7.5% for each AR below 10 + + if (mods.Any(m => m is ModFlashlight)) + // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. + value *= 1.35f * lengthBonus; + + // Scale the aim value with accuracy _slightly_ + value *= Math.Pow(accuracy(), 5.5f); + + // Custom multipliers for NoFail. SpunOut is not applicable. + if (mods.Any(m => m is ModNoFail)) + value *= 0.90f; + + return value; + } + + private float accuracy() => totalHits() == 0 ? 0 : MathHelper.Clamp((float)totalSuccessfulHits() / totalHits(), 0f, 1f); + private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed; + private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit; + private int totalComboHits() => misses + ticksHit + fruitsHit; + } +} diff --git a/osu.Game/Scoring/Legacy/LegacyScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyScoreInfo.cs new file mode 100644 index 0000000000..df80f848e3 --- /dev/null +++ b/osu.Game/Scoring/Legacy/LegacyScoreInfo.cs @@ -0,0 +1,116 @@ +// 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.Rulesets.Scoring; + +namespace osu.Game.Scoring.Legacy +{ + public class LegacyScoreInfo : ScoreInfo + { + private int countGeki; + + public int CountGeki + { + get => countGeki; + set + { + countGeki = value; + + switch (Ruleset?.ID ?? RulesetID) + { + case 3: + Statistics[HitResult.Perfect] = value; + break; + } + } + } + + private int count300; + + public int Count300 + { + get => count300; + set + { + count300 = value; + + switch (Ruleset?.ID ?? RulesetID) + { + case 0: + case 1: + case 3: + Statistics[HitResult.Great] = value; + break; + case 2: + Statistics[HitResult.Perfect] = value; + break; + } + } + } + + private int countKatu; + + public int CountKatu + { + get => countKatu; + set + { + countKatu = value; + + switch (Ruleset?.ID ?? RulesetID) + { + case 3: + Statistics[HitResult.Good] = value; + break; + } + } + } + + private int count100; + + public int Count100 + { + get => count100; + set + { + count100 = value; + + switch (Ruleset?.ID ?? RulesetID) + { + case 0: + case 1: + Statistics[HitResult.Good] = value; + break; + case 3: + Statistics[HitResult.Ok] = value; + break; + } + } + } + + private int count50; + + public int Count50 + { + get => count50; + set + { + count50 = value; + + switch (Ruleset?.ID ?? RulesetID) + { + case 0: + case 3: + Statistics[HitResult.Meh] = value; + break; + } + } + } + + public int CountMiss + { + get => Statistics[HitResult.Miss]; + set => Statistics[HitResult.Miss] = value; + } + } +} diff --git a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs index ace8892330..51810945db 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs @@ -34,7 +34,9 @@ namespace osu.Game.Scoring.Legacy using (SerializationReader sr = new SerializationReader(stream)) { currentRuleset = GetRuleset(sr.ReadByte()); - score.ScoreInfo = new ScoreInfo { Ruleset = currentRuleset.RulesetInfo }; + var scoreInfo = new LegacyScoreInfo { Ruleset = currentRuleset.RulesetInfo }; + + score.ScoreInfo = scoreInfo; var version = sr.ReadInt32(); @@ -43,66 +45,39 @@ namespace osu.Game.Scoring.Legacy throw new BeatmapNotFoundException(); currentBeatmap = workingBeatmap.Beatmap; - score.ScoreInfo.Beatmap = currentBeatmap.BeatmapInfo; + scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; - score.ScoreInfo.User = new User { Username = sr.ReadString() }; + scoreInfo.User = new User { Username = sr.ReadString() }; // MD5Hash sr.ReadString(); - var count300 = (int)sr.ReadUInt16(); - var count100 = (int)sr.ReadUInt16(); - var count50 = (int)sr.ReadUInt16(); - var countGeki = (int)sr.ReadUInt16(); - var countKatu = (int)sr.ReadUInt16(); - var countMiss = (int)sr.ReadUInt16(); + scoreInfo.Count300 = sr.ReadUInt16(); + scoreInfo.Count100 = sr.ReadUInt16(); + scoreInfo.Count50 = sr.ReadUInt16(); + scoreInfo.CountGeki = sr.ReadUInt16(); + scoreInfo.CountKatu = sr.ReadUInt16(); + scoreInfo.CountMiss = sr.ReadUInt16(); - switch (currentRuleset.LegacyID) - { - case 0: - score.ScoreInfo.Statistics[HitResult.Great] = count300; - score.ScoreInfo.Statistics[HitResult.Good] = count100; - score.ScoreInfo.Statistics[HitResult.Meh] = count50; - score.ScoreInfo.Statistics[HitResult.Miss] = countMiss; - break; - case 1: - score.ScoreInfo.Statistics[HitResult.Great] = count300; - score.ScoreInfo.Statistics[HitResult.Good] = count100; - score.ScoreInfo.Statistics[HitResult.Miss] = countMiss; - break; - case 2: - score.ScoreInfo.Statistics[HitResult.Perfect] = count300; - score.ScoreInfo.Statistics[HitResult.Miss] = countMiss; - break; - case 3: - score.ScoreInfo.Statistics[HitResult.Perfect] = countGeki; - score.ScoreInfo.Statistics[HitResult.Great] = count300; - score.ScoreInfo.Statistics[HitResult.Good] = countKatu; - score.ScoreInfo.Statistics[HitResult.Ok] = count100; - score.ScoreInfo.Statistics[HitResult.Meh] = count50; - score.ScoreInfo.Statistics[HitResult.Miss] = countMiss; - break; - } - - score.ScoreInfo.TotalScore = sr.ReadInt32(); - score.ScoreInfo.MaxCombo = sr.ReadUInt16(); + scoreInfo.TotalScore = sr.ReadInt32(); + scoreInfo.MaxCombo = sr.ReadUInt16(); /* score.Perfect = */ sr.ReadBoolean(); - score.ScoreInfo.Mods = currentRuleset.ConvertLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); + scoreInfo.Mods = currentRuleset.ConvertLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); /* score.HpGraphString = */ sr.ReadString(); - score.ScoreInfo.Date = sr.ReadDateTime(); + scoreInfo.Date = sr.ReadDateTime(); var compressedReplay = sr.ReadByteArray(); if (version >= 20140721) - score.ScoreInfo.OnlineScoreID = sr.ReadInt64(); + scoreInfo.OnlineScoreID = sr.ReadInt64(); else if (version >= 20121008) - score.ScoreInfo.OnlineScoreID = sr.ReadInt32(); + scoreInfo.OnlineScoreID = sr.ReadInt32(); if (compressedReplay?.Length > 0) {