From e0fde330ef1b7dd8922b82a42d17437d5ebe5413 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 17 Sep 2022 01:38:44 +0300 Subject: [PATCH 1/2] Initial commit --- .editorconfig | 194 +++++++++++++ Pettanko.Tests/CalculatorTests.cs | 48 ++++ Pettanko.Tests/Pettanko.Tests.csproj | 20 ++ Pettanko.sln | 31 +++ .../Calculators/CatchPerformanceCalculator.cs | 82 ++++++ .../Calculators/IPerformanceCalculator.cs | 10 + .../Calculators/ManiaPerformanceCalculator.cs | 111 ++++++++ .../Calculators/OsuPerformanceCalculator.cs | 258 ++++++++++++++++++ .../Calculators/TaikoPerformanceCalculator.cs | 88 ++++++ .../Difficulty/CatchDifficultyAttributes.cs | 14 + Pettanko/Difficulty/DifficultyAttributes.cs | 16 ++ .../Difficulty/ManiaDifficultyAttributes.cs | 14 + .../Difficulty/OsuDifficultyAttributes.cs | 64 +++++ .../Difficulty/TaikoDifficultyAttributes.cs | 12 + Pettanko/Extensions.cs | 26 ++ Pettanko/Mod.cs | 8 + Pettanko/Mods/ModDoubleTime.cs | 8 + Pettanko/Mods/ModEasy.cs | 8 + Pettanko/Mods/ModFlashlight.cs | 8 + Pettanko/Mods/ModHalfTime.cs | 8 + Pettanko/Mods/ModHardRock.cs | 8 + Pettanko/Mods/ModHidden.cs | 8 + Pettanko/Mods/ModNoFail.cs | 8 + Pettanko/Mods/ModPerfect.cs | 8 + Pettanko/Mods/ModRelax.cs | 8 + Pettanko/Mods/Osu/OsuModBlinds.cs | 8 + Pettanko/Mods/Osu/OsuModSpunOut.cs | 8 + Pettanko/Mods/Osu/OsuModTouchDevice.cs | 8 + .../Performance/CatchPerformanceAttributes.cs | 7 + .../Performance/ManiaPerformanceAttributes.cs | 12 + .../Performance/OsuPerformanceAttributes.cs | 16 ++ Pettanko/Performance/PerformanceAttributes.cs | 8 + .../Performance/TaikoPerformanceAttributes.cs | 10 + Pettanko/Pettanko.cs | 27 ++ Pettanko/Pettanko.csproj | 7 + Pettanko/Score.cs | 30 ++ README.md | 13 +- 37 files changed, 1220 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 Pettanko.Tests/CalculatorTests.cs create mode 100644 Pettanko.Tests/Pettanko.Tests.csproj create mode 100644 Pettanko.sln create mode 100644 Pettanko/Calculators/CatchPerformanceCalculator.cs create mode 100644 Pettanko/Calculators/IPerformanceCalculator.cs create mode 100644 Pettanko/Calculators/ManiaPerformanceCalculator.cs create mode 100644 Pettanko/Calculators/OsuPerformanceCalculator.cs create mode 100644 Pettanko/Calculators/TaikoPerformanceCalculator.cs create mode 100644 Pettanko/Difficulty/CatchDifficultyAttributes.cs create mode 100644 Pettanko/Difficulty/DifficultyAttributes.cs create mode 100644 Pettanko/Difficulty/ManiaDifficultyAttributes.cs create mode 100644 Pettanko/Difficulty/OsuDifficultyAttributes.cs create mode 100644 Pettanko/Difficulty/TaikoDifficultyAttributes.cs create mode 100644 Pettanko/Extensions.cs create mode 100644 Pettanko/Mod.cs create mode 100644 Pettanko/Mods/ModDoubleTime.cs create mode 100644 Pettanko/Mods/ModEasy.cs create mode 100644 Pettanko/Mods/ModFlashlight.cs create mode 100644 Pettanko/Mods/ModHalfTime.cs create mode 100644 Pettanko/Mods/ModHardRock.cs create mode 100644 Pettanko/Mods/ModHidden.cs create mode 100644 Pettanko/Mods/ModNoFail.cs create mode 100644 Pettanko/Mods/ModPerfect.cs create mode 100644 Pettanko/Mods/ModRelax.cs create mode 100644 Pettanko/Mods/Osu/OsuModBlinds.cs create mode 100644 Pettanko/Mods/Osu/OsuModSpunOut.cs create mode 100644 Pettanko/Mods/Osu/OsuModTouchDevice.cs create mode 100644 Pettanko/Performance/CatchPerformanceAttributes.cs create mode 100644 Pettanko/Performance/ManiaPerformanceAttributes.cs create mode 100644 Pettanko/Performance/OsuPerformanceAttributes.cs create mode 100644 Pettanko/Performance/PerformanceAttributes.cs create mode 100644 Pettanko/Performance/TaikoPerformanceAttributes.cs create mode 100644 Pettanko/Pettanko.cs create mode 100644 Pettanko/Pettanko.csproj create mode 100644 Pettanko/Score.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..440c24f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,194 @@ +# EditorConfig is awesome: http://editorconfig.org +root = true + +[*.cs] +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +#Roslyn naming styles + +#PascalCase for public and protected members +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.public_members_pascalcase.severity = error +dotnet_naming_rule.public_members_pascalcase.symbols = public_members +dotnet_naming_rule.public_members_pascalcase.style = pascalcase + +#camelCase for private members +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_symbols.private_members.applicable_accessibilities = private +dotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.private_members_camelcase.severity = warning +dotnet_naming_rule.private_members_camelcase.symbols = private_members +dotnet_naming_rule.private_members_camelcase.style = camelcase + +dotnet_naming_symbols.local_function.applicable_kinds = local_function +dotnet_naming_rule.local_function_camelcase.severity = warning +dotnet_naming_rule.local_function_camelcase.symbols = local_function +dotnet_naming_rule.local_function_camelcase.style = camelcase + +#all_lower for private and local constants/static readonlys +dotnet_naming_style.all_lower.capitalization = all_lower +dotnet_naming_style.all_lower.word_separator = _ + +dotnet_naming_symbols.private_constants.applicable_accessibilities = private +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_rule.private_const_all_lower.severity = warning +dotnet_naming_rule.private_const_all_lower.symbols = private_constants +dotnet_naming_rule.private_const_all_lower.style = all_lower + +dotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly.applicable_kinds = field +dotnet_naming_rule.private_static_readonly_all_lower.severity = warning +dotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly +dotnet_naming_rule.private_static_readonly_all_lower.style = all_lower + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.required_modifiers = const +dotnet_naming_rule.local_const_all_lower.severity = warning +dotnet_naming_rule.local_const_all_lower.symbols = local_constants +dotnet_naming_rule.local_const_all_lower.style = all_lower + +#ALL_UPPER for non private constants/static readonlys +dotnet_naming_style.all_upper.capitalization = all_upper +dotnet_naming_style.all_upper.word_separator = _ + +dotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_constants.required_modifiers = const +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_rule.public_const_all_upper.severity = warning +dotnet_naming_rule.public_const_all_upper.symbols = public_constants +dotnet_naming_rule.public_const_all_upper.style = all_upper + +dotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly +dotnet_naming_symbols.public_static_readonly.applicable_kinds = field +dotnet_naming_rule.public_static_readonly_all_upper.severity = warning +dotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly +dotnet_naming_rule.public_static_readonly_all_upper.style = all_upper + +#Roslyn formating options + +#Formatting - indentation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +#Formatting - new line options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_open_brace = all +#csharp_new_line_before_members_in_anonymous_types = true +#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing +csharp_new_line_between_query_expression_clauses = true + +#Formatting - organize using options +dotnet_sort_system_directives_first = true + +#Formatting - spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#Roslyn language styles + +#Style - this. qualification +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +#Style - type names +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_for_built_in_types = true:none +csharp_style_var_elsewhere = true:silent + +#Style - modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +csharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning + +#Style - parentheses +# Skipped because roslyn cannot separate +-*/ with << >> + +#Style - expression bodies +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_local_functions = true:silent + +#Style - expression preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_compound_assignment = true:warning + +#Style - null/type checks +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_throw_expression = true:silent +csharp_style_conditional_delegate_call = true:warning + +#Style - unused +dotnet_style_readonly_field = true:silent +dotnet_code_quality_unused_parameters = non_public:silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#Style - variable declaration +csharp_style_inlined_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = true:warning + +#Style - other C# 7.x features +dotnet_style_prefer_inferred_tuple_names = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent + +#Style - C# 8 features +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_switch_expression = false:none + +#Supressing roslyn built-in analyzers +# Suppress: EC112 + +#Private method is unused +dotnet_diagnostic.IDE0051.severity = silent +#Private member is unused +dotnet_diagnostic.IDE0052.severity = silent + +#Rules for disposable +dotnet_diagnostic.IDE0067.severity = none +dotnet_diagnostic.IDE0068.severity = none +dotnet_diagnostic.IDE0069.severity = none \ No newline at end of file diff --git a/Pettanko.Tests/CalculatorTests.cs b/Pettanko.Tests/CalculatorTests.cs new file mode 100644 index 0000000..9d0063c --- /dev/null +++ b/Pettanko.Tests/CalculatorTests.cs @@ -0,0 +1,48 @@ +using Pettanko.Difficulty; +using Pettanko.Performance; +using Xunit; + +namespace Pettanko.Tests +{ + public class CalculatorTests + { + [Fact] + public void TestOsuCalculator() + { + var result = Pettanko.Calculate( + new OsuDifficultyAttributes + { + AimDifficulty = 2.9864489908605893, + SpeedDifficulty = 3.1111283473891254, + FlashlightDifficulty = 3.54341969421949, + SliderFactor = 0.99796469139376365, + ApproachRate = 9.50, + OverallDifficulty = 9.00, + MaxCombo = 3220, + HitCircleCount = 1534, + SliderCount = 587, + SpinnerCount = 5 + }, + new Score + { + RulesetId = 0, + Accuracy = 1.0, + MaxCombo = 3220, + Statistics = new Statistics + { + Count300 = 2126 + } + }); + + var osuPerfAttributes = result as OsuPerformanceAttributes; + + Assert.NotNull(osuPerfAttributes); + Assert.Equal(455.90599187959032, osuPerfAttributes.Total); + Assert.Equal(141.45654132294621, osuPerfAttributes.Aim); + Assert.Equal(167.49667236961548, osuPerfAttributes.Speed); + Assert.Equal(140.70696717850745, osuPerfAttributes.Accuracy); + Assert.Equal(0.0000, osuPerfAttributes.Flashlight); + Assert.Equal(0.0000, osuPerfAttributes.EffectiveMissCount); + } + } +} diff --git a/Pettanko.Tests/Pettanko.Tests.csproj b/Pettanko.Tests/Pettanko.Tests.csproj new file mode 100644 index 0000000..0724168 --- /dev/null +++ b/Pettanko.Tests/Pettanko.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/Pettanko.sln b/Pettanko.sln new file mode 100644 index 0000000..5685772 --- /dev/null +++ b/Pettanko.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pettanko", "Pettanko\Pettanko.csproj", "{A5154878-72F7-4E36-9FB7-25B5C9E2B884}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pettanko.Tests", "Pettanko.Tests\Pettanko.Tests.csproj", "{1D2CD22C-6716-4707-8A6E-007F7D1A9FB4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A5154878-72F7-4E36-9FB7-25B5C9E2B884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5154878-72F7-4E36-9FB7-25B5C9E2B884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5154878-72F7-4E36-9FB7-25B5C9E2B884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5154878-72F7-4E36-9FB7-25B5C9E2B884}.Release|Any CPU.Build.0 = Release|Any CPU + {1D2CD22C-6716-4707-8A6E-007F7D1A9FB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D2CD22C-6716-4707-8A6E-007F7D1A9FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D2CD22C-6716-4707-8A6E-007F7D1A9FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D2CD22C-6716-4707-8A6E-007F7D1A9FB4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {73ADA6B4-4B6C-439B-89F6-49B0BC85CE5B} + EndGlobalSection +EndGlobal diff --git a/Pettanko/Calculators/CatchPerformanceCalculator.cs b/Pettanko/Calculators/CatchPerformanceCalculator.cs new file mode 100644 index 0000000..a95107c --- /dev/null +++ b/Pettanko/Calculators/CatchPerformanceCalculator.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using Pettanko.Difficulty; +using Pettanko.Mods; +using Pettanko.Performance; + +namespace Pettanko.Calculators +{ + public class CatchPerformanceCalculator : IPerformanceCalculator + { + private int fruitsHit; + private int ticksHit; + private int tinyTicksHit; + private int tinyTicksMissed; + private int misses; + + public PerformanceAttributes Calculate(DifficultyAttributes difficultyAttributes, Score scoreData) + { + var catchAttributes = (CatchDifficultyAttributes)difficultyAttributes; + + fruitsHit = scoreData.Statistics.Count300; + ticksHit = scoreData.Statistics.Count100; + tinyTicksHit = scoreData.Statistics.Count50; + tinyTicksMissed = scoreData.Statistics.CountKatu; + misses = scoreData.Statistics.CountMiss; + + // We are heavily relying on aim in catch the beat + double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0; + + // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo + int numTotalHits = totalComboHits(); + + double lengthBonus = + 0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) + + (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0); + value *= lengthBonus; + + value *= Math.Pow(0.97, misses); + + // Combo scaling + if (catchAttributes.MaxCombo > 0) + value *= Math.Min(Math.Pow(scoreData.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0); + + double approachRate = catchAttributes.ApproachRate; + double approachRateFactor = 1.0; + if (approachRate > 9.0) + approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 + if (approachRate > 10.0) + approachRateFactor += 0.1 * (approachRate - 10.0); // Additional 10% at AR 11, 30% total + else if (approachRate < 8.0) + approachRateFactor += 0.025 * (8.0 - approachRate); // 2.5% for each AR below 8 + + value *= approachRateFactor; + + if (scoreData.Mods.Any(m => m is ModHidden)) + { + // Hiddens gives almost nothing on max approach rate, and more the lower it is + if (approachRate <= 10.0) + value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10 + else if (approachRate > 10.0) + value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11 + } + + if (scoreData.Mods.Any(m => m is ModFlashlight)) + value *= 1.35 * lengthBonus; + + value *= Math.Pow(accuracy(), 5.5); + + if (scoreData.Mods.Any(m => m is ModNoFail)) + value *= 0.90; + + return new CatchPerformanceAttributes + { + Total = value + }; + } + private double accuracy() => totalHits() == 0 ? 0 : Extensions.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1); + private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed; + private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit; + private int totalComboHits() => misses + ticksHit + fruitsHit; + } +} diff --git a/Pettanko/Calculators/IPerformanceCalculator.cs b/Pettanko/Calculators/IPerformanceCalculator.cs new file mode 100644 index 0000000..6997b06 --- /dev/null +++ b/Pettanko/Calculators/IPerformanceCalculator.cs @@ -0,0 +1,10 @@ +using Pettanko.Difficulty; +using Pettanko.Performance; + +namespace Pettanko.Calculators +{ + public interface IPerformanceCalculator + { + PerformanceAttributes Calculate(DifficultyAttributes difficultyAttributes, Score scoreData); + } +} diff --git a/Pettanko/Calculators/ManiaPerformanceCalculator.cs b/Pettanko/Calculators/ManiaPerformanceCalculator.cs new file mode 100644 index 0000000..7dd2fbf --- /dev/null +++ b/Pettanko/Calculators/ManiaPerformanceCalculator.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; +using Pettanko.Difficulty; +using Pettanko.Mods; +using Pettanko.Performance; + +namespace Pettanko.Calculators +{ + public class ManiaPerformanceCalculator : IPerformanceCalculator + { + // Score after being scaled by non-difficulty-increasing mods + private double scaledScore; + + private int countPerfect; + private int countGreat; + private int countGood; + private int countOk; + private int countMeh; + private int countMiss; + + public PerformanceAttributes Calculate(DifficultyAttributes difficultyAttributes, Score scoreData) + { + var maniaAttributes = (ManiaDifficultyAttributes)difficultyAttributes; + + scaledScore = scoreData.TotalScore; + + // FIXME: this is probably incorrect, fix when mania switches to using hit results + countPerfect = scoreData.Statistics.Count300; + countGreat = scoreData.Statistics.CountGeki; + countGood = scoreData.Statistics.Count100; + countOk = scoreData.Statistics.CountKatu; + countMeh = scoreData.Statistics.Count50; + countMiss = scoreData.Statistics.CountMiss; + + // TODO: + if (scoreData.Mods.Length > 0) + throw new NotImplementedException("Mania mods are not implemented yet!"); + + //IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + //foreach (var m in scoreData.Mods.Where(m => !scoreIncreaseMods.Contains(m))) + // scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; + + // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. + // The specific number has no intrinsic meaning and can be adjusted as needed. + double multiplier = 0.8; + + if (scoreData.Mods.Any(m => m is ModNoFail)) + multiplier *= 0.9; + if (scoreData.Mods.Any(m => m is ModEasy)) + multiplier *= 0.5; + + double difficultyValue = computeDifficultyValue(maniaAttributes); + double accValue = computeAccuracyValue(difficultyValue, maniaAttributes); + double totalValue = + Math.Pow( + Math.Pow(difficultyValue, 1.1) + + Math.Pow(accValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + return new ManiaPerformanceAttributes + { + Difficulty = difficultyValue, + Accuracy = accValue, + ScaledScore = scaledScore, + Total = totalValue + }; + } + private double computeDifficultyValue(ManiaDifficultyAttributes attributes) + { + double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; + + difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + + if (scaledScore <= 500000) + difficultyValue = 0; + else if (scaledScore <= 600000) + difficultyValue *= (scaledScore - 500000) / 100000 * 0.3; + else if (scaledScore <= 700000) + difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25; + else if (scaledScore <= 800000) + difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20; + else if (scaledScore <= 900000) + difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15; + else + difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1; + + return difficultyValue; + } + + private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes) + { + if (attributes.GreatHitWindow <= 0) + return 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667) + * difficultyValue + * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); + + return accuracyValue; + } + + private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; + } +} diff --git a/Pettanko/Calculators/OsuPerformanceCalculator.cs b/Pettanko/Calculators/OsuPerformanceCalculator.cs new file mode 100644 index 0000000..fa09b4c --- /dev/null +++ b/Pettanko/Calculators/OsuPerformanceCalculator.cs @@ -0,0 +1,258 @@ +using System; +using System.Linq; +using Pettanko.Difficulty; +using Pettanko.Mods; +using Pettanko.Mods.Osu; +using Pettanko.Performance; + +namespace Pettanko.Calculators +{ + public class OsuPerformanceCalculator : IPerformanceCalculator + { + private double accuracy; + private int scoreMaxCombo; + private int countGreat; + private int countOk; + private int countMeh; + private int countMiss; + + private double effectiveMissCount; + + public PerformanceAttributes Calculate(DifficultyAttributes difficultyAttributes, Score scoreData) + { + var osuAttributes = (OsuDifficultyAttributes)difficultyAttributes; + + accuracy = scoreData.Accuracy; + scoreMaxCombo = scoreData.MaxCombo; + countGreat = scoreData.Statistics.Count300; + countOk = scoreData.Statistics.Count100; + countMeh = scoreData.Statistics.Count50; + countMiss = scoreData.Statistics.Count50; + effectiveMissCount = calculateEffectiveMissCount(osuAttributes); + + double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + + if (scoreData.Mods.Any(m => m is ModNoFail)) + multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); + + if (scoreData.Mods.Any(m => m is OsuModSpunOut) && totalHits > 0) + multiplier *= 1.0 - Math.Pow((double)osuAttributes.SpinnerCount / totalHits, 0.85); + + if (scoreData.Mods.Any(h => h is ModRelax)) + { + // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. + effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); + + multiplier *= 0.6; + } + + double aimValue = computeAimValue(scoreData, osuAttributes); + double speedValue = computeSpeedValue(scoreData, osuAttributes); + double accuracyValue = computeAccuracyValue(scoreData, osuAttributes); + double flashlightValue = computeFlashlightValue(scoreData, osuAttributes); + double totalValue = + Math.Pow( + Math.Pow(aimValue, 1.1) + + Math.Pow(speedValue, 1.1) + + Math.Pow(accuracyValue, 1.1) + + Math.Pow(flashlightValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + return new OsuPerformanceAttributes + { + Aim = aimValue, + Speed = speedValue, + Accuracy = accuracyValue, + Flashlight = flashlightValue, + EffectiveMissCount = effectiveMissCount, + Total = totalValue + }; + } + + private double computeAimValue(Score score, OsuDifficultyAttributes attributes) + { + double rawAim = attributes.AimDifficulty; + + if (score.Mods.Any(m => m is OsuModTouchDevice)) + rawAim = Math.Pow(rawAim, 0.8); + + double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; + + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); + aimValue *= lengthBonus; + + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. + if (effectiveMissCount > 0) + aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); + + aimValue *= getComboScalingFactor(attributes); + + double approachRateFactor = 0.0; + if (attributes.ApproachRate > 10.33) + approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + else if (attributes.ApproachRate < 8.0) + approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); + + aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + + if (score.Mods.Any(m => m is OsuModBlinds)) + aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); + else if (score.Mods.Any(h => h is ModHidden)) + { + // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. + aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + } + + // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. + double estimateDifficultSliders = attributes.SliderCount * 0.15; + + if (attributes.SliderCount > 0) + { + double estimateSliderEndsDropped = Extensions.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); + double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor; + aimValue *= sliderNerfFactor; + } + + aimValue *= accuracy; + // It is important to consider accuracy difficulty when scaling with accuracy. + aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + + return aimValue; + } + + private double computeSpeedValue(Score score, OsuDifficultyAttributes attributes) + { + double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; + + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); + speedValue *= lengthBonus; + + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. + if (effectiveMissCount > 0) + speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + + speedValue *= getComboScalingFactor(attributes); + + double approachRateFactor = 0.0; + if (attributes.ApproachRate > 10.33) + approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + + speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + + if (score.Mods.Any(m => m is OsuModBlinds)) + { + // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. + speedValue *= 1.12; + } + else if (score.Mods.Any(m => m is ModHidden)) + { + // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. + speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + } + + // Scale the speed value with accuracy and OD. + speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); + + // Scale the speed value with # of 50s to punish doubletapping. + speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); + + return speedValue; + } + + private double computeAccuracyValue(Score score, OsuDifficultyAttributes attributes) + { + if (score.Mods.Any(h => h is ModRelax)) + return 0.0; + + // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. + double betterAccuracyPercentage; + int amountHitObjectsWithAccuracy = attributes.HitCircleCount; + + if (amountHitObjectsWithAccuracy > 0) + betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + else + betterAccuracyPercentage = 0; + + // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points. + if (betterAccuracyPercentage < 0) + betterAccuracyPercentage = 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. + double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. + accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); + + // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. + if (score.Mods.Any(m => m is OsuModBlinds)) + accuracyValue *= 1.14; + else if (score.Mods.Any(m => m is ModHidden)) + accuracyValue *= 1.08; + + if (score.Mods.Any(m => m is ModFlashlight)) + accuracyValue *= 1.02; + + return accuracyValue; + } + + private double computeFlashlightValue(Score score, OsuDifficultyAttributes attributes) + { + if (!score.Mods.Any(h => h is ModFlashlight)) + return 0.0; + + double rawFlashlight = attributes.FlashlightDifficulty; + + if (score.Mods.Any(m => m is OsuModTouchDevice)) + rawFlashlight = Math.Pow(rawFlashlight, 0.8); + + double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0; + + // Add an additional bonus for HDFL. + if (score.Mods.Any(h => h is ModHidden)) + flashlightValue *= 1.3; + + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. + if (effectiveMissCount > 0) + flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + + // Combo scaling. + if (attributes.MaxCombo > 0) + flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + + // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. + flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + + (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); + + // Scale the flashlight value with accuracy _slightly_. + flashlightValue *= 0.5 + accuracy / 2.0; + // It is important to also consider accuracy difficulty when doing that. + flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + + return flashlightValue; + } + + private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes) + { + // Guess the number of misses + slider breaks from combo + double comboBasedMissCount = 0.0; + + if (attributes.SliderCount > 0) + { + double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount; + if (scoreMaxCombo < fullComboThreshold) + comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); + } + + // Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations + comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); + + return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); + } + + private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + private int totalHits => countGreat + countOk + countMeh + countMiss; + } +} diff --git a/Pettanko/Calculators/TaikoPerformanceCalculator.cs b/Pettanko/Calculators/TaikoPerformanceCalculator.cs new file mode 100644 index 0000000..fe4ed17 --- /dev/null +++ b/Pettanko/Calculators/TaikoPerformanceCalculator.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using Pettanko.Difficulty; +using Pettanko.Mods; +using Pettanko.Performance; + +namespace Pettanko.Calculators +{ + public class TaikoPerformanceCalculator : IPerformanceCalculator + { + private int countGreat; + private int countOk; + private int countMeh; + private int countMiss; + + public PerformanceAttributes Calculate(DifficultyAttributes difficultyAttributes, Score scoreData) + { + var taikoAttributes = (TaikoDifficultyAttributes)difficultyAttributes; + + countGreat = scoreData.Statistics.Count300; + countOk = scoreData.Statistics.Count100; + countMeh = scoreData.Statistics.Count50; + countMiss = scoreData.Statistics.Count50; + + + // Custom multipliers for NoFail and SpunOut. + double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (scoreData.Mods.Any(m => m is ModNoFail)) + multiplier *= 0.90; + + if (scoreData.Mods.Any(m => m is ModHidden)) + multiplier *= 1.10; + + double difficultyValue = computeDifficultyValue(scoreData, taikoAttributes); + double accuracyValue = computeAccuracyValue(scoreData, taikoAttributes); + double totalValue = + Math.Pow( + Math.Pow(difficultyValue, 1.1) + + Math.Pow(accuracyValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + return new TaikoPerformanceAttributes + { + Difficulty = difficultyValue, + Accuracy = accuracyValue, + Total = totalValue + }; + } + + private double computeDifficultyValue(Score score, TaikoDifficultyAttributes attributes) + { + double strainValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; + + // Longer maps are worth more + double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + strainValue *= lengthBonus; + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + strainValue *= Math.Pow(0.985, countMiss); + + if (score.Mods.Any(m => m is ModHidden)) + strainValue *= 1.025; + + if (score.Mods.Any(m => m is ModFlashlight)) + // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. + strainValue *= 1.05 * lengthBonus; + + // Scale the speed value with accuracy _slightly_ + return strainValue * score.Accuracy; + } + + private double computeAccuracyValue(Score score, TaikoDifficultyAttributes attributes) + { + if (attributes.GreatHitWindow <= 0) + return 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + } + + private int totalHits => countGreat + countOk + countMeh + countMiss; + } +} diff --git a/Pettanko/Difficulty/CatchDifficultyAttributes.cs b/Pettanko/Difficulty/CatchDifficultyAttributes.cs new file mode 100644 index 0000000..c08d5a3 --- /dev/null +++ b/Pettanko/Difficulty/CatchDifficultyAttributes.cs @@ -0,0 +1,14 @@ + +namespace Pettanko.Difficulty +{ + public class CatchDifficultyAttributes : DifficultyAttributes + { + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// + public double ApproachRate { get; set; } + } +} diff --git a/Pettanko/Difficulty/DifficultyAttributes.cs b/Pettanko/Difficulty/DifficultyAttributes.cs new file mode 100644 index 0000000..b25d3b7 --- /dev/null +++ b/Pettanko/Difficulty/DifficultyAttributes.cs @@ -0,0 +1,16 @@ + +namespace Pettanko.Difficulty +{ + public class DifficultyAttributes + { + /// + /// The combined star rating of all skills. + /// + public double StarRating { get; set; } + + /// + /// The maximum achievable combo. + /// + public int MaxCombo { get; set; } + } +} diff --git a/Pettanko/Difficulty/ManiaDifficultyAttributes.cs b/Pettanko/Difficulty/ManiaDifficultyAttributes.cs new file mode 100644 index 0000000..777e027 --- /dev/null +++ b/Pettanko/Difficulty/ManiaDifficultyAttributes.cs @@ -0,0 +1,14 @@ + +namespace Pettanko.Difficulty +{ + public class ManiaDifficultyAttributes : DifficultyAttributes + { + /// + /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods do not affect the hit window at all in osu-stable. + /// + public double GreatHitWindow { get; set; } + } +} diff --git a/Pettanko/Difficulty/OsuDifficultyAttributes.cs b/Pettanko/Difficulty/OsuDifficultyAttributes.cs new file mode 100644 index 0000000..d083404 --- /dev/null +++ b/Pettanko/Difficulty/OsuDifficultyAttributes.cs @@ -0,0 +1,64 @@ + +namespace Pettanko.Difficulty +{ + public class OsuDifficultyAttributes : DifficultyAttributes + { + /// + /// The difficulty corresponding to the aim skill. + /// + public double AimDifficulty { get; set; } + + /// + /// The difficulty corresponding to the speed skill. + /// + public double SpeedDifficulty { get; set; } + + /// + /// The difficulty corresponding to the flashlight skill. + /// + public double FlashlightDifficulty { get; set; } + + /// + /// Describes how much of is contributed to by hitcircles or sliders. + /// A value closer to 1.0 indicates most of is contributed by hitcircles. + /// A value closer to 0.0 indicates most of is contributed by sliders. + /// + public double SliderFactor { get; set; } + + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// + public double ApproachRate { get; set; } + + /// + /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// + public double OverallDifficulty { get; set; } + + /// + /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. + /// + public double DrainRate { get; set; } + + /// + /// The number of hitcircles in the beatmap. + /// + public int HitCircleCount { get; set; } + + /// + /// The number of sliders in the beatmap. + /// + public int SliderCount { get; set; } + + /// + /// The number of spinners in the beatmap. + /// + public int SpinnerCount { get; set; } + } +} diff --git a/Pettanko/Difficulty/TaikoDifficultyAttributes.cs b/Pettanko/Difficulty/TaikoDifficultyAttributes.cs new file mode 100644 index 0000000..5e3ddb7 --- /dev/null +++ b/Pettanko/Difficulty/TaikoDifficultyAttributes.cs @@ -0,0 +1,12 @@ + +namespace Pettanko.Difficulty +{ + public class TaikoDifficultyAttributes : DifficultyAttributes + { + public double StaminaStrain { get; set; } + public double RhythmStrain { get; set; } + public double ColourStrain { get; set; } + public double ApproachRate { get; set; } + public double GreatHitWindow { get; set; } + } +} diff --git a/Pettanko/Extensions.cs b/Pettanko/Extensions.cs new file mode 100644 index 0000000..7567332 --- /dev/null +++ b/Pettanko/Extensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Pettanko +{ + internal static class Extensions + { + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) + { + if (dictionary == null) + throw new ArgumentNullException(nameof(dictionary)); + + return !dictionary.TryGetValue(key, out var obj) ? default : obj; + } + + public static int Clamp(int value, int min, int max) + { + return Math.Max(Math.Min(value, max), min); + } + + public static double Clamp(double value, double min, double max) + { + return Math.Max(Math.Min(value, max), min); + } + } +} diff --git a/Pettanko/Mod.cs b/Pettanko/Mod.cs new file mode 100644 index 0000000..e655027 --- /dev/null +++ b/Pettanko/Mod.cs @@ -0,0 +1,8 @@ + +namespace Pettanko +{ + public abstract class Mod + { + public abstract string Acronym { get; } + } +} diff --git a/Pettanko/Mods/ModDoubleTime.cs b/Pettanko/Mods/ModDoubleTime.cs new file mode 100644 index 0000000..becb52e --- /dev/null +++ b/Pettanko/Mods/ModDoubleTime.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModDoubleTime : Mod + { + public override string Acronym => "DT"; + } +} diff --git a/Pettanko/Mods/ModEasy.cs b/Pettanko/Mods/ModEasy.cs new file mode 100644 index 0000000..3c6a768 --- /dev/null +++ b/Pettanko/Mods/ModEasy.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModEasy : Mod + { + public override string Acronym => "EZ"; + } +} diff --git a/Pettanko/Mods/ModFlashlight.cs b/Pettanko/Mods/ModFlashlight.cs new file mode 100644 index 0000000..1b95302 --- /dev/null +++ b/Pettanko/Mods/ModFlashlight.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModFlashlight : Mod + { + public override string Acronym => "FL"; + } +} diff --git a/Pettanko/Mods/ModHalfTime.cs b/Pettanko/Mods/ModHalfTime.cs new file mode 100644 index 0000000..efb50c5 --- /dev/null +++ b/Pettanko/Mods/ModHalfTime.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModHalfTime : Mod + { + public override string Acronym => "HT"; + } +} diff --git a/Pettanko/Mods/ModHardRock.cs b/Pettanko/Mods/ModHardRock.cs new file mode 100644 index 0000000..3746f06 --- /dev/null +++ b/Pettanko/Mods/ModHardRock.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModHardRock : Mod + { + public override string Acronym => "HR"; + } +} diff --git a/Pettanko/Mods/ModHidden.cs b/Pettanko/Mods/ModHidden.cs new file mode 100644 index 0000000..ccf45f5 --- /dev/null +++ b/Pettanko/Mods/ModHidden.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModHidden : Mod + { + public override string Acronym => "HD"; + } +} diff --git a/Pettanko/Mods/ModNoFail.cs b/Pettanko/Mods/ModNoFail.cs new file mode 100644 index 0000000..0133779 --- /dev/null +++ b/Pettanko/Mods/ModNoFail.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModNoFail : Mod + { + public override string Acronym => "NF"; + } +} diff --git a/Pettanko/Mods/ModPerfect.cs b/Pettanko/Mods/ModPerfect.cs new file mode 100644 index 0000000..ba53001 --- /dev/null +++ b/Pettanko/Mods/ModPerfect.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModPerfect : Mod + { + public override string Acronym => "PF"; + } +} diff --git a/Pettanko/Mods/ModRelax.cs b/Pettanko/Mods/ModRelax.cs new file mode 100644 index 0000000..f3541e3 --- /dev/null +++ b/Pettanko/Mods/ModRelax.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods +{ + public class ModRelax : Mod + { + public override string Acronym => "RX"; + } +} diff --git a/Pettanko/Mods/Osu/OsuModBlinds.cs b/Pettanko/Mods/Osu/OsuModBlinds.cs new file mode 100644 index 0000000..f5c5c42 --- /dev/null +++ b/Pettanko/Mods/Osu/OsuModBlinds.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods.Osu +{ + public class OsuModBlinds : Mod + { + public override string Acronym => "BL"; + } +} diff --git a/Pettanko/Mods/Osu/OsuModSpunOut.cs b/Pettanko/Mods/Osu/OsuModSpunOut.cs new file mode 100644 index 0000000..438df17 --- /dev/null +++ b/Pettanko/Mods/Osu/OsuModSpunOut.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods.Osu +{ + public class OsuModSpunOut : Mod + { + public override string Acronym => "SO"; + } +} diff --git a/Pettanko/Mods/Osu/OsuModTouchDevice.cs b/Pettanko/Mods/Osu/OsuModTouchDevice.cs new file mode 100644 index 0000000..464b80d --- /dev/null +++ b/Pettanko/Mods/Osu/OsuModTouchDevice.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Mods.Osu +{ + public class OsuModTouchDevice : Mod + { + public override string Acronym => "TD"; + } +} diff --git a/Pettanko/Performance/CatchPerformanceAttributes.cs b/Pettanko/Performance/CatchPerformanceAttributes.cs new file mode 100644 index 0000000..951868a --- /dev/null +++ b/Pettanko/Performance/CatchPerformanceAttributes.cs @@ -0,0 +1,7 @@ + +namespace Pettanko.Performance +{ + public class CatchPerformanceAttributes : PerformanceAttributes + { + } +} diff --git a/Pettanko/Performance/ManiaPerformanceAttributes.cs b/Pettanko/Performance/ManiaPerformanceAttributes.cs new file mode 100644 index 0000000..2ff0447 --- /dev/null +++ b/Pettanko/Performance/ManiaPerformanceAttributes.cs @@ -0,0 +1,12 @@ + +namespace Pettanko.Performance +{ + public class ManiaPerformanceAttributes : PerformanceAttributes + { + public double Difficulty { get; set; } + + public double Accuracy { get; set; } + + public double ScaledScore { get; set; } + } +} diff --git a/Pettanko/Performance/OsuPerformanceAttributes.cs b/Pettanko/Performance/OsuPerformanceAttributes.cs new file mode 100644 index 0000000..10a08b8 --- /dev/null +++ b/Pettanko/Performance/OsuPerformanceAttributes.cs @@ -0,0 +1,16 @@ + +namespace Pettanko.Performance +{ + public class OsuPerformanceAttributes : PerformanceAttributes + { + public double Aim { get; set; } + + public double Speed { get; set; } + + public double Accuracy { get; set; } + + public double Flashlight { get; set; } + + public double EffectiveMissCount { get; set; } + } +} diff --git a/Pettanko/Performance/PerformanceAttributes.cs b/Pettanko/Performance/PerformanceAttributes.cs new file mode 100644 index 0000000..e835528 --- /dev/null +++ b/Pettanko/Performance/PerformanceAttributes.cs @@ -0,0 +1,8 @@ + +namespace Pettanko.Performance +{ + public class PerformanceAttributes + { + public double Total { get; set; } + } +} diff --git a/Pettanko/Performance/TaikoPerformanceAttributes.cs b/Pettanko/Performance/TaikoPerformanceAttributes.cs new file mode 100644 index 0000000..195bfe2 --- /dev/null +++ b/Pettanko/Performance/TaikoPerformanceAttributes.cs @@ -0,0 +1,10 @@ + +namespace Pettanko.Performance +{ + public class TaikoPerformanceAttributes : PerformanceAttributes + { + public double Difficulty { get; set; } + + public double Accuracy { get; set; } + } +} diff --git a/Pettanko/Pettanko.cs b/Pettanko/Pettanko.cs new file mode 100644 index 0000000..5167315 --- /dev/null +++ b/Pettanko/Pettanko.cs @@ -0,0 +1,27 @@ +using System; +using Pettanko.Calculators; +using Pettanko.Difficulty; +using Pettanko.Performance; + +namespace Pettanko +{ + public static class Pettanko + { + public static PerformanceAttributes Calculate(DifficultyAttributes difficultyAttributes, Score scoreData) + { + switch (scoreData.RulesetId) + { + case 0: + return new OsuPerformanceCalculator().Calculate(difficultyAttributes, scoreData); + case 1: + return new TaikoPerformanceCalculator().Calculate(difficultyAttributes, scoreData); + case 2: + return new CatchPerformanceCalculator().Calculate(difficultyAttributes, scoreData); + case 3: + return new ManiaPerformanceCalculator().Calculate(difficultyAttributes, scoreData); + default: + throw new ArgumentException("Ruleset is not supported"); + } + } + } +} diff --git a/Pettanko/Pettanko.csproj b/Pettanko/Pettanko.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/Pettanko/Pettanko.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/Pettanko/Score.cs b/Pettanko/Score.cs new file mode 100644 index 0000000..8e2a5fc --- /dev/null +++ b/Pettanko/Score.cs @@ -0,0 +1,30 @@ + +using System; + +namespace Pettanko +{ + public class Score + { + public int RulesetId { get; set; } + + public long TotalScore { get; set; } + + public int MaxCombo { get; set; } + + public double Accuracy { get; set; } + + public Statistics Statistics { get; set; } + + public Mod[] Mods { get; set; } = Array.Empty(); + } + + public class Statistics + { + public int Count300 { get; set; } + public int CountGeki { get; set; } + public int Count100 { get; set; } + public int CountKatu { get; set; } + public int Count50 { get; set; } + public int CountMiss { get; set; } + } +} diff --git a/README.md b/README.md index f85ef32..dea18a8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# Pettanko -Stripped down version of osu! performance calculators +# Pettanko - stripped down version of osu! performance calculators. + +Pettanko is because you can't name something related to pp without using some kind of japanese boobs word. And also because it's *slim*. + +## How to use + +1) Add it to your project +2) Get beatmap difficulty attributes [from osu! API v2](https://osu.ppy.sh/docs/index.html#get-beatmap-attributes) +3) Call `Pettanko.Calculate(DifficultyAttributes difficultyAttributes, Score scoreData)` + +Calculators don't need anything other than difficulty attributes and score data - no beatmaps, slow parsing or anything else. \ No newline at end of file From 69259fac1581ebe36ef996bf2e14e982b9b5f7d6 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 17 Sep 2022 01:49:17 +0300 Subject: [PATCH 2/2] Setup nuget, licenses --- .../Calculators/CatchPerformanceCalculator.cs | 4 +++- .../Calculators/ManiaPerformanceCalculator.cs | 4 +++- .../Calculators/OsuPerformanceCalculator.cs | 4 +++- .../Calculators/TaikoPerformanceCalculator.cs | 4 +++- Pettanko/Pettanko.csproj | 19 ++++++++++++++++--- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Pettanko/Calculators/CatchPerformanceCalculator.cs b/Pettanko/Calculators/CatchPerformanceCalculator.cs index a95107c..e136e76 100644 --- a/Pettanko/Calculators/CatchPerformanceCalculator.cs +++ b/Pettanko/Calculators/CatchPerformanceCalculator.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + +using System; using System.Linq; using Pettanko.Difficulty; using Pettanko.Mods; diff --git a/Pettanko/Calculators/ManiaPerformanceCalculator.cs b/Pettanko/Calculators/ManiaPerformanceCalculator.cs index 7dd2fbf..f468952 100644 --- a/Pettanko/Calculators/ManiaPerformanceCalculator.cs +++ b/Pettanko/Calculators/ManiaPerformanceCalculator.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + +using System; using System.Linq; using Pettanko.Difficulty; using Pettanko.Mods; diff --git a/Pettanko/Calculators/OsuPerformanceCalculator.cs b/Pettanko/Calculators/OsuPerformanceCalculator.cs index fa09b4c..bb78e31 100644 --- a/Pettanko/Calculators/OsuPerformanceCalculator.cs +++ b/Pettanko/Calculators/OsuPerformanceCalculator.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + +using System; using System.Linq; using Pettanko.Difficulty; using Pettanko.Mods; diff --git a/Pettanko/Calculators/TaikoPerformanceCalculator.cs b/Pettanko/Calculators/TaikoPerformanceCalculator.cs index fe4ed17..e13351d 100644 --- a/Pettanko/Calculators/TaikoPerformanceCalculator.cs +++ b/Pettanko/Calculators/TaikoPerformanceCalculator.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + +using System; using System.Linq; using Pettanko.Difficulty; using Pettanko.Mods; diff --git a/Pettanko/Pettanko.csproj b/Pettanko/Pettanko.csproj index 9f5c4f4..9dd5638 100644 --- a/Pettanko/Pettanko.csproj +++ b/Pettanko/Pettanko.csproj @@ -1,7 +1,20 @@ - - netstandard2.0 - + + netstandard2.0 + + + + Pettanko + StanR + Stripped down version of osu! performance calculators + StanR.Pettanko + true + osu! osu pp + https://github.com/stanriders/Pettanko + Git + true + 1.0.0 +