protected override void RunAllRules(List <HitObjectBase> hitObjects) { BeatmapBase Beatmap = OsuHelper.GetCurrentBeatmap(); // Mods are not yet supported. TODO // Fill our custom tpHitObject class, that carries additional information tpHitObjects = new List <tpHitObject>(hitObjects.Count); float CircleRadius = (PLAYFIELD_WIDTH / 16.0f) * (1.0f - 0.7f * ((float)Beatmap.DifficultyCircleSize - 5.0f) / 5.0f); foreach (HitObjectBase hitObject in hitObjects) { tpHitObjects.Add(new tpHitObject(hitObject, CircleRadius)); } // Sort tpHitObjects by StartTime of the HitObjects - just to make sure. Not using CompareTo, since it results in a crash (HitObjectBase inherits MarshalByRefObject) tpHitObjects.Sort((a, b) => a.BaseHitObject.StartTime - b.BaseHitObject.StartTime); if (CalculateStrainValues() == false) { Reports.Add(new AiReport(Severity.Error, "Could not compute strain values. Aborting difficulty calculation.")); return; } double SpeedDifficulty = CalculateDifficulty(DifficultyType.Speed); double AimDifficulty = CalculateDifficulty(DifficultyType.Aim); // OverallDifficulty is not considered in this algorithm and neither is HpDrainRate. That means, that in this form the algorithm determines how hard it physically is // to play the map, assuming, that too much of an error will not lead to a death. // It might be desirable to include OverallDifficulty into map difficulty, but in my personal opinion it belongs more to the weighting of the actual peformance // and is superfluous in the beatmap difficulty rating. // If it were to be considered, then I would look at the hit window of normal HitCircles only, since Sliders and Spinners are (almost) "free" 300s and take map length // into account as well. Reports.Add(new AiReport(Severity.Info, "Speed difficulty: " + SpeedDifficulty + " | Aim difficulty: " + AimDifficulty)); // The difficulty can be scaled by any desired metric. // In osu!tp it gets squared to account for the rapid increase in difficulty as the limit of a human is approached. (Of course it also gets scaled afterwards.) // It would not be suitable for a star rating, therefore: // The following is a proposal to forge a star rating from 0 to 5. It consists of taking the square root of the difficulty, since by simply scaling the easier // 5-star maps would end up with one star. double SpeedStars = Math.Sqrt(SpeedDifficulty) * STAR_SCALING_FACTOR; double AimStars = Math.Sqrt(AimDifficulty) * STAR_SCALING_FACTOR; Reports.Add(new AiReport(Severity.Info, "Speed stars: " + SpeedStars + " | Aim stars: " + AimStars)); // Again, from own observations and from the general opinion of the community a map with high speed and low aim (or vice versa) difficulty is harder, // than a map with mediocre difficulty in both. Therefore we can not just add both difficulties together, but will introduce a scaling that favors extremes. double StarRating = SpeedStars + AimStars + Math.Abs(SpeedStars - AimStars) * EXTREME_SCALING_FACTOR; // Another approach to this would be taking Speed and Aim separately to a chosen power, which again would be equivalent. This would be more convenient if // the hit window size is to be considered as well. // Note: The star rating is tuned extremely tight! Airman (/b/104229) and Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars. // Expect the easier kind of maps that officially get 5 stars to obtain around 2 by this metric. The tutorial still scores about half a star. // Tune by yourself as you please. ;) Reports.Add(new AiReport(Severity.Info, "Total star rating: " + StarRating)); }
public ppData Calculate() { var ruleset = OsuHelper.GetRulesetFromID(gamemode); Mod[] mods = ruleset.ConvertLegacyMods((LegacyMods)score.EnabledMods).ToArray(); double objectCount; double hitCount = score.CountGeki + score.Count300 + score.Count100 + score.CountKatu + score.Count50 + score.CountMiss; if (gamemode == 1 || gamemode == 2) { objectCount = score.CountGeki + score.Count300 + score.Count100 + score.CountKatu + score.Count50 + score.CountMiss; } else { objectCount = beatmap.CountNormal + beatmap.CountSlider + beatmap.CountSpinner; } double hitMultiplier = objectCount / hitCount; var workingBeatmap = new ProcessorWorkingBeatmap(beatmap.BeatmapId.ToString()); workingBeatmap.BeatmapInfo.Ruleset = ruleset.RulesetInfo; var result = new ppData(); if (score.PP == 0) { var parsedScore = new ProcessorScoreParser(workingBeatmap).Parse(new ScoreInfo { Ruleset = ruleset.RulesetInfo, MaxCombo = score.MaxCombo, TotalScore = score.Score, Mods = mods, Accuracy = OsuHelper.CalculateAccuracy(score, gamemode), Statistics = new Dictionary <HitResult, int> { { HitResult.Perfect, score.CountGeki }, { HitResult.Great, score.Count300 }, { HitResult.Good, score.Count100 }, { HitResult.Ok, score.CountKatu }, { HitResult.Meh, score.Count50 }, { HitResult.Miss, score.CountMiss } } }); if (gamemode == 2) { parsedScore.ScoreInfo.Statistics[HitResult.Perfect] = score.Count300; parsedScore.ScoreInfo.Statistics[HitResult.Great] = score.CountGeki; } else if (gamemode == 3) { parsedScore.ScoreInfo.Statistics[HitResult.Good] = score.CountKatu; parsedScore.ScoreInfo.Statistics[HitResult.Ok] = score.Count100; } result.AchievedPp = ruleset.CreatePerformanceCalculator(workingBeatmap, parsedScore.ScoreInfo).Calculate(); } if (!score.Perfect && gamemode != 1 && gamemode != 3) { var fullComboScore = new ProcessorScoreParser(workingBeatmap).Parse(new ScoreInfo { Ruleset = ruleset.RulesetInfo, MaxCombo = beatmap.MaxCombo, TotalScore = score.Score, Mods = mods, Accuracy = OsuHelper.CalculateAccuracy(score, gamemode), Statistics = new Dictionary <HitResult, int> { { HitResult.Perfect, (int)Math.Round(hitMultiplier * score.CountGeki) }, { HitResult.Great, (int)Math.Round(hitMultiplier * score.Count300) }, { HitResult.Good, (int)Math.Round(hitMultiplier * score.Count100) }, { HitResult.Ok, (int)Math.Round(hitMultiplier * score.CountKatu) }, { HitResult.Meh, (int)Math.Round(hitMultiplier * score.Count50 + score.CountMiss) }, { HitResult.Miss, 0 } } }); if (gamemode == 2) { fullComboScore.ScoreInfo.Statistics[HitResult.Perfect] = (int)Math.Round(hitMultiplier * score.Count300); fullComboScore.ScoreInfo.Statistics[HitResult.Great] = (int)Math.Round(hitMultiplier * score.CountGeki); } else if (gamemode == 3) { fullComboScore.ScoreInfo.Statistics[HitResult.Good] = (int)Math.Round(hitMultiplier * score.CountKatu); fullComboScore.ScoreInfo.Statistics[HitResult.Ok] = (int)Math.Round(hitMultiplier * score.Count100); } result.FullComboPp = ruleset.CreatePerformanceCalculator(workingBeatmap, fullComboScore.ScoreInfo).Calculate(); } if (OsuHelper.CalculateAccuracy(score, gamemode) != 1) { var ssScore = new ProcessorScoreParser(workingBeatmap).Parse(new ScoreInfo { Ruleset = ruleset.RulesetInfo, MaxCombo = beatmap.MaxCombo, Mods = mods, Accuracy = 1, Statistics = new Dictionary <HitResult, int> { { HitResult.Perfect, 0 }, { HitResult.Great, (int)objectCount }, { HitResult.Good, 0 }, { HitResult.Ok, 0 }, { HitResult.Meh, 0 }, { HitResult.Miss, 0 } } }); if (gamemode == 2) { ssScore.ScoreInfo.Statistics[HitResult.Perfect] = score.Count300 + score.CountMiss; ssScore.ScoreInfo.Statistics[HitResult.Great] = 0; } else if (gamemode == 3) { ssScore.ScoreInfo.Statistics[HitResult.Perfect] = (int)hitCount; ssScore.ScoreInfo.Statistics[HitResult.Great] = 0; ssScore.ScoreInfo.TotalScore = 1000000; } result.ssPp = ruleset.CreatePerformanceCalculator(workingBeatmap, ssScore.ScoreInfo).Calculate(); } return(result); }