├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── master.yml │ └── pr.yml ├── .gitignore ├── HitScoreVisualizer.sln ├── HitScoreVisualizer ├── Compile │ ├── CompilerFeatureRequiredAttribute.cs │ ├── IsExternalInit.cs │ └── RequiredMemberAttribute.cs ├── Components │ ├── HsvFlyingEffect.cs │ └── HsvFlyingEffectSpawner.cs ├── Config-Documentation.md ├── Directory.Build.props ├── HarmonyPatches │ ├── BadNoteCutEffectSpawnerPatch.cs │ ├── EffectPoolsManualInstallerPatch.cs │ ├── FlyingScoreEffectPatch.cs │ ├── GameCoreInstallerHook.cs │ ├── HarmonyPatchManager.cs │ ├── MissedNoteEffectSpawnerPatch.cs │ ├── PersistentScoreEffectPatch.cs │ └── TranspilerHelper.cs ├── HitScoreVisualizer.csproj ├── Installers │ ├── HsvAppInstaller.cs │ ├── HsvMenuInstaller.cs │ └── HsvPlayerInstaller.cs ├── Models │ ├── BadCutDisplay.cs │ ├── BadCutDisplayType.cs │ ├── BasicScoreData.cs │ ├── ChainHeadJudgment.cs │ ├── ChainLinkDisplay.cs │ ├── ConfigInfo.cs │ ├── ConfigMigrations │ │ ├── ConfigMigration200.cs │ │ ├── ConfigMigration210.cs │ │ ├── ConfigMigration223.cs │ │ ├── ConfigMigration320.cs │ │ ├── ConfigMigration340.cs │ │ ├── ConfigMigration360.cs │ │ └── IHsvConfigMigration.cs │ ├── ConfigState.cs │ ├── ConfigValidations │ │ ├── IConfigValidation.cs │ │ ├── JudgmentSegmentsValidation.cs │ │ ├── JudgmentsValidation.cs │ │ ├── TimeDependenceDecimalValidation.cs │ │ └── TimeDependenceJudgmentsValidation.cs │ ├── Direction.cs │ ├── DisplayMode.cs │ ├── FlyingTextEffectAnimationData.cs │ ├── HsvConfigModel.cs │ ├── HsvFontType.cs │ ├── IJudgment.cs │ ├── JudgmentDetails.cs │ ├── JudgmentSegment.cs │ ├── MissDisplay.cs │ ├── NormalJudgment.cs │ └── TimeDependenceJudgmentSegment.cs ├── Plugin.cs ├── PluginConfig.cs ├── UI │ ├── ConfigPreviewAnimatedTab.cs │ ├── ConfigPreviewCustomTab.cs │ ├── ConfigPreviewGridTab.cs │ ├── ConfigPreviewViewController.cs │ ├── ConfigSelectorViewController.cs │ ├── HsvMainFlowCoordinator.cs │ ├── IPreviewTextEffectDidFinishEvent.cs │ ├── MenuButtonManager.cs │ ├── PluginSettingsViewController.cs │ ├── PreviewGridText.cs │ ├── PreviewTextEffect.cs │ └── Views │ │ ├── ConfigPreview.bsml │ │ ├── ConfigSelector.bsml │ │ └── PluginSettings.bsml └── Utilities │ ├── ArrayPicker.cs │ ├── DummyScores.cs │ ├── Extensions │ ├── ColorExtensions.cs │ ├── ConfigStateExtensions.cs │ ├── DirectionConversion.cs │ ├── HiveVersionExtensions.cs │ ├── HsvConfigExtensions.cs │ ├── HsvConfigValidation.cs │ ├── JudgmentExtensions.cs │ ├── NoteExtensions.cs │ ├── ScoreJudgment.cs │ └── TextMeshExtensions.cs │ ├── FilePathUtils.cs │ ├── ItemRevolver.cs │ ├── Json │ ├── ColorArrayConverter.cs │ ├── HsvConfigContractResolver.cs │ └── Vector3Converter.cs │ ├── MaterialProperties.cs │ └── Services │ ├── BloomFontProvider.cs │ ├── ConfigLoader.cs │ ├── ConfigMigrator.cs │ ├── PluginDirectories.cs │ └── RandomScoreGenerator.cs ├── LICENSE ├── README.md └── changelog.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = tab 7 | tab_width = 4 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | charset = utf-8 11 | max_line_length = 200 12 | insert_final_newline = false 13 | 14 | # ReSharper properties 15 | resharper_csharp_indent_style = tab 16 | resharper_csharp_max_line_length = 200 17 | resharper_html_indent_style = tab 18 | resharper_html_max_line_length = 200 19 | resharper_resx_indent_style = tab 20 | resharper_resx_max_line_length = 200 21 | resharper_use_indent_from_vs = false 22 | resharper_vb_indent_style = tab 23 | resharper_vb_max_line_length = 200 24 | resharper_xmldoc_indent_style = tab 25 | resharper_xmldoc_max_line_length = 200 26 | resharper_xml_indent_style = tab 27 | resharper_xml_max_line_length = 200 28 | 29 | # ReSharper inspection severities 30 | resharper_web_config_module_not_resolved_highlighting = warning 31 | resharper_web_config_type_not_resolved_highlighting = warning 32 | resharper_web_config_wrong_module_highlighting = warning 33 | 34 | # Organize usings 35 | dotnet_sort_system_directives_first = true 36 | dotnet_separate_import_directive_groups = false 37 | 38 | # this. preferences 39 | dotnet_style_qualification_for_field = false:warning 40 | dotnet_style_qualification_for_property = false:warning 41 | dotnet_style_qualification_for_method = false:warning 42 | dotnet_style_qualification_for_event = false:warning 43 | 44 | # Language keywords vs BCL types preferences 45 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 46 | dotnet_style_predefined_type_for_member_access = true:warning 47 | 48 | # Parentheses preferences 49 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent 50 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent 51 | dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:silent 52 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 53 | 54 | # Modifier preferences 55 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning 56 | dotnet_style_readonly_field = true:suggestion 57 | 58 | # Expression-level preferences 59 | dotnet_style_object_initializer = true:suggestion 60 | dotnet_style_collection_initializer = true:suggestion 61 | dotnet_style_explicit_tuple_names = true:suggestion 62 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 63 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 64 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 65 | dotnet_style_prefer_auto_properties = true:suggestion 66 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 67 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 68 | dotnet_style_prefer_compound_assignment = true:suggestion 69 | 70 | # Null-checking preferences 71 | dotnet_style_coalesce_expression = true:suggestion 72 | dotnet_style_null_propagation = true:suggestion 73 | 74 | # Parameter preferences 75 | dotnet_code_quality_unused_parameters = non_public:suggestion 76 | 77 | ############################### 78 | # C# Code Style Rules # 79 | ############################### 80 | 81 | # var preferences 82 | csharp_style_var_for_built_in_types = true:warning 83 | csharp_style_var_when_type_is_apparent = true:warning 84 | csharp_style_var_elsewhere = true:suggestion 85 | 86 | # Expression-bodied members 87 | csharp_style_expression_bodied_methods = false:suggestion 88 | csharp_style_expression_bodied_constructors = false:suggestion 89 | csharp_style_expression_bodied_operators = false:suggestion 90 | csharp_style_expression_bodied_properties = true:suggestion 91 | csharp_style_expression_bodied_indexers = true:suggestion 92 | csharp_style_expression_bodied_accessors = true:suggestion 93 | csharp_style_expression_bodied_lambdas = true:suggestion 94 | csharp_style_expression_bodied_local_functions = false:warning 95 | 96 | # Pattern-matching preferences 97 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 98 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 99 | 100 | # Null-checking preferences 101 | csharp_style_throw_expression = true:suggestion 102 | csharp_style_conditional_delegate_call = true:suggestion 103 | 104 | # Modifier preferences 105 | csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion 106 | 107 | # Expression-level preferences 108 | csharp_prefer_braces = true:warning 109 | csharp_style_deconstructed_variable_declaration = true:suggestion 110 | csharp_prefer_simple_default_expression = true:warning 111 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 112 | csharp_style_inlined_variable_declaration = true:warning 113 | 114 | ############################### 115 | # C# Formatting Rules # 116 | ############################### 117 | 118 | # New line preferences 119 | csharp_new_line_before_open_brace = all 120 | csharp_new_line_before_else = true 121 | csharp_new_line_before_catch = true 122 | csharp_new_line_before_finally = true 123 | csharp_new_line_before_members_in_object_initializers = true 124 | csharp_new_line_before_members_in_anonymous_types = true 125 | csharp_new_line_between_query_expression_clauses = true 126 | 127 | # Indentation preferences 128 | csharp_indent_case_contents = true 129 | csharp_indent_switch_labels = true 130 | csharp_indent_labels = flush_left 131 | 132 | # Space preferences 133 | csharp_space_after_cast = true 134 | csharp_space_after_keywords_in_control_flow_statements = true 135 | csharp_space_between_method_call_parameter_list_parentheses = false 136 | csharp_space_between_method_declaration_parameter_list_parentheses = false 137 | csharp_space_between_parentheses = false 138 | csharp_space_before_colon_in_inheritance_clause = true 139 | csharp_space_after_colon_in_inheritance_clause = true 140 | csharp_space_around_binary_operators = before_and_after 141 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 142 | csharp_space_between_method_call_name_and_opening_parenthesis = false 143 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 144 | csharp_space_after_comma = true 145 | csharp_space_after_dot = false 146 | 147 | # Wrapping preferences 148 | csharp_preserve_single_line_statements = false 149 | csharp_preserve_single_line_blocks = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | paths: 8 | - 'HitScoreVisualizer.sln' 9 | - 'HitScoreVisualizer/**' 10 | - '.github/workflows/master.yml' 11 | 12 | jobs: 13 | Build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Setup dotnet 18 | uses: actions/setup-dotnet@v1 19 | with: 20 | dotnet-version: 6.0.x 21 | - name: Acquire SIRA References 22 | uses: ProjectSIRA/download-sira-stripped@1.0.0 23 | with: 24 | manifest: ${{github.workspace}}/HitScoreVisualizer/manifest.json 25 | sira-server-code: ${{ secrets.SIRA_SERVER_CODE }} 26 | - name: Download Mod Dependencies 27 | uses: Goobwabber/download-beatmods-deps@1.2 28 | with: 29 | manifest: ${{github.workspace}}/HitScoreVisualizer/manifest.json 30 | - name: Build 31 | id: Build 32 | run: dotnet build --configuration Release 33 | - name: GitStatus 34 | run: git status 35 | - name: Echo Filename 36 | run: echo $BUILDTEXT \($ASSEMBLYNAME\) 37 | env: 38 | BUILDTEXT: Filename=${{ steps.Build.outputs.filename }} 39 | ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }} 40 | - name: Upload Artifact 41 | uses: actions/upload-artifact@v1 42 | with: 43 | name: ${{ steps.Build.outputs.filename }} 44 | path: ${{ steps.Build.outputs.artifactpath }} -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR Build 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths: 7 | - 'HitScoreVisualizer/**' 8 | - '.github/workflows/**.yml' 9 | 10 | jobs: 11 | Build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup dotnet 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 6.0.x 19 | - name: Acquire SIRA References 20 | uses: ProjectSIRA/download-sira-stripped@1.0.0 21 | with: 22 | manifest: ${{github.workspace}}/HitScoreVisualizer/manifest.json 23 | sira-server-code: ${{ secrets.SIRA_SERVER_CODE }} 24 | - name: Download Mod Dependencies 25 | uses: Goobwabber/download-beatmods-deps@1.2 26 | with: 27 | manifest: ${{github.workspace}}/HitScoreVisualizer/manifest.json 28 | - name: Build 29 | id: Build 30 | run: dotnet build --configuration Release 31 | - name: GitStatus 32 | run: git status 33 | - name: Echo Filename 34 | run: echo $BUILDTEXT \($ASSEMBLYNAME\) 35 | env: 36 | BUILDTEXT: Filename=${{ steps.Build.outputs.filename }} 37 | ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }} 38 | - name: Upload Artifact 39 | uses: actions/upload-artifact@v1 40 | with: 41 | name: ${{ steps.Build.outputs.filename }} 42 | path: ${{ steps.Build.outputs.artifactpath }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /HitScoreVisualizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2035 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HitScoreVisualizer", "HitScoreVisualizer\HitScoreVisualizer.csproj", "{7D8A403B-A4EE-42AD-85EF-ACFD7F087A79}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3D499509-8565-4D6B-8395-F23C092A20EA}" 9 | ProjectSection(SolutionItems) = preProject 10 | changelog.md = changelog.md 11 | .editorconfig = .editorconfig 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {7D8A403B-A4EE-42AD-85EF-ACFD7F087A79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {7D8A403B-A4EE-42AD-85EF-ACFD7F087A79}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {7D8A403B-A4EE-42AD-85EF-ACFD7F087A79}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {7D8A403B-A4EE-42AD-85EF-ACFD7F087A79}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(ExtensibilityGlobals) = postSolution 30 | SolutionGuid = {FCB42BAF-E3A9-4B03-B8A5-2538DB0373DF} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /HitScoreVisualizer/Compile/CompilerFeatureRequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace System.Runtime.CompilerServices; 3 | 4 | #pragma warning disable CS9113 // Parameter is unread. 5 | public class CompilerFeatureRequiredAttribute(string name) : Attribute; 6 | #pragma warning restore CS9113 // Parameter is unread. 7 | -------------------------------------------------------------------------------- /HitScoreVisualizer/Compile/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace System.Runtime.CompilerServices; 3 | 4 | internal static class IsExternalInit; -------------------------------------------------------------------------------- /HitScoreVisualizer/Compile/RequiredMemberAttribute.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace System.Runtime.CompilerServices; 3 | 4 | public class RequiredMemberAttribute : Attribute; -------------------------------------------------------------------------------- /HitScoreVisualizer/Components/HsvFlyingEffect.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Models; 2 | using TMPro; 3 | using UnityEngine; 4 | using Zenject; 5 | 6 | namespace HitScoreVisualizer.Components; 7 | 8 | internal class HsvFlyingEffect : FlyingObjectEffect 9 | { 10 | public class Pool : MonoMemoryPool; 11 | 12 | private AnimationCurve fadeAnimationCurve = null!; 13 | 14 | [Inject] 15 | public void Init(FlyingTextEffectAnimationData animationData) 16 | { 17 | fadeAnimationCurve = animationData.Fade; 18 | _moveAnimationCurve = animationData.Move; 19 | } 20 | 21 | [SerializeField] public TextMeshPro? textMesh; 22 | 23 | private Color color; 24 | 25 | public void InitAndPresent(string text, float duration, Vector3 targetPos, Quaternion rotation, Color color, float fontSize, bool shake) 26 | { 27 | if (textMesh == null) 28 | { 29 | return; 30 | } 31 | 32 | this.color = color; 33 | textMesh.text = text; 34 | textMesh.fontSize = fontSize; 35 | InitAndPresent(duration, targetPos, rotation, shake); 36 | } 37 | 38 | public override void ManualUpdate(float t) 39 | { 40 | if (textMesh != null) 41 | { 42 | textMesh.color = color with { a = fadeAnimationCurve.Evaluate(t) }; 43 | } 44 | } 45 | 46 | public static HsvFlyingEffect CreatePrefab() 47 | { 48 | var effect = new GameObject(nameof(HsvFlyingEffect)).AddComponent(); 49 | var textObject = new GameObject("Text") { layer = LayerMask.NameToLayer("UI") }; 50 | effect.textMesh = textObject.AddComponent(); 51 | effect.textMesh.alignment = TextAlignmentOptions.Capline; 52 | effect.textMesh.fontStyle = FontStyles.Bold | FontStyles.Italic; 53 | textObject.transform.SetParent(effect.gameObject.transform, false); 54 | return effect; 55 | } 56 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Components/HsvFlyingEffectSpawner.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using Zenject; 3 | 4 | namespace HitScoreVisualizer.Components; 5 | 6 | internal class HsvFlyingEffectSpawner : MonoBehaviour, IFlyingObjectEffectDidFinishEvent 7 | { 8 | public record InitData(float Duration, float XSpread, float TargetYPos, float TargetZPos, Color Color, float FontSize); 9 | 10 | private float duration; 11 | private float xSpread; 12 | private float targetYPos; 13 | private float targetZPos; 14 | private Color color; 15 | private float fontSize; 16 | private HsvFlyingEffect.Pool missTextEffectPool = null!; 17 | private PluginConfig pluginConfig = null!; 18 | 19 | [Inject] 20 | public void Init(InitData initData, HsvFlyingEffect.Pool effectPool, PluginConfig config) 21 | { 22 | duration = initData.Duration; 23 | xSpread = initData.XSpread; 24 | targetYPos = initData.TargetYPos; 25 | targetZPos = initData.TargetZPos; 26 | color = initData.Color; 27 | fontSize = initData.FontSize; 28 | missTextEffectPool = effectPool; 29 | pluginConfig = config; 30 | } 31 | 32 | public void SpawnText(Vector3 pos, Quaternion rotation, Quaternion inverseRotation, string text, Color? color) 33 | { 34 | var missTextEffect = missTextEffectPool.Spawn(); 35 | missTextEffect.didFinishEvent.Add(this); 36 | missTextEffect.transform.localPosition = pos; 37 | 38 | var targetPos = rotation * new Vector3(Mathf.Sign((inverseRotation * pos).x) * xSpread, targetYPos, targetZPos); 39 | 40 | missTextEffect.InitAndPresent(text, duration, targetPos, rotation, color ?? this.color, fontSize, false); 41 | } 42 | 43 | public void HandleFlyingObjectEffectDidFinish(FlyingObjectEffect flyingObjectEffect) 44 | { 45 | flyingObjectEffect.didFinishEvent.Remove(this); 46 | missTextEffectPool.Despawn((HsvFlyingEffect)flyingObjectEffect); 47 | } 48 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Config-Documentation.md: -------------------------------------------------------------------------------- 1 | ## HitScoreVisualizer config documentation 2 | 3 | Below is a series of descriptions that relate to each value in a HitScoreVisualizer json config 4 | 5 | ### "majorVersion" "minorVersion" "patchVersion" 6 | If the version number (excluding patch version) of the config is higher than that of the plugin, the config will not be loaded. If the version number of the config is lower than that of the plugin, the file will be automatically converted. Conversion is not guaranteed to occur, or be accurate, across major versions 7 | 8 | ### "displayMode" 9 | - If not set, by default, it will display judgment text above score. 10 | - If set to "scoreOnTop", it is like default but with score above. 11 | - If set to "format", displays the judgment text, with the following format specifiers allowed: 12 | - %b: The score contributed by the part of the swing before cutting the block 13 | - %c: The score contributed by the accuracy of the cut 14 | - %a: The score contributed by the part of the swing after cutting the block 15 | - %t: The time dependence of the swing 16 | - %B, %C, %A, %T: As above, except using the appropriate judgment from that part of the swing (as configured for "beforeCutAngleJudgments", "accuracyJudgments", "afterCutAngleJudgments", or "timeDependencyJudgments") 17 | - %d: An arrow that points towards the center line of the note relative to the cut line 18 | - %s: The total score for the cut 19 | - %p: The percent out of 115 you achieved with your swing's score 20 | - %%: A literal percent symbol[README.md](../README.md) 21 | - %n: A newline 22 | - If set to "textOnly", displays only the judgment text. 23 | - If set to "numeric", displays only the note score. 24 | - If set to "directions", displays judgement text and off-direction arrow. 25 | 26 | ### "fixedPosition" 27 | If not null, judgments will appear and stay at rather than moving as normal, this will take priority over TargetPositionOffset. Additionally, the previous judgment will disappear when a new one is created (so there won't be overlap) 28 | 29 | ### "targetPositionOffset" 30 | Will offset the target position of the hitscore fade animation. If a fixed position is defined in the config, that one will take priority over this one and this will be fully ignored 31 | 32 | ### "timeDependencyDecimalPrecision" 33 | Number of decimal places to show time dependence to 34 | 35 | ### "timeDependencyDecimalOffset" 36 | Which power of 10 to multiply the time dependence by 37 | 38 | ### "doIntermediateUpdates" 39 | Disabling this only updates the score number once the note is cut and once the post swing score is done calculating; this slightly improves performance 40 | 41 | ### "assumeMaxPostSwing" 42 | When the note is first cut, should the post swing score show the max rating before it finishes calculating 43 | 44 | ### "judgments" 45 | Order from highest threshold to lowest; the first matching judgment will be applied 46 | 47 | ### "chainHeadJudgments" 48 | Same as normal judgments but for burst sliders aka. chain notes 49 | 50 | ### "chainLinkDisplay" 51 | Text displayed for burst slider segments 52 | 53 | ### "beforeCutAngleJudgments" 54 | Judgments for the part of the swing before cutting the block (score is from 0-70) 55 | 56 | ### "accuracyJudgments" 57 | Judgments for the accuracy of the cut (how close to the center of the block the cut was, score is from 0-15) 58 | 59 | ### "afterCutAngleJudgments" 60 | Judgments for the part of the swing after cutting the block (score is from 0-30) 61 | 62 | ### "timeDependencyJudgments" 63 | Judgments for time dependence (score is from 0-1) 64 | 65 | ### "badCutDisplays" 66 | What to show when achieving a bad cut, either through hitting a note in the wrong direction or with the wrong color saber, or by hitting a bomb 67 | 68 | ### "randomizeBadCutDisplays" 69 | Whether to randomize the order in which bad cut displays appear 70 | 71 | ### "missDisplays" 72 | What to show missing a note 73 | 74 | ### "randomizeMissDisplays" 75 | Whether to randomize the order in which miss displays appear 76 | 77 | ### Example Config 78 | ```json 79 | { 80 | "majorVersion": 3, 81 | "minorVersion": 6, 82 | "patchVersion": 0, 83 | "displayMode": "format", 84 | "fixedPosition": null, 85 | "targetPositionOffset": null, 86 | "timeDependencyDecimalPrecision": 1, 87 | "timeDependencyDecimalOffset": 2, 88 | "doIntermediateUpdates": true, 89 | "assumeMaxPostSwing": false, 90 | "judgments": [ 91 | { "threshold": 115, "text": "•", "color": [1, 1, 1, 1] }, 92 | { "threshold": 108, "text": "%B%c%A", "color": [1, 1, 1, 1] }, 93 | { "threshold": 0, "text": "%B%c%A", "color": [1, 1, 1, 1] } 94 | ], 95 | "chainHeadJudgments":[ 96 | { "threshold": 85, "text": "•", "color": [1, 1, 1, 1] }, 97 | { "threshold": 78, "text": "%B%c", "color": [1, 1, 1, 1] }, 98 | { "threshold": 0, "text": "%B%c", "color": [1, 1, 1, 1] } 99 | ], 100 | "chainLinkDisplay": { 101 | "text": "•", 102 | "color": [1, 1, 1, 1] 103 | }, 104 | "beforeCutAngleJudgments": [ 105 | { "threshold": 70, "text": " " }, 106 | { "threshold": 63, "text": "-" }, 107 | { "threshold": 56, "text": "-" }, 108 | { "threshold": 0, "text": "-" } 109 | ], 110 | "accuracyJudgments": [], 111 | "afterCutAngleJudgments": [ 112 | { "threshold": 30, "text": " " }, 113 | { "threshold": 27, "text": "-" }, 114 | { "threshold": 24, "text": "-" }, 115 | { "threshold": 0, "text": "-" } 116 | ], 117 | "timeDependencyJudgments": [], 118 | "randomizeBadCutDisplays": false, 119 | "badCutDisplays": [ 120 | { "text": "Bad Cut", "type": "WrongDirection", "color": [1, 1, 1, 1] }, 121 | { "text": "Bad Cut", "type": "WrongColor", "color": [1, 1, 1, 1] }, 122 | { "text": "Bomb", "type": "Bomb", "color": [1, 1, 1, 1] } 123 | ], 124 | "randomizeMissDisplays": true, 125 | "missDisplays": [ 126 | { "text": "MISS", "color": [1, 1, 1, 1] }, 127 | { "text": "MOSS", "color": [1, 1, 1, 1] }, 128 | { "text": "MASS", "color": [1, 1, 1, 1] }, 129 | { "text": "MESS", "color": [1, 1, 1, 1] } 130 | ] 131 | } 132 | ``` -------------------------------------------------------------------------------- /HitScoreVisualizer/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HitScoreVisualizer 8 | HitScoreVisualizer 9 | Eris 10 | 3.7.0 11 | 1.40.4 12 | 13 | Visualizes the scores of your hits! Overly complex config options! Numbers? 14 | All the pros use it, except the ones who don't. (But they should.) 15 | 16 | https://github.com/ErisApps/HitScoreVisualizer 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | BSIPA 31 | true 32 | 33 | 34 | 35 | true 36 | 37 | 38 | -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/BadNoteCutEffectSpawnerPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using HitScoreVisualizer.Components; 4 | using HitScoreVisualizer.Models; 5 | using HitScoreVisualizer.Utilities; 6 | using HitScoreVisualizer.Utilities.Extensions; 7 | using SiraUtil.Affinity; 8 | 9 | namespace HitScoreVisualizer.HarmonyPatches; 10 | 11 | internal class BadNoteCutEffectSpawnerPatch : IAffinity 12 | { 13 | private readonly HsvFlyingEffectSpawner flyingEffectSpawner; 14 | private readonly HsvConfigModel config; 15 | private readonly Random random; 16 | 17 | private readonly ArrayPicker wrongDirectionPicker = new([]); 18 | private readonly ArrayPicker wrongColorPicker = new([]); 19 | private readonly ArrayPicker bombPicker = new([]); 20 | 21 | public BadNoteCutEffectSpawnerPatch(HsvFlyingEffectSpawner flyingEffectSpawner, HsvConfigModel config, Random random) 22 | { 23 | this.flyingEffectSpawner = flyingEffectSpawner; 24 | this.config = config; 25 | this.random = random; 26 | 27 | if (config.BadCutDisplays is null or []) 28 | { 29 | return; 30 | } 31 | 32 | wrongDirectionPicker = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.WrongDirection or BadCutDisplayType.All).ToArray()); 33 | wrongColorPicker = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.WrongColor or BadCutDisplayType.All).ToArray()); 34 | bombPicker = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.Bomb or BadCutDisplayType.All).ToArray()); 35 | } 36 | 37 | [AffinityPrefix] 38 | [AffinityPatch(typeof(BadNoteCutEffectSpawner), nameof(BadNoteCutEffectSpawner.HandleNoteWasCut))] 39 | private bool HandleNoteWasCutPrefix(MissedNoteEffectSpawner __instance, NoteController noteController, in NoteCutInfo noteCutInfo) 40 | { 41 | if (noteController.noteData.time + 0.5f < __instance._audioTimeSyncController.songTime) 42 | { 43 | // Do nothing 44 | return false; 45 | } 46 | 47 | if (noteController.IsBomb()) 48 | { 49 | return !TrySpawnText(bombPicker, noteController, in noteCutInfo); 50 | } 51 | 52 | if (!noteCutInfo.IsBadCut()) 53 | { 54 | // Not a bomb or a bad cut, do nothing 55 | return false; 56 | } 57 | 58 | return !TrySpawnText(noteCutInfo.saberTypeOK ? wrongDirectionPicker : wrongColorPicker, noteController, in noteCutInfo); 59 | } 60 | 61 | private bool TrySpawnText(ArrayPicker picker, NoteController noteController, in NoteCutInfo noteCutInfo) 62 | { 63 | if (config.RandomizeBadCutDisplays && picker.TryGetRandom(random, out var display)) 64 | { 65 | SpawnText(display, noteController, in noteCutInfo); 66 | return true; 67 | } 68 | 69 | if (picker.TryGetNext(out display)) 70 | { 71 | SpawnText(display, noteController, in noteCutInfo); 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | 78 | private void SpawnText(BadCutDisplay display, NoteController noteController, in NoteCutInfo noteCutInfo) 79 | { 80 | flyingEffectSpawner.SpawnText( 81 | noteCutInfo.cutPoint, 82 | noteController.worldRotation, 83 | noteController.inverseWorldRotation, 84 | display.Text, 85 | display.Color); 86 | } 87 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/EffectPoolsManualInstallerPatch.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Utilities.Services; 2 | using SiraUtil.Affinity; 3 | using TMPro; 4 | 5 | // ReSharper disable InconsistentNaming 6 | 7 | namespace HitScoreVisualizer.HarmonyPatches; 8 | 9 | internal class EffectPoolsManualInstallerPatch : IAffinity 10 | { 11 | private readonly BloomFontProvider bloomFontProvider; 12 | private readonly PluginConfig pluginConfig; 13 | 14 | private EffectPoolsManualInstallerPatch(BloomFontProvider bloomFontProvider, PluginConfig pluginConfig) 15 | { 16 | this.bloomFontProvider = bloomFontProvider; 17 | this.pluginConfig = pluginConfig; 18 | } 19 | 20 | [AffinityPrefix] 21 | [AffinityPatch(typeof(EffectPoolsManualInstaller), nameof(EffectPoolsManualInstaller.ManualInstallBindings))] 22 | internal void ManualInstallBindingsPrefix(FlyingScoreEffect ____flyingScoreEffectPrefab) 23 | { 24 | var text = ____flyingScoreEffectPrefab._text; 25 | text.richText = true; 26 | text.enableWordWrapping = false; 27 | text.overflowMode = TextOverflowModes.Overflow; 28 | 29 | // Configure font shader and italics 30 | text.fontStyle = pluginConfig.DisableItalics ? FontStyles.Normal : FontStyles.Italic; 31 | text.font = bloomFontProvider.GetFontForType(pluginConfig.FontType); 32 | } 33 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/FlyingScoreEffectPatch.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Models; 2 | using HitScoreVisualizer.Utilities.Extensions; 3 | using SiraUtil.Affinity; 4 | using UnityEngine; 5 | 6 | namespace HitScoreVisualizer.HarmonyPatches; 7 | 8 | internal class FlyingScoreEffectPatch : IAffinity 9 | { 10 | private readonly HsvConfigModel config; 11 | 12 | private FlyingScoreEffectPatch(HsvConfigModel config) 13 | { 14 | this.config = config; 15 | } 16 | 17 | // When the flying score effect spawns, InitAndPresent is called 18 | // When the post swing score changes - as the saber moves - HandleCutScoreBufferDidChange is called 19 | // When the post swing score stops changing - HandleCutScoreBufferDidFinish is called 20 | 21 | [AffinityPrefix] 22 | [AffinityPatch(typeof(FlyingScoreEffect), nameof(FlyingScoreEffect.InitAndPresent))] 23 | internal bool InitAndPresent(ref FlyingScoreEffect __instance, IReadonlyCutScoreBuffer cutScoreBuffer, float duration, Vector3 targetPos) 24 | { 25 | var judgmentDetails = new JudgmentDetails(cutScoreBuffer) 26 | { 27 | AfterCutScore = config.AssumeMaxPostSwing ? cutScoreBuffer.noteScoreDefinition.maxAfterCutScore : cutScoreBuffer.afterCutScore, 28 | }; 29 | 30 | var (text, color) = config.Judge(in judgmentDetails); 31 | __instance._text.text = text; 32 | __instance._color = color; 33 | __instance._cutScoreBuffer = cutScoreBuffer; 34 | __instance._maxCutDistanceScoreIndicator.enabled = false; 35 | __instance._colorAMultiplier = 1f; 36 | 37 | if (!cutScoreBuffer.isFinished) 38 | { 39 | cutScoreBuffer.RegisterDidChangeReceiver(__instance); 40 | cutScoreBuffer.RegisterDidFinishReceiver(__instance); 41 | __instance._registeredToCallbacks = true; 42 | } 43 | 44 | if (config.FixedPosition != null) 45 | { 46 | // Set current and target position to the desired fixed position 47 | targetPos = config.FixedPosition.Value; 48 | __instance.transform.position = targetPos; 49 | } 50 | else if (config.TargetPositionOffset != null) 51 | { 52 | targetPos += config.TargetPositionOffset.Value; 53 | } 54 | 55 | __instance.InitAndPresent(duration, targetPos, cutScoreBuffer.noteCutInfo.worldRotation, false); 56 | 57 | return false; 58 | } 59 | 60 | [AffinityPrefix] 61 | [AffinityPatch(typeof(FlyingScoreEffect), nameof(FlyingScoreEffect.HandleCutScoreBufferDidChange))] 62 | internal bool HandleCutScoreBufferDidChange(FlyingScoreEffect __instance, CutScoreBuffer cutScoreBuffer) 63 | { 64 | if (!config.DoIntermediateUpdates) 65 | { 66 | return false; 67 | } 68 | 69 | var judgmentDetails = new JudgmentDetails(cutScoreBuffer); 70 | var (text, color) = config.Judge(in judgmentDetails); 71 | __instance._text.text = text; 72 | __instance._color = color; 73 | 74 | return false; 75 | } 76 | 77 | [AffinityPrefix] 78 | [AffinityPatch(typeof(FlyingScoreEffect), nameof(FlyingScoreEffect.HandleCutScoreBufferDidFinish))] 79 | internal void HandleCutScoreBufferDidFinish(FlyingScoreEffect __instance, CutScoreBuffer cutScoreBuffer) 80 | { 81 | var judgmentDetails = new JudgmentDetails(cutScoreBuffer); 82 | var (text, color) = config.Judge(in judgmentDetails); 83 | __instance._text.text = text; 84 | __instance._color = color; 85 | } 86 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/GameCoreInstallerHook.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Components; 2 | using HitScoreVisualizer.Models; 3 | using SiraUtil.Affinity; 4 | using UnityEngine; 5 | 6 | namespace HitScoreVisualizer.HarmonyPatches; 7 | 8 | public class GameCoreInstallerHook : IAffinity 9 | { 10 | [AffinityPrefix] 11 | [AffinityPatch(typeof(GameplayCoreInstaller), nameof(GameplayCoreInstaller.InstallBindings))] 12 | private void InstallBindingsPostfix(GameplayCoreInstaller __instance) 13 | { 14 | var container = __instance.Container; 15 | var missSpriteSpawner = __instance._missedNoteEffectSpawnerPrefab._missedNoteFlyingSpriteSpawner; 16 | var flyingTextEffect = __instance._effectPoolsManualInstaller._flyingTextEffectPrefab; 17 | 18 | container.BindInstance(new HsvFlyingEffectSpawner.InitData( 19 | missSpriteSpawner._duration, 20 | missSpriteSpawner._xSpread, 21 | missSpriteSpawner._targetYPos, 22 | missSpriteSpawner._targetZPos, 23 | Color.white, 24 | 4.5f)).AsSingle(); 25 | 26 | container.BindInstance(new FlyingTextEffectAnimationData( 27 | flyingTextEffect._fadeAnimationCurve, 28 | flyingTextEffect._moveAnimationCurve)).AsSingle(); 29 | } 30 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/HarmonyPatchManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using HarmonyLib; 4 | using Zenject; 5 | 6 | namespace HitScoreVisualizer.HarmonyPatches; 7 | 8 | internal class HarmonyPatchManager : IInitializable, IDisposable 9 | { 10 | private readonly Harmony harmony = new(Plugin.Metadata.Id); 11 | private readonly Assembly executingAssembly = Assembly.GetExecutingAssembly(); 12 | 13 | public void Initialize() 14 | { 15 | try 16 | { 17 | harmony.PatchAll(executingAssembly); 18 | } 19 | catch (Exception e) 20 | { 21 | Plugin.Log.Error(e); 22 | } 23 | } 24 | 25 | public void Dispose() 26 | { 27 | harmony.UnpatchSelf(); 28 | } 29 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/MissedNoteEffectSpawnerPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HitScoreVisualizer.Components; 3 | using HitScoreVisualizer.Models; 4 | using HitScoreVisualizer.Utilities; 5 | using SiraUtil.Affinity; 6 | 7 | namespace HitScoreVisualizer.HarmonyPatches; 8 | 9 | internal class MissedNoteEffectSpawnerPatch : IAffinity 10 | { 11 | private readonly HsvFlyingEffectSpawner flyingEffectSpawner; 12 | private readonly HsvConfigModel config; 13 | private readonly Random random; 14 | 15 | private readonly ArrayPicker missPicker = new([]); 16 | 17 | public MissedNoteEffectSpawnerPatch(HsvFlyingEffectSpawner flyingEffectSpawner, HsvConfigModel config, Random random) 18 | { 19 | this.flyingEffectSpawner = flyingEffectSpawner; 20 | this.config = config; 21 | this.random = random; 22 | 23 | if (config.MissDisplays is null) 24 | { 25 | return; 26 | } 27 | 28 | missPicker = new(config.MissDisplays.ToArray()); 29 | } 30 | 31 | [AffinityPrefix] 32 | [AffinityPatch(typeof(MissedNoteEffectSpawner), nameof(MissedNoteEffectSpawner.HandleNoteWasMissed))] 33 | private bool HandleNoteWasMissedPrefix(MissedNoteEffectSpawner __instance, NoteController noteController) 34 | { 35 | if (noteController.hidden 36 | || noteController.noteData.time + 0.5f < __instance._audioTimeSyncController.songTime 37 | || noteController.noteData.colorType == ColorType.None) 38 | { 39 | // Do nothing 40 | return false; 41 | } 42 | 43 | return !TrySpawnText(missPicker, noteController, __instance._spawnPosZ); 44 | } 45 | 46 | private bool TrySpawnText(ArrayPicker picker, NoteController noteController, float spawnPosZ) 47 | { 48 | if (config.RandomizeMissDisplays && picker.TryGetRandom(random, out var display)) 49 | { 50 | SpawnText(display, noteController, spawnPosZ); 51 | return true; 52 | } 53 | 54 | if (picker.TryGetNext(out display)) 55 | { 56 | SpawnText(display, noteController, spawnPosZ); 57 | return true; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | private void SpawnText(MissDisplay display, NoteController noteController, float spawnPosZ) 64 | { 65 | var position = noteController.inverseWorldRotation * noteController.noteTransform.position; 66 | position.z = spawnPosZ; 67 | 68 | flyingEffectSpawner.SpawnText( 69 | noteController.worldRotation * position, 70 | noteController.worldRotation, 71 | noteController.inverseWorldRotation, 72 | display.Text, 73 | display.Color); 74 | } 75 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/PersistentScoreEffectPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Reflection.Emit; 5 | using HarmonyLib; 6 | 7 | namespace HitScoreVisualizer.HarmonyPatches; 8 | 9 | [HarmonyPatch(typeof(GameplayCoreInstaller), nameof(GameplayCoreInstaller.InstallBindings))] 10 | internal class PersistentScoreEffectPatch 11 | { 12 | private static readonly MethodInfo GetZenMode = AccessTools.PropertyGetter(typeof(GameplayModifiers), nameof(GameplayModifiers.zenMode)); 13 | private static readonly MethodInfo GetNoTextsAndHuds = AccessTools.PropertyGetter(typeof(PlayerSpecificSettings), nameof(PlayerSpecificSettings.noTextsAndHuds)); 14 | 15 | /* 16 | * Changes: 17 | * - if (!playerSpecificSettings.noTextsAndHuds && !gameplayModifiers.zenMode) 18 | * + if (!gameplayModifiers.zenMode && (!playerSpecificSettings.noTextsAndHuds || overrideNoTextsAndHuds)) 19 | * { 20 | * Container.Bind().FromComponentInNewPrefab(_noteCutScoreSpawnerPrefab).AsSingle().NonLazy(); 21 | * } 22 | * 23 | * Description: Binds the NoteCutScoreSpawner when "No Texts And Huds" is enabled, and the "Override No Texts And Huds" 24 | * setting in HitScoreVisualizer is enabled 25 | */ 26 | 27 | public static IEnumerable Transpiler(IEnumerable instructions) => new CodeMatcher(instructions) 28 | .MatchStartForward( 29 | new CodeMatch(OpCodes.Ldloc_S), 30 | new CodeMatch(OpCodes.Callvirt, GetNoTextsAndHuds), 31 | new CodeMatch(), 32 | new CodeMatch(), 33 | new CodeMatch(OpCodes.Callvirt, GetZenMode), 34 | new CodeMatch(OpCodes.Brtrue) 35 | ) 36 | .ThrowIfInvalid("Couldn't find match for if (!playerSpecificSettings.noTextsAndHuds && !gameplayModifiers.zenMode)") 37 | // remove everything in the if statement but the last "Brtrue"; we will use that to check our delegate result and end the if statement 38 | .RemoveInstructions(5) 39 | .Insert(new List 40 | { 41 | new(OpCodes.Ldloc_S, 4), // load PlayerSpecificSettings 42 | new(OpCodes.Ldloc_S, 5), // load GameplayModifiers 43 | Transpilers.EmitDelegate>( 44 | ((playerSpecificSettings, gameplayModifiers) => 45 | !gameplayModifiers.zenMode && (!playerSpecificSettings.noTextsAndHuds || Plugin.Config.OverrideNoTextsAndHuds))) 46 | }) 47 | .MatchForward(useEnd: false, 48 | new CodeMatch(OpCodes.Brtrue) 49 | ) 50 | // replace the "Brtrue" with "Brfalse" because we're using a single bool check instead of && 51 | .SetOpcodeAndAdvance(OpCodes.Brfalse) 52 | .InstructionEnumeration(); 53 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HarmonyPatches/TranspilerHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using HarmonyLib; 5 | 6 | namespace HitScoreVisualizer.HarmonyPatches; 7 | 8 | public static class TranspilerHelper 9 | { 10 | public static CodeMatcher LogInstructionsFromPosition(this CodeMatcher matcher, int count) => 11 | matcher.LogInstructions(matcher.Instructions().Skip(matcher.Pos).Take(count)); 12 | 13 | public static CodeMatcher LogInstructions(this CodeMatcher matcher, int count, int offset) => 14 | matcher.LogInstructions(matcher.Instructions().Skip(offset).Take(count)); 15 | 16 | public static CodeMatcher LogInstructions(this CodeMatcher matcher) => 17 | matcher.LogInstructions(matcher.Instructions()); 18 | 19 | public static CodeMatcher GetOperand(this CodeMatcher matcher, out object operand) 20 | { 21 | operand = matcher.Operand; 22 | return matcher; 23 | } 24 | 25 | public static CodeMatcher GetOpcode(this CodeMatcher matcher, out object opcode) 26 | { 27 | opcode = matcher.Opcode; 28 | return matcher; 29 | } 30 | 31 | private static CodeMatcher LogInstructions(this CodeMatcher matcher, IEnumerable instructions) 32 | { 33 | var stringBuilder = new StringBuilder(); 34 | 35 | foreach (var instruction in instructions) 36 | { 37 | stringBuilder.AppendLine(instruction.ToString()); 38 | } 39 | 40 | Plugin.Log.Notice(stringBuilder.ToString()); 41 | return matcher; 42 | } 43 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/HitScoreVisualizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net472 7 | latest 8 | enable 9 | true 10 | true 11 | $(BeatSaberDir)\Beat Saber_Data\Managed 12 | false 13 | 14 | 15 | 16 | 17 | all 18 | build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $(BeatSaberDir)\Libs\0Harmony.dll 36 | false 37 | 38 | 39 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll 40 | false 41 | 42 | 43 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatSaber.ViewSystem.dll 44 | false 45 | 46 | 47 | $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.DotnetExtension.dll 48 | false 49 | 50 | 51 | $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.UnityExtension.dll 52 | false 53 | 54 | 55 | $(BeatSaberDir)\Plugins\BSML.dll 56 | false 57 | 58 | 59 | $(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll 60 | false 61 | 62 | 63 | $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll 64 | false 65 | 66 | 67 | $(BeatSaberDir)\Libs\Hive.Versioning.dll 68 | false 69 | 70 | 71 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll 72 | false 73 | 74 | 75 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll 76 | false 77 | 78 | 79 | $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll 80 | false 81 | 82 | 83 | $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll 84 | false 85 | 86 | 87 | $(BeatSaberDir)\Beat Saber_Data\Managed\mscorlib.dll 88 | false 89 | 90 | 91 | $(BeatSaberDir)\Beat Saber_Data\Managed\netstandard.dll 92 | false 93 | 94 | 95 | $(BeatSaberDir)\Beat Saber_Data\Managed\Newtonsoft.Json.dll 96 | false 97 | 98 | 99 | $(BeatSaberDir)\Plugins\SiraUtil.dll 100 | false 101 | 102 | 103 | $(BeatSaberDir)\Beat Saber_Data\Managed\System.dll 104 | false 105 | 106 | 107 | $(BeatSaberDir)\Beat Saber_Data\Managed\System.Core.dll 108 | false 109 | 110 | 111 | $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll 112 | false 113 | 114 | 115 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll 116 | false 117 | 118 | 119 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll 120 | false 121 | 122 | 123 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll 124 | false 125 | 126 | 127 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll 128 | false 129 | 130 | 131 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll 132 | false 133 | 134 | 135 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll 136 | false 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /HitScoreVisualizer/Installers/HsvAppInstaller.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.HarmonyPatches; 2 | using HitScoreVisualizer.UI; 3 | using HitScoreVisualizer.Utilities.Services; 4 | using JetBrains.Annotations; 5 | using Zenject; 6 | 7 | namespace HitScoreVisualizer.Installers; 8 | 9 | [UsedImplicitly] 10 | internal sealed class HsvAppInstaller : Installer 11 | { 12 | private readonly PluginConfig pluginConfig; 13 | 14 | private HsvAppInstaller(PluginConfig pluginConfig) 15 | { 16 | this.pluginConfig = pluginConfig; 17 | } 18 | 19 | public override void InstallBindings() 20 | { 21 | Container.BindInstance(pluginConfig); 22 | Container.Bind().AsSingle(); 23 | 24 | Container.BindInterfacesAndSelfTo().AsSingle(); 25 | Container.Bind().AsSingle(); 26 | 27 | Container.BindInterfacesAndSelfTo().AsSingle(); 28 | 29 | Container.Bind().AsSingle(); 30 | 31 | // Patches 32 | Container.BindInterfacesTo().AsSingle(); 33 | Container.BindInterfacesTo().AsSingle(); 34 | Container.BindInterfacesTo().AsSingle(); 35 | } 36 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Installers/HsvMenuInstaller.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.UI; 2 | using Zenject; 3 | 4 | namespace HitScoreVisualizer.Installers; 5 | 6 | internal sealed class HsvMenuInstaller : Installer 7 | { 8 | public override void InstallBindings() 9 | { 10 | Container.BindInterfacesTo().AsSingle(); 11 | Container.Bind().FromNewComponentOnNewGameObject().AsSingle(); 12 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 13 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 14 | 15 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 16 | Container.Bind().AsSingle(); 17 | Container.Bind().AsSingle(); 18 | Container.Bind().AsSingle(); 19 | } 20 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Installers/HsvPlayerInstaller.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Components; 2 | using HitScoreVisualizer.HarmonyPatches; 3 | using Zenject; 4 | 5 | namespace HitScoreVisualizer.Installers; 6 | 7 | internal class HsvPlayerInstaller : Installer 8 | { 9 | private readonly PluginConfig pluginConfig; 10 | private readonly HsvFlyingEffect hsvFlyingEffectPrefab = HsvFlyingEffect.CreatePrefab(); 11 | 12 | public HsvPlayerInstaller(PluginConfig pluginConfig) 13 | { 14 | this.pluginConfig = pluginConfig; 15 | } 16 | 17 | public override void InstallBindings() 18 | { 19 | var currentConfig = pluginConfig.SelectedConfig?.Config; 20 | if (currentConfig is null) 21 | { 22 | // No valid HSV config is selected, so HSV will not do anything during gameplay 23 | return; 24 | } 25 | 26 | Container.BindInstance(currentConfig).AsSingle(); 27 | 28 | Container.Bind().FromNewComponentOnNewGameObject().AsSingle(); 29 | Container.BindMemoryPool() 30 | .WithInitialSize(20) 31 | .FromComponentInNewPrefab(hsvFlyingEffectPrefab); 32 | 33 | // Patches 34 | Container.BindInterfacesTo().AsSingle(); 35 | Container.BindInterfacesTo().AsSingle(); 36 | Container.BindInterfacesTo().AsSingle(); 37 | } 38 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/BadCutDisplay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace HitScoreVisualizer.Models; 5 | 6 | [Serializable] 7 | public class BadCutDisplay 8 | { 9 | public required string Text { get; init; } 10 | 11 | public required Color Color { get; init; } 12 | 13 | public BadCutDisplayType? Type { get; init; } = BadCutDisplayType.All; 14 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/BadCutDisplayType.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | public enum BadCutDisplayType 4 | { 5 | /// 6 | /// This display will be used for any kind of bad cut 7 | /// 8 | All, 9 | 10 | /// 11 | /// This display will be used when the bad cut is caused by the note being cut in the wrong direction by the correct color saber 12 | /// 13 | WrongDirection, 14 | 15 | /// 16 | /// This display will be used when the bad cut is caused by the note being cut by the wrong color saber 17 | /// 18 | WrongColor, 19 | 20 | /// 21 | /// This display will be used by bad cuts caused by hitting bombs 22 | /// 23 | Bomb 24 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/BasicScoreData.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | internal record BasicScoreData(int Before, int Center, int After); -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ChainHeadJudgment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using UnityEngine; 4 | 5 | namespace HitScoreVisualizer.Models; 6 | 7 | [Serializable] 8 | public class ChainHeadJudgment : IJudgment 9 | { 10 | // This judgment will be applied only to chain note heads hit with score >= this number. 11 | // Note that if no judgment can be applied to a note, the text will appear as in the unmodded 12 | // game. 13 | public required int Threshold { get; init; } 14 | 15 | // The text to display (if judgment text is enabled). 16 | public required string Text { get; init; } 17 | 18 | // 4 floats, 0-1; red, green, blue, glow (not transparency!) 19 | // leaving this out should look obviously wrong 20 | public required Color Color { get; init; } 21 | 22 | // If true, the text color will be interpolated between this judgment's color and the previous 23 | // based on how close to the next threshold it is. 24 | // Specifying fade : true for the first judgment in the array is an error, and will crash the 25 | // plugin. 26 | public bool Fade { get; init; } 27 | 28 | [JsonIgnore] 29 | internal static ChainHeadJudgment Default { get; } = new() 30 | { 31 | Threshold = 0, 32 | Text = "%s", 33 | Color = Color.white 34 | }; 35 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ChainLinkDisplay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using UnityEngine; 4 | 5 | namespace HitScoreVisualizer.Models; 6 | 7 | [Serializable] 8 | public class ChainLinkDisplay 9 | { 10 | // The text to display for a chain segment 11 | public required string Text { get; set; } 12 | 13 | // 4 floats, 0-1; red, green, blue, glow (not transparency!) 14 | // leaving this out should look obviously wrong 15 | public required Color Color { get; set; } 16 | 17 | [JsonIgnore] 18 | internal static ChainLinkDisplay Default { get; } = new() 19 | { 20 | Text = "20", 21 | Color = Color.white 22 | }; 23 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace HitScoreVisualizer.Models; 4 | 5 | internal class ConfigInfo 6 | { 7 | public FileInfo File { get; } 8 | public string ConfigName { get; } 9 | public string Description { get; } 10 | public ConfigState State { get; } 11 | 12 | public HsvConfigModel? Config { get; set; } 13 | 14 | public ConfigInfo(FileInfo file, string description, ConfigState state) 15 | { 16 | File = file; 17 | ConfigName = Path.GetFileNameWithoutExtension(file.Name); 18 | Description = description; 19 | State = state; 20 | } 21 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/ConfigMigration200.cs: -------------------------------------------------------------------------------- 1 | using Hive.Versioning; 2 | 3 | namespace HitScoreVisualizer.Models.ConfigMigrations; 4 | 5 | internal class ConfigMigration200 : IHsvConfigMigration 6 | { 7 | public Version Version { get; } = new(2, 0, 0); 8 | public void Migrate(HsvConfigModel config) 9 | { 10 | config.BeforeCutAngleJudgments = [JudgmentSegment.Default]; 11 | config.AccuracyJudgments = [JudgmentSegment.Default]; 12 | config.AfterCutAngleJudgments = [JudgmentSegment.Default]; 13 | } 14 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/ConfigMigration210.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Hive.Versioning; 3 | 4 | namespace HitScoreVisualizer.Models.ConfigMigrations; 5 | 6 | internal class ConfigMigration210 : IHsvConfigMigration 7 | { 8 | public Version Version { get; } = new(2, 1, 0); 9 | public void Migrate(HsvConfigModel config) 10 | { 11 | config.Judgments = config.Judgments 12 | .Where(j => j.Threshold == 110) 13 | .Select(j => new NormalJudgment 14 | { 15 | Threshold = 115, 16 | Text = j.Text, 17 | Color = j.Color, 18 | Fade = j.Fade 19 | }).ToList(); 20 | 21 | if (config.AccuracyJudgments != null) 22 | { 23 | config.AccuracyJudgments = config.AccuracyJudgments 24 | .Where(aj => aj.Threshold == 10) 25 | .Select(s => new JudgmentSegment 26 | { 27 | Threshold = 15, 28 | Text = s.Text, 29 | }).ToList(); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/ConfigMigration223.cs: -------------------------------------------------------------------------------- 1 | using Hive.Versioning; 2 | 3 | namespace HitScoreVisualizer.Models.ConfigMigrations; 4 | 5 | internal class ConfigMigration223 : IHsvConfigMigration 6 | { 7 | public Version Version { get; } = new(2, 2, 3); 8 | public void Migrate(HsvConfigModel config) 9 | { 10 | config.DoIntermediateUpdates = true; 11 | } 12 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/ConfigMigration320.cs: -------------------------------------------------------------------------------- 1 | using Hive.Versioning; 2 | using UnityEngine; 3 | 4 | namespace HitScoreVisualizer.Models.ConfigMigrations; 5 | 6 | internal class ConfigMigration320 : IHsvConfigMigration 7 | { 8 | public Version Version { get; } = new(3, 2, 0); 9 | public void Migrate(HsvConfigModel config) 10 | { 11 | #pragma warning disable 618 12 | if (config.UseFixedPos) 13 | { 14 | config.FixedPosition = new Vector3(config.FixedPosX, config.FixedPosY, config.FixedPosZ); 15 | } 16 | #pragma warning restore 618 17 | } 18 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/ConfigMigration340.cs: -------------------------------------------------------------------------------- 1 | using Hive.Versioning; 2 | 3 | namespace HitScoreVisualizer.Models.ConfigMigrations; 4 | 5 | internal class ConfigMigration340 : IHsvConfigMigration 6 | { 7 | public Version Version { get; } = new(3, 4, 0); 8 | 9 | public void Migrate(HsvConfigModel config) 10 | { 11 | if (config.ChainHeadJudgments is null or []) 12 | { 13 | config.ChainHeadJudgments = [ChainHeadJudgment.Default]; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/ConfigMigration360.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Hive.Versioning; 3 | 4 | namespace HitScoreVisualizer.Models.ConfigMigrations; 5 | 6 | internal class ConfigMigration360 : IHsvConfigMigration 7 | { 8 | public Version Version { get; } = new(3, 6, 0); 9 | 10 | public void Migrate(HsvConfigModel config) 11 | { 12 | config.Judgments = config.Judgments.OrderByDescending(x => x.Threshold).ToList(); 13 | config.ChainHeadJudgments = config.ChainHeadJudgments.OrderByDescending(x => x.Threshold).ToList(); 14 | config.TimeDependenceJudgments = config.TimeDependenceJudgments?.OrderByDescending(x => x.Threshold).ToList(); 15 | } 16 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigMigrations/IHsvConfigMigration.cs: -------------------------------------------------------------------------------- 1 | using Hive.Versioning; 2 | 3 | namespace HitScoreVisualizer.Models.ConfigMigrations; 4 | 5 | internal interface IHsvConfigMigration 6 | { 7 | public Version Version { get; } 8 | public void Migrate(HsvConfigModel config); 9 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigState.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | internal enum ConfigState 4 | { 5 | Broken, 6 | Incompatible, 7 | ValidationFailed, 8 | NeedsMigration, 9 | Compatible, 10 | NewerVersion 11 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigValidations/IConfigValidation.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models.ConfigValidations; 2 | 3 | internal interface IConfigValidation 4 | { 5 | bool IsValid(HsvConfigModel config); 6 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigValidations/JudgmentSegmentsValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace HitScoreVisualizer.Models.ConfigValidations; 6 | 7 | internal class JudgmentSegmentsValidation : IConfigValidation 8 | { 9 | private readonly Func?> propertyGetter; 10 | 11 | public JudgmentSegmentsValidation(Func?> propertyGetter) 12 | { 13 | this.propertyGetter = propertyGetter; 14 | } 15 | 16 | public bool IsValid(HsvConfigModel config) 17 | { 18 | var segments = propertyGetter(config); 19 | if (segments is null || segments.Count <= 1) 20 | { 21 | return true; 22 | } 23 | 24 | var isOrdered = segments 25 | .Zip(segments.Skip(1), (a, b) => a.Threshold > b.Threshold) 26 | .All(x => x); 27 | 28 | if (!isOrdered) 29 | { 30 | Plugin.Log.Warn("Judgment segments are not correctly ordered; they should be ordered from highest to lowest threshold"); 31 | return false; 32 | } 33 | 34 | var hasDuplicate = segments 35 | .Zip(segments.Skip(1), (a, b) => a.Threshold == b.Threshold) 36 | .Contains(true); 37 | 38 | if (hasDuplicate) 39 | { 40 | Plugin.Log.Warn("Judgment segments contain a duplicate threshold"); 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigValidations/JudgmentsValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace HitScoreVisualizer.Models.ConfigValidations; 6 | 7 | internal class JudgmentsValidation : IConfigValidation 8 | { 9 | private readonly Func> propertyGetter; 10 | 11 | public JudgmentsValidation(Func> propertyGetter) 12 | { 13 | this.propertyGetter = propertyGetter; 14 | } 15 | 16 | public bool IsValid(HsvConfigModel config) 17 | { 18 | var judgments = propertyGetter(config).ToList(); 19 | if (judgments is []) 20 | { 21 | Plugin.Log.Warn("Config contains no Judgments when it should specify at least one Judgment"); 22 | return false; 23 | } 24 | 25 | var isOrdered = judgments 26 | .Zip(judgments.Skip(1), (a, b) => a.Threshold > b.Threshold) 27 | .All(x => x); 28 | 29 | if (!isOrdered) 30 | { 31 | Plugin.Log.Warn("Judgments are not correctly ordered; they should be ordered from highest to lowest threshold"); 32 | return false; 33 | } 34 | 35 | var isHighestJudgmentValid = !judgments.First().Fade; 36 | 37 | var hasDuplicates = judgments.Count > 1 && judgments 38 | .Zip(judgments.Skip(1), (a, b) => a.Threshold == b.Threshold) 39 | .Contains(true); 40 | 41 | if (!isHighestJudgmentValid) 42 | { 43 | Plugin.Log.Warn("The first judgment cannot have fade set to true"); 44 | } 45 | 46 | if (hasDuplicates) 47 | { 48 | Plugin.Log.Warn("Judgments contain a duplicate threshold"); 49 | } 50 | 51 | return isHighestJudgmentValid && !hasDuplicates; 52 | } 53 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigValidations/TimeDependenceDecimalValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HitScoreVisualizer.Models.ConfigValidations; 4 | 5 | internal class TimeDependenceDecimalValidation : IConfigValidation 6 | { 7 | public bool IsValid(HsvConfigModel config) 8 | { 9 | // 99 is the max for NumberFormatInfo.NumberDecimalDigits 10 | if (config.TimeDependenceDecimalPrecision is < 0 or > 99) 11 | { 12 | Plugin.Log.Warn("timeDependencyDecimalPrecision is outside the range 0 to 99"); 13 | return false; 14 | } 15 | 16 | if (config.TimeDependenceDecimalOffset < 0 || config.TimeDependenceDecimalOffset > Math.Log10(float.MaxValue)) 17 | { 18 | Plugin.Log.Warn($"timeDependencyDecimalOffset value is outside the range 0 to {(int) Math.Log10(float.MaxValue)}"); 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/ConfigValidations/TimeDependenceJudgmentsValidation.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using UnityEngine; 3 | 4 | namespace HitScoreVisualizer.Models.ConfigValidations; 5 | 6 | internal class TimeDependenceJudgmentsValidation : IConfigValidation 7 | { 8 | public bool IsValid(HsvConfigModel config) 9 | { 10 | var judgments = config.TimeDependenceJudgments; 11 | if (judgments is null || judgments.Count <= 1) 12 | { 13 | return true; 14 | } 15 | 16 | var isOrdered = judgments 17 | .Zip(judgments.Skip(1), (a, b) => a.Threshold > b.Threshold) 18 | .All(x => x); 19 | 20 | if (!isOrdered) 21 | { 22 | Plugin.Log.Warn("Time dependence judgments are not correctly ordered; they should be ordered from highest to lowest threshold"); 23 | return false; 24 | } 25 | 26 | var hasDuplicate = judgments 27 | .Zip(judgments.Skip(1), (a, b) => Mathf.Approximately(a.Threshold, b.Threshold)) 28 | .Contains(true); 29 | 30 | if (hasDuplicate) 31 | { 32 | Plugin.Log.Warn("Time dependence judgments contain a duplicate threshold"); 33 | return false; 34 | } 35 | 36 | return true; 37 | } 38 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/Direction.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | public enum Direction 4 | { 5 | Up = 0, 6 | UpRight = 1, 7 | Right = 2, 8 | DownRight = 3, 9 | Down = 4, 10 | DownLeft = 5, 11 | Left = 6, 12 | UpLeft = 7, 13 | None, 14 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/DisplayMode.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | public enum DisplayMode 4 | { 5 | None = 0, 6 | Format = 1, 7 | TextOnly = 2, 8 | Numeric = 3, 9 | ScoreOnTop = 4, 10 | Directions = 5, 11 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/FlyingTextEffectAnimationData.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace HitScoreVisualizer.Models; 4 | 5 | internal record FlyingTextEffectAnimationData(AnimationCurve Fade, AnimationCurve Move); -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/HsvConfigModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using UnityEngine; 5 | 6 | namespace HitScoreVisualizer.Models; 7 | 8 | [Serializable] 9 | public class HsvConfigModel 10 | { 11 | // Properties that are optional are marked as nullable. They can be ignored during serialization. 12 | // When a config object is made, all required fields will have to be provided with a value. 13 | 14 | public required ulong MajorVersion { get; set; } = Plugin.Metadata.HVersion.Major; 15 | public required ulong MinorVersion { get; set; } = Plugin.Metadata.HVersion.Minor; 16 | public required ulong PatchVersion { get; set; } = Plugin.Metadata.HVersion.Patch; 17 | 18 | public required DisplayMode DisplayMode { get; set; } 19 | 20 | [Obsolete("Use the FixedPosition property instead.")] public bool UseFixedPos { get; set; } 21 | [Obsolete("Use the FixedPosition property instead.")] public float FixedPosX { get; set; } 22 | [Obsolete("Use the FixedPosition property instead.")] public float FixedPosY { get; set; } 23 | [Obsolete("Use the FixedPosition property instead.")] public float FixedPosZ { get; set; } 24 | 25 | public Vector3? FixedPosition { get; set; } 26 | public Vector3? TargetPositionOffset { get; set; } 27 | 28 | public required bool DoIntermediateUpdates { get; set; } = true; 29 | 30 | public required bool AssumeMaxPostSwing { get; set; } 31 | 32 | public required List Judgments { get; set; } 33 | 34 | public required List ChainHeadJudgments { get; set; } 35 | public ChainLinkDisplay? ChainLinkDisplay { get; set; } 36 | 37 | public List? BeforeCutAngleJudgments { get; set; } 38 | public List? AccuracyJudgments { get; set; } 39 | public List? AfterCutAngleJudgments { get; set; } 40 | 41 | public int TimeDependenceDecimalPrecision { get; set; } = 1; 42 | public int TimeDependenceDecimalOffset { get; set; } = 2; 43 | public List? TimeDependenceJudgments { get; set; } 44 | 45 | public bool RandomizeBadCutDisplays { get; set; } = true; 46 | public List? BadCutDisplays { get; set; } 47 | 48 | public bool RandomizeMissDisplays { get; set; } = true; 49 | public List? MissDisplays { get; set; } 50 | 51 | [JsonIgnore] 52 | internal static HsvConfigModel Default { get; } = new() 53 | { 54 | MajorVersion = Plugin.Metadata.HVersion.Major, 55 | MinorVersion = Plugin.Metadata.HVersion.Minor, 56 | PatchVersion = Plugin.Metadata.HVersion.Patch, 57 | DisplayMode = DisplayMode.Format, 58 | DoIntermediateUpdates = true, 59 | AssumeMaxPostSwing = false, 60 | Judgments = [ 61 | new() 62 | { 63 | Threshold = 115, 64 | Text = "%s", 65 | Color = new(1f, 1f, 1f) 66 | }, 67 | new() 68 | { 69 | Threshold = 110, 70 | Text = "%B%C%s%A", 71 | Color = new(0f, 0.5f, 1.0f) 72 | }, 73 | new() 74 | { 75 | Threshold = 105, 76 | Text = "%B%C%s%A", 77 | Color = new(0f, 1f, 0f) 78 | }, 79 | new() 80 | { 81 | Threshold = 100, 82 | Text = "%B%C%s%A", 83 | Color = new(1f, 1f, 0f) 84 | }, 85 | new() 86 | { 87 | Threshold = 50, 88 | Text = "%B%s%A", 89 | Color = new(1f, 0f, 0f), 90 | Fade = true 91 | }, 92 | new() 93 | { 94 | Threshold = 0, 95 | Text = "%B%s%A", 96 | Color = new(1f, 0f, 0f) 97 | } 98 | ], 99 | ChainHeadJudgments = [ 100 | new() 101 | { 102 | Threshold = 85, 103 | Text = "%s", 104 | Color = new(1f, 1f, 1f) 105 | }, 106 | new() 107 | { 108 | Threshold = 80, 109 | Text = "%B%C%s", 110 | Color = new(0f, 0.5f, 1.0f) 111 | }, 112 | new() 113 | { 114 | Threshold = 75, 115 | Text = "%B%C%s", 116 | Color = new(0f, 1f, 0f) 117 | }, 118 | new() 119 | { 120 | Threshold = 70, 121 | Text = "%B%C%s", 122 | Color = new(1f, 1f, 0f) 123 | }, 124 | new() 125 | { 126 | Threshold = 35, 127 | Text = "%B%s", 128 | Color = new(1f, 0f, 0f), 129 | Fade = true 130 | }, 131 | new() 132 | { 133 | Threshold = 0, 134 | Text = "%B%s", 135 | Color = new(1f, 0f, 0f) 136 | } 137 | ], 138 | ChainLinkDisplay = new() 139 | { 140 | Text = "%s", 141 | Color = new(1f, 1f, 1f) 142 | }, 143 | BeforeCutAngleJudgments = [ 144 | new() 145 | { 146 | Threshold = 70, 147 | Text = " + " 148 | }, 149 | new() 150 | { 151 | Threshold = 0, 152 | Text = " - " 153 | } 154 | ], 155 | AccuracyJudgments = [ 156 | new() 157 | { 158 | Threshold = 15, 159 | Text = "" 160 | }, 161 | new() 162 | { 163 | Threshold = 0, 164 | Text = "" 165 | } 166 | ], 167 | AfterCutAngleJudgments = [ 168 | new() 169 | { 170 | Threshold = 30, 171 | Text = " + " 172 | }, 173 | new() 174 | { 175 | Threshold = 0, 176 | Text = " - " 177 | } 178 | ], 179 | TimeDependenceDecimalPrecision = 1, 180 | TimeDependenceDecimalOffset = 2, 181 | TimeDependenceJudgments = [] 182 | }; 183 | 184 | [JsonIgnore] 185 | internal static HsvConfigModel Vanilla { get; } = new() 186 | { 187 | MajorVersion = Plugin.Metadata.HVersion.Major, 188 | MinorVersion = Plugin.Metadata.HVersion.Minor, 189 | PatchVersion = Plugin.Metadata.HVersion.Patch, 190 | DisplayMode = DisplayMode.Format, 191 | DoIntermediateUpdates = true, 192 | AssumeMaxPostSwing = false, 193 | Judgments = 194 | [ 195 | new() { Threshold = 104, Text = "%C%s", Color = Color.white }, 196 | new() { Threshold = 0, Text = "%C%s", Color = Color.white } 197 | ], 198 | ChainHeadJudgments = 199 | [ 200 | new() { Threshold = 77, Text = "%C%s", Color = Color.white }, 201 | new() { Threshold = 0, Text = "%C%s", Color = Color.white } 202 | ], 203 | ChainLinkDisplay = new() { Text = "%s", Color = Color.white }, 204 | AccuracyJudgments = 205 | [ 206 | new() { Threshold = 15, Text = "" } 207 | ] 208 | }; 209 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/HsvFontType.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | public enum HsvFontType 4 | { 5 | Default = 0, 6 | Bloom 7 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/IJudgment.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace HitScoreVisualizer.Models; 4 | 5 | internal interface IJudgment 6 | { 7 | int Threshold { get; init; } 8 | string Text { get; init; } 9 | Color Color { get; init; } 10 | bool Fade { get; init; } 11 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/JudgmentDetails.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.Models; 2 | 3 | internal readonly ref struct JudgmentDetails 4 | { 5 | public int BeforeCutScore { get; init; } 6 | public int CenterCutScore { get; init; } 7 | public int AfterCutScore { get; init; } 8 | public int TotalCutScore { get; init; } 9 | public int MaxPossibleScore { get; init; } 10 | public NoteCutInfo CutInfo { get; init; } 11 | 12 | public JudgmentDetails(IReadonlyCutScoreBuffer cutScoreBuffer) 13 | { 14 | BeforeCutScore = cutScoreBuffer.beforeCutScore; 15 | CenterCutScore = cutScoreBuffer.centerDistanceCutScore; 16 | AfterCutScore = cutScoreBuffer.afterCutScore; 17 | TotalCutScore = cutScoreBuffer.cutScore; 18 | MaxPossibleScore = cutScoreBuffer.noteScoreDefinition.maxCutScore; 19 | CutInfo = cutScoreBuffer.noteCutInfo; 20 | } 21 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/JudgmentSegment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace HitScoreVisualizer.Models; 5 | 6 | [Serializable] 7 | public class JudgmentSegment 8 | { 9 | // This judgment will be applied only when the appropriate part of the swing contributes score >= this number. 10 | // If no judgment can be applied, the judgment for this segment will be "" (the empty string). 11 | public required int Threshold { get; init; } 12 | 13 | // The text to replace the appropriate judgment specifier with (%B, %C, %A) when this judgment applies. 14 | public required string Text { get; init; } 15 | 16 | [JsonIgnore] 17 | internal static JudgmentSegment Default { get; } = new() 18 | { 19 | Threshold = 0, 20 | Text = "%s" 21 | }; 22 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/MissDisplay.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace HitScoreVisualizer.Models; 4 | 5 | public class MissDisplay 6 | { 7 | public required string Text { get; init; } 8 | 9 | public required Color Color { get; init; } 10 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/NormalJudgment.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using UnityEngine; 3 | 4 | namespace HitScoreVisualizer.Models; 5 | 6 | public class NormalJudgment : IJudgment 7 | { 8 | // This judgment will be applied only to normal notes hit with score >= this number. 9 | // Note that if no judgment can be applied to a note, the text will appear as in the unmodded 10 | // game. 11 | public required int Threshold { get; init; } 12 | 13 | // The text to display (if judgment text is enabled). 14 | public required string Text { get; init; } 15 | 16 | // 4 floats, 0-1; red, green, blue, glow (not transparency!) 17 | // leaving this out should look obviously wrong 18 | public required Color Color { get; init; } 19 | 20 | // If true, the text color will be interpolated between this judgment's color and the previous 21 | // based on how close to the next threshold it is. 22 | // Specifying fade : true for the first judgment in the array is an error, and will crash the 23 | // plugin. 24 | public bool Fade { get; init; } 25 | 26 | [JsonIgnore] 27 | internal static NormalJudgment Default { get; } = new() 28 | { 29 | Threshold = 0, 30 | Text = "%s", 31 | Color = Color.white 32 | }; 33 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Models/TimeDependenceJudgmentSegment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HitScoreVisualizer.Models; 4 | 5 | [Serializable] 6 | public class TimeDependenceJudgmentSegment 7 | { 8 | // This judgment will be applied only when the time dependence >= this number. 9 | // If no judgment can be applied, the judgment for this segment will be "" (the empty string). 10 | public required float Threshold { get; init; } 11 | 12 | // The text to replace the appropriate judgment specifier with (%T) when this judgment applies. 13 | public required string Text { get; init; } 14 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/Plugin.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Installers; 2 | using IPA; 3 | using IPA.Config; 4 | using IPA.Config.Stores; 5 | using IPA.Loader; 6 | using SiraUtil.Zenject; 7 | using Logger = IPA.Logging.Logger; 8 | 9 | namespace HitScoreVisualizer; 10 | 11 | [Plugin(RuntimeOptions.DynamicInit), NoEnableDisable] 12 | public class Plugin 13 | { 14 | internal static PluginMetadata Metadata { get; private set; } = null!; 15 | internal static Logger Log { get; private set; } = null!; 16 | internal static PluginConfig Config { get; private set; } = null!; 17 | 18 | [Init] 19 | public Plugin(Logger logger, Config config, PluginMetadata pluginMetadata, Zenjector zenject) 20 | { 21 | Metadata = pluginMetadata; 22 | Log = logger; 23 | Config = config.Generated(); 24 | 25 | zenject.UseLogger(logger); 26 | zenject.UseMetadataBinder(); 27 | 28 | zenject.Install(Location.App, Config); 29 | zenject.Install(Location.Menu); 30 | zenject.Install(Location.Player); 31 | } 32 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/PluginConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using HitScoreVisualizer.Models; 3 | using IPA.Config.Stores; 4 | using IPA.Config.Stores.Attributes; 5 | 6 | [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)] 7 | namespace HitScoreVisualizer; 8 | 9 | internal class PluginConfig 10 | { 11 | public virtual string? ConfigFilePath { get; set; } 12 | public virtual HsvFontType FontType { get; set; } 13 | public virtual bool DisableItalics { get; set; } 14 | public virtual bool OverrideNoTextsAndHuds { get; set; } 15 | 16 | [Ignore] 17 | public ConfigInfo? SelectedConfig { get; set; } 18 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/ConfigPreviewAnimatedTab.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using BeatSaberMarkupLanguage.Attributes; 5 | using BGLib.UnityExtension; 6 | using HitScoreVisualizer.Models; 7 | using HitScoreVisualizer.Utilities.Extensions; 8 | using HitScoreVisualizer.Utilities.Services; 9 | using TMPro; 10 | using UnityEngine; 11 | using UnityEngine.UI; 12 | using Zenject; 13 | using Object = UnityEngine.Object; 14 | using Random = System.Random; 15 | 16 | namespace HitScoreVisualizer.UI; 17 | 18 | internal class ConfigPreviewAnimatedTab : IPreviewTextEffectDidFinishEvent 19 | { 20 | [Inject] private readonly ICoroutineStarter coroutineStarter = null!; 21 | [Inject] private readonly Random random = null!; 22 | [Inject] private readonly PluginConfig pluginConfig = null!; 23 | [Inject] private readonly ConfigLoader configLoader = null!; 24 | [Inject] private readonly RandomScoreGenerator randomScoreGenerator = null!; 25 | 26 | [UIComponent("PreviewTextTemplate")] private readonly TextMeshProUGUI previewTextTemplate = null!; 27 | [UIComponent("TextContainer")] private readonly RectTransform textContainer = null!; 28 | 29 | private PreviewTextEffect[] textEffects = null!; // assigned in initializer 30 | private const int NumberOfEffects = 14; 31 | private const float AnimationDuration = 0.7f; // based on FlyingScoreSpawner.SpawnFlyingScore 32 | 33 | [UIAction("#post-parse")] 34 | public void PostParse() 35 | { 36 | var prefab = AddressablesExtensions.LoadContent("Assets/Prefabs/Effects/FlyingTextEffect.prefab").FirstOrDefault(); 37 | if (prefab == null) 38 | { 39 | return; 40 | } 41 | previewTextTemplate.gameObject.SetActive(false); 42 | previewTextTemplate.gameObject.name = nameof(PreviewTextEffect); 43 | previewTextTemplate.gameObject.AddComponent().ignoreLayout = true; 44 | previewTextTemplate.enableAutoSizing = true; 45 | previewTextTemplate.fontSizeMax = 5f; 46 | var flyingTextEffect = prefab.GetComponent(); 47 | var textEffectPrefab = PreviewTextEffect.Construct(previewTextTemplate.gameObject, previewTextTemplate, flyingTextEffect._fadeAnimationCurve, flyingTextEffect._moveAnimationCurve); 48 | textEffects = Enumerable.Range(1, NumberOfEffects).Select(_ => Object.Instantiate(textEffectPrefab, textContainer)).ToArray(); 49 | } 50 | 51 | public void Enable() 52 | { 53 | configLoader.ConfigChanged += RestartAnimation; 54 | RestartAnimation(pluginConfig.SelectedConfig?.Config); 55 | } 56 | 57 | public void Disable() 58 | { 59 | configLoader.ConfigChanged -= RestartAnimation; 60 | StopAnimation(); 61 | } 62 | 63 | private Coroutine? currentAnimation; 64 | 65 | private void RestartAnimation(HsvConfigModel? config) 66 | { 67 | StopAnimation(); 68 | currentAnimation = coroutineStarter.StartCoroutine(AnimateTextEffects(config ?? HsvConfigModel.Vanilla)); 69 | animating = true; 70 | } 71 | 72 | private void StopAnimation() 73 | { 74 | if (currentAnimation != null) 75 | { 76 | coroutineStarter.StopCoroutine(currentAnimation); 77 | animating = false; 78 | } 79 | } 80 | 81 | private bool animating; 82 | private WaitForSeconds? animationInterval; 83 | private readonly WaitForSeconds initialDelay = new(0.25f); 84 | 85 | private IEnumerator AnimateTextEffects(HsvConfigModel config) 86 | { 87 | yield return initialDelay; 88 | animationInterval = new(AnimationDuration / textEffects.Length); 89 | while (animating) 90 | { 91 | foreach (var effect in textEffects) 92 | { 93 | SpawnTextEffect(effect, config); 94 | yield return animationInterval; 95 | } 96 | } 97 | } 98 | 99 | private void SpawnTextEffect(PreviewTextEffect effect, HsvConfigModel config) 100 | { 101 | var (text, color) = config.Judge(randomScoreGenerator.GetRandomScore()); 102 | var endPos = effect.transform.localPosition with 103 | { 104 | x = random.Next((int) textContainer.rect.xMin, (int) textContainer.rect.xMax + 1), 105 | y = random.Next((int) (textContainer.rect.yMin + textContainer.rect.yMax / 2f), (int) textContainer.rect.yMax + 1) 106 | }; 107 | var startPos = endPos with { y = textContainer.rect.yMin }; 108 | effect.InitAndPresent(AnimationDuration, startPos, endPos, color, text, !pluginConfig.DisableItalics); 109 | effect.DidFinishEvent.Add(this); 110 | effect.gameObject.SetActive(true); 111 | } 112 | 113 | public void HandlePreviewTextEffectDidFinish(PreviewTextEffect effect) 114 | { 115 | effect.DidFinishEvent.Remove(this); 116 | effect.gameObject.SetActive(false); 117 | } 118 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/ConfigPreviewCustomTab.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using BeatSaberMarkupLanguage.Attributes; 5 | using HitScoreVisualizer.Models; 6 | using HitScoreVisualizer.Utilities; 7 | using HitScoreVisualizer.Utilities.Extensions; 8 | using HitScoreVisualizer.Utilities.Services; 9 | using TMPro; 10 | using UnityEngine; 11 | using Zenject; 12 | 13 | namespace HitScoreVisualizer.UI; 14 | 15 | internal class ConfigPreviewCustomTab 16 | { 17 | [Inject] private readonly PluginConfig pluginConfig = null!; 18 | [Inject] private readonly ConfigLoader configLoader = null!; 19 | 20 | [UIComponent("CustomPreviewText")] private readonly TextMeshProUGUI previewText = null!; 21 | // private TextMeshProUGUI[] texts = null!; // set in initializer 22 | 23 | private CustomPreviewTab currentTab = CustomPreviewTab.Judgments; 24 | private JudgmentType currentJudgmentType = JudgmentType.Normal; 25 | private BadCutDisplayType currentBadCutType = BadCutDisplayType.All; 26 | 27 | private float timeDependence; 28 | private int before = 70; 29 | private int center = 15; 30 | private int after = 30; 31 | 32 | private ItemRevolver allBadCuts = new(); 33 | private ItemRevolver wrongDirections = new(); 34 | private ItemRevolver wrongColors = new(); 35 | private ItemRevolver bombs = new(); 36 | private ItemRevolver misses = new(); 37 | 38 | [UIAction("#post-parse")] 39 | public void PostParse() 40 | { 41 | previewText.enableAutoSizing = true; 42 | previewText.fontSizeMax = 5; 43 | } 44 | 45 | public void Enable() 46 | { 47 | configLoader.ConfigChanged += ConfigChanged; 48 | ConfigChanged(pluginConfig.SelectedConfig?.Config); 49 | } 50 | 51 | public void Disable() 52 | { 53 | configLoader.ConfigChanged -= ConfigChanged; 54 | } 55 | 56 | public object EnumFormatter(Enum v) 57 | { 58 | return Regex.Replace(v.ToString(), "(\\B[A-Z])", " $1"); 59 | } 60 | 61 | public Array JudgmentOptions { get; } = Enum.GetValues(typeof(JudgmentType)); 62 | public Array BadCutTypeOptions { get; } = Enum.GetValues(typeof(BadCutDisplayType)); 63 | 64 | public int Before 65 | { 66 | get => before; 67 | set 68 | { 69 | before = value; 70 | UpdateText(); 71 | } 72 | } 73 | 74 | public int Center 75 | { 76 | get => center; 77 | set 78 | { 79 | center = value; 80 | UpdateText(); 81 | } 82 | } 83 | 84 | public int After 85 | { 86 | get => after; 87 | set 88 | { 89 | after = value; 90 | UpdateText(); 91 | } 92 | } 93 | 94 | public float TimeDependence 95 | { 96 | get => timeDependence; 97 | set 98 | { 99 | timeDependence = value; 100 | UpdateText(); 101 | } 102 | } 103 | 104 | public JudgmentType JudgmentType 105 | { 106 | get => currentJudgmentType; 107 | set 108 | { 109 | currentJudgmentType = value; 110 | UpdateText(); 111 | } 112 | } 113 | 114 | public BadCutDisplayType BadCutType 115 | { 116 | get => currentBadCutType; 117 | set 118 | { 119 | currentBadCutType = value; 120 | UpdateText(); 121 | } 122 | } 123 | 124 | public void TabChanged(object segmentedControl, int idx) 125 | { 126 | currentTab = (CustomPreviewTab)idx; 127 | UpdateText(); 128 | } 129 | 130 | public void NextBadCut() 131 | { 132 | CurrentBadCuts.AdvanceNext(); 133 | UpdateText(); 134 | } 135 | 136 | public void PreviousBadCut() 137 | { 138 | CurrentBadCuts.AdvancePrevious(); 139 | UpdateText(); 140 | } 141 | 142 | public void NextMiss() 143 | { 144 | misses.AdvanceNext(); 145 | UpdateText(); 146 | } 147 | 148 | public void PreviousMiss() 149 | { 150 | misses.AdvancePrevious(); 151 | UpdateText(); 152 | } 153 | 154 | private void ConfigChanged(HsvConfigModel? config) 155 | { 156 | if (config is not null) 157 | { 158 | if (config.BadCutDisplays is not null or []) 159 | { 160 | allBadCuts = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.All)); 161 | wrongDirections = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.All or BadCutDisplayType.WrongDirection)); 162 | wrongColors = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.All or BadCutDisplayType.WrongColor)); 163 | bombs = new(config.BadCutDisplays.Where(x => x.Type is BadCutDisplayType.All or BadCutDisplayType.Bomb)); 164 | } 165 | 166 | if (config.MissDisplays is not null or []) 167 | { 168 | misses = new(config.MissDisplays); 169 | } 170 | } 171 | else 172 | { 173 | allBadCuts = new(); 174 | wrongDirections = new(); 175 | wrongColors = new(); 176 | bombs = new(); 177 | misses = new(); 178 | } 179 | 180 | UpdateText(); 181 | } 182 | 183 | private void UpdateText() 184 | { 185 | previewText.fontStyle = pluginConfig.DisableItalics ? FontStyles.Normal : FontStyles.Italic; 186 | (previewText.text, previewText.color) = currentTab switch 187 | { 188 | CustomPreviewTab.Judgments => GetJudgmentsText(), 189 | CustomPreviewTab.ChainLink => GetChainLinkText(), 190 | CustomPreviewTab.BadCut => GetBadCutText(), 191 | CustomPreviewTab.Miss => GetMissText(), 192 | _ => throw new ArgumentOutOfRangeException() 193 | }; 194 | } 195 | 196 | private ItemRevolver CurrentBadCuts => currentBadCutType switch 197 | { 198 | BadCutDisplayType.All => allBadCuts, 199 | BadCutDisplayType.WrongDirection => wrongDirections, 200 | BadCutDisplayType.WrongColor => wrongColors, 201 | BadCutDisplayType.Bomb => bombs, 202 | _ => throw new ArgumentOutOfRangeException() 203 | }; 204 | 205 | private (string, Color) GetJudgmentsText() 206 | { 207 | var (afterCut, max, cutInfo) = currentJudgmentType switch 208 | { 209 | JudgmentType.Normal => (after, 115, DummyScores.Normal), 210 | JudgmentType.ChainHead => (0, 85, DummyScores.ChainHead), 211 | _ => throw new ArgumentOutOfRangeException() 212 | }; 213 | return (pluginConfig.SelectedConfig?.Config ?? HsvConfigModel.Vanilla).Judge(new() 214 | { 215 | BeforeCutScore = before, 216 | CenterCutScore = center, 217 | AfterCutScore = afterCut, 218 | MaxPossibleScore = max, 219 | TotalCutScore = before + center + afterCut, 220 | CutInfo = cutInfo 221 | }); 222 | } 223 | 224 | private (string, Color) GetChainLinkText() 225 | { 226 | return (pluginConfig.SelectedConfig?.Config ?? HsvConfigModel.Vanilla).Judge(new() 227 | { 228 | BeforeCutScore = 0, 229 | CenterCutScore = 0, 230 | AfterCutScore = 0, 231 | MaxPossibleScore = 20, 232 | TotalCutScore = 20, 233 | CutInfo = DummyScores.ChainLink 234 | }); 235 | } 236 | 237 | private (string, Color) GetBadCutText(BadCutDisplay? display = null) 238 | { 239 | display ??= currentBadCutType switch 240 | { 241 | BadCutDisplayType.All => allBadCuts.Current, 242 | BadCutDisplayType.WrongDirection => wrongDirections.Current, 243 | BadCutDisplayType.WrongColor => wrongColors.Current, 244 | BadCutDisplayType.Bomb => bombs.Current, 245 | _ => throw new ArgumentOutOfRangeException() 246 | }; 247 | var text = display?.Text ?? "No display."; 248 | var color = display?.Color ?? new Color32(0xFF, 0xFF, 0xFF, 0xAA); 249 | return (text, color); 250 | } 251 | 252 | private (string, Color) GetMissText(MissDisplay? display = null) 253 | { 254 | display ??= misses.Current; 255 | var text = display?.Text ?? "No display."; 256 | var color = display?.Color ?? new Color32(0xFF, 0xFF, 0xFF, 0xAA); 257 | return (text, color); 258 | } 259 | } 260 | 261 | internal enum JudgmentType 262 | { 263 | Normal, 264 | ChainHead 265 | } 266 | 267 | internal enum CustomPreviewTab 268 | { 269 | Judgments = 0, 270 | ChainLink = 1, 271 | BadCut = 2, 272 | Miss = 3 273 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/ConfigPreviewGridTab.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq; 3 | using BeatSaberMarkupLanguage.Attributes; 4 | using HitScoreVisualizer.Models; 5 | using HitScoreVisualizer.Utilities.Services; 6 | using TMPro; 7 | using UnityEngine; 8 | using UnityEngine.UI; 9 | using Zenject; 10 | using Object = UnityEngine.Object; 11 | 12 | namespace HitScoreVisualizer.UI; 13 | 14 | internal class ConfigPreviewGridTab 15 | { 16 | [Inject] private readonly ConfigLoader configLoader = null!; 17 | [Inject] private readonly PluginConfig pluginConfig = null!; 18 | [Inject] private readonly ICoroutineStarter coroutineStarter = null!; 19 | 20 | [UIComponent("ScoreGrid")] private readonly GridLayoutGroup scoreGrid = null!; 21 | [UIObject("GridTextTemplate")] private readonly GameObject gridTextTemplate = null!; 22 | private readonly BasicScoreData[] gridScores = 23 | [ 24 | new(70, 15, 30), new(70, 14, 30), new(70, 13, 30), new(70, 12, 30), 25 | new(70, 11, 30), new(70, 10, 30), new(70, 05, 30), new(70, 00, 30), 26 | new(61, 15, 26), new(52, 10, 22), new(43, 05, 18), new(34, 00, 14), 27 | new(26, 15, 10), new(17, 10, 07), new(09, 05, 04), new(00, 00, 00) 28 | ]; 29 | 30 | private readonly WaitForSeconds animationInterval = new(0.04f); 31 | private PreviewGridText[] gridTexts = null!; // assigned in initializer 32 | 33 | [UIAction("#post-parse")] 34 | public void PostParse() 35 | { 36 | scoreGrid.childAlignment = TextAnchor.MiddleCenter; 37 | scoreGrid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; 38 | scoreGrid.constraintCount = 4; 39 | gridTexts = gridScores.Select(score => 40 | { 41 | var gameObject = Object.Instantiate(gridTextTemplate, scoreGrid.transform); 42 | gameObject.name = nameof(PreviewGridText); 43 | var textMesh = gameObject.GetComponentInChildren(); 44 | textMesh.enableAutoSizing = true; 45 | textMesh.fontSizeMax = 5f; 46 | return new PreviewGridText(gameObject, textMesh, score); 47 | }).ToArray(); 48 | } 49 | 50 | public void Enable() 51 | { 52 | configLoader.ConfigChanged += RestartAnimation; 53 | RestartAnimation(pluginConfig.SelectedConfig?.Config); 54 | } 55 | 56 | public void Disable() 57 | { 58 | configLoader.ConfigChanged -= RestartAnimation; 59 | StopAnimation(); 60 | } 61 | 62 | private Coroutine? currentAnimation; 63 | 64 | private void RestartAnimation(HsvConfigModel? config) 65 | { 66 | StopAnimation(); 67 | currentAnimation = coroutineStarter.StartCoroutine(AnimateGridDisplay(config ?? HsvConfigModel.Vanilla)); 68 | } 69 | 70 | private void StopAnimation() 71 | { 72 | if (currentAnimation != null) 73 | { 74 | coroutineStarter.StopCoroutine(currentAnimation); 75 | } 76 | } 77 | 78 | private IEnumerator AnimateGridDisplay(HsvConfigModel config) 79 | { 80 | foreach (var text in gridTexts) 81 | { 82 | text.SetActive(false); 83 | } 84 | 85 | foreach (var text in gridTexts) 86 | { 87 | text.SetActive(true); 88 | text.FontStyle = pluginConfig.DisableItalics ? FontStyles.Normal : FontStyles.Italic; 89 | text.SetTextForConfig(config); 90 | 91 | yield return animationInterval; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/ConfigPreviewViewController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BeatSaberMarkupLanguage.Attributes; 3 | using BeatSaberMarkupLanguage.ViewControllers; 4 | using Zenject; 5 | 6 | namespace HitScoreVisualizer.UI; 7 | 8 | [HotReload(RelativePathToLayout = @"\Views\ConfigPreview.bsml")] 9 | [ViewDefinition("HitScoreVisualizer.UI.Views.ConfigPreview.bsml")] 10 | internal class ConfigPreviewViewController : BSMLAutomaticViewController 11 | { 12 | [UIAction("#post-parse")] 13 | public void PostParse() 14 | { 15 | } 16 | 17 | [Inject] public ConfigPreviewGridTab GridTab { get; set; } = null!; 18 | [Inject] public ConfigPreviewAnimatedTab AnimatedTab { get; set; } = null!; 19 | [Inject] public ConfigPreviewCustomTab CustomTab { get; set; } = null!; 20 | 21 | private PreviewTab currentTab; 22 | 23 | public void PreviewTabChanged(object segmentedControl, int index) 24 | { 25 | NotifyCurrentTabDisabled(); 26 | currentTab = (PreviewTab)index; 27 | NotifyCurrentTabEnabled(); 28 | } 29 | 30 | protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 31 | { 32 | base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); 33 | NotifyCurrentTabEnabled(); 34 | } 35 | 36 | protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) 37 | { 38 | base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); 39 | NotifyCurrentTabDisabled(); 40 | } 41 | 42 | private void NotifyCurrentTabEnabled() 43 | { 44 | switch (currentTab) 45 | { 46 | case PreviewTab.Grid: 47 | GridTab.Enable(); 48 | break; 49 | case PreviewTab.Animated: 50 | AnimatedTab.Enable(); 51 | break; 52 | case PreviewTab.Custom: 53 | CustomTab.Enable(); 54 | break; 55 | default: 56 | throw new ArgumentOutOfRangeException(); 57 | } 58 | } 59 | 60 | private void NotifyCurrentTabDisabled() 61 | { 62 | switch (currentTab) 63 | { 64 | case PreviewTab.Grid: 65 | GridTab.Disable(); 66 | break; 67 | case PreviewTab.Animated: 68 | AnimatedTab.Disable(); 69 | break; 70 | case PreviewTab.Custom: 71 | CustomTab.Disable(); 72 | break; 73 | default: 74 | throw new ArgumentOutOfRangeException(); 75 | } 76 | } 77 | 78 | private enum PreviewTab 79 | { 80 | Grid = 0, 81 | Animated = 1, 82 | Custom = 2 83 | } 84 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/ConfigSelectorViewController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using BeatSaberMarkupLanguage.Attributes; 7 | using BeatSaberMarkupLanguage.Components; 8 | using BeatSaberMarkupLanguage.ViewControllers; 9 | using HitScoreVisualizer.Models; 10 | using HitScoreVisualizer.Utilities.Extensions; 11 | using HitScoreVisualizer.Utilities.Services; 12 | using HMUI; 13 | using IPA.Utilities.Async; 14 | using Zenject; 15 | 16 | namespace HitScoreVisualizer.UI; 17 | 18 | [HotReload(RelativePathToLayout = @"Views\ConfigSelector.bsml")] 19 | [ViewDefinition("HitScoreVisualizer.UI.Views.ConfigSelector.bsml")] 20 | internal class ConfigSelectorViewController : BSMLAutomaticViewController 21 | { 22 | [Inject] private readonly PluginConfig pluginConfig = null!; 23 | [Inject] private readonly ConfigLoader configLoader = null!; 24 | [Inject] private readonly PluginDirectories directories = null!; 25 | 26 | [UIComponent("configs-list")] 27 | private readonly CustomCellListTableData configsList = null!; 28 | 29 | public bool LoadingConfigs { get; private set; } 30 | 31 | public bool HasLoadedConfigs => !LoadingConfigs; 32 | 33 | public bool ConfigPickable => 34 | pluginConfig.SelectedConfig is { State: ConfigState.Compatible or ConfigState.NeedsMigration }; 35 | 36 | public bool HasConfigCurrently => 37 | !string.IsNullOrWhiteSpace(pluginConfig.ConfigFilePath); 38 | 39 | public string LoadedConfigText => 40 | $"Currently Loaded Config : {(HasConfigCurrently ? Path.GetFileNameWithoutExtension(pluginConfig.ConfigFilePath) : "None")}"; 41 | 42 | public bool ConfigYeetable => pluginConfig.SelectedConfig is { Config: not null }; 43 | 44 | public void ConfigSelected(TableView tableView, object obj) 45 | { 46 | pluginConfig.SelectedConfig = (ConfigInfo)obj; 47 | NotifyPropertyChanged(nameof(ConfigPickable)); 48 | NotifyPropertyChanged(nameof(ConfigYeetable)); 49 | } 50 | 51 | public async void RefreshList() 52 | { 53 | try 54 | { 55 | await RefreshListInternal(); 56 | } 57 | catch (Exception ex) 58 | { 59 | Plugin.Log.Error($"Encountered a problem while refreshing config list: {ex}"); 60 | } 61 | } 62 | 63 | public async void PickConfig() 64 | { 65 | try 66 | { 67 | if (await configLoader.TrySelectConfig(pluginConfig.SelectedConfig)) 68 | { 69 | await RefreshListInternal(); 70 | } 71 | } 72 | catch (Exception ex) 73 | { 74 | Plugin.Log.Error($"Encountered a problem while picking selected config: {ex}"); 75 | } 76 | } 77 | 78 | public async void UnpickConfig() 79 | { 80 | try 81 | { 82 | if (!HasConfigCurrently) 83 | { 84 | return; 85 | } 86 | 87 | configsList.TableView.ClearSelection(); 88 | await configLoader.TrySelectConfig(null); 89 | 90 | NotifyPropertyChanged(nameof(HasConfigCurrently)); 91 | NotifyPropertyChanged(nameof(LoadedConfigText)); 92 | } 93 | catch (Exception ex) 94 | { 95 | Plugin.Log.Error($"Encountered a problem while unpicking config: {ex}"); 96 | } 97 | } 98 | 99 | public async void YeetConfig() 100 | { 101 | try 102 | { 103 | if (!ConfigYeetable) 104 | { 105 | return; 106 | } 107 | 108 | pluginConfig.SelectedConfig?.Yeet(); 109 | await RefreshListInternal(); 110 | 111 | NotifyPropertyChanged(nameof(ConfigYeetable)); 112 | } 113 | catch (Exception ex) 114 | { 115 | Plugin.Log.Error($"Encountered a problem while deleting selected config: {ex}"); 116 | } 117 | } 118 | 119 | public void FolderButtonPressed() 120 | { 121 | Process.Start(directories.Configs.FullName); 122 | } 123 | 124 | protected override async void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 125 | { 126 | try 127 | { 128 | base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); 129 | await RefreshListInternal(); 130 | } 131 | catch (Exception ex) 132 | { 133 | Plugin.Log.Error($"Encountered a problem while deactivating {nameof(ConfigSelectorViewController)}: {ex}"); 134 | } 135 | } 136 | 137 | private async Task RefreshListInternal() 138 | { 139 | LoadingConfigs = true; 140 | 141 | NotifyPropertyChanged(nameof(LoadingConfigs)); 142 | NotifyPropertyChanged(nameof(HasLoadedConfigs)); 143 | 144 | var intermediateConfigs = (await configLoader.LoadAllHsvConfigs()) 145 | .OrderByDescending(x => x.State) 146 | .ThenBy(x => x.ConfigName) 147 | .ToList(); 148 | var currentConfigIndex = intermediateConfigs.FindIndex(configInfo => configInfo.File.FullName == pluginConfig.SelectedConfig?.File.FullName); 149 | 150 | configsList.Data = intermediateConfigs; 151 | 152 | await UnityMainThreadTaskScheduler.Factory.StartNew(() => 153 | { 154 | configsList.TableView.ReloadData(); 155 | configsList.TableView.ScrollToCellWithIdx(0, TableView.ScrollPositionType.Beginning, false); 156 | if (currentConfigIndex >= 0) 157 | { 158 | configsList.TableView.SelectCellWithIdx(currentConfigIndex, true); 159 | } 160 | 161 | LoadingConfigs = false; 162 | NotifyPropertyChanged(nameof(LoadingConfigs)); 163 | NotifyPropertyChanged(nameof(HasLoadedConfigs)); 164 | NotifyPropertyChanged(nameof(HasConfigCurrently)); 165 | NotifyPropertyChanged(nameof(LoadedConfigText)); 166 | }); 167 | } 168 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/HsvMainFlowCoordinator.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage; 2 | using HMUI; 3 | using Zenject; 4 | 5 | namespace HitScoreVisualizer.UI; 6 | 7 | internal class HsvMainFlowCoordinator : FlowCoordinator 8 | { 9 | [Inject] private readonly ConfigSelectorViewController configSelectorViewController = null!; 10 | [Inject] private readonly PluginSettingsViewController pluginSettingsViewController = null!; 11 | [Inject] private readonly ConfigPreviewViewController configPreviewViewController = null!; 12 | 13 | protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 14 | { 15 | if (firstActivation) 16 | { 17 | SetTitle(Plugin.Metadata.Name); 18 | showBackButton = true; 19 | 20 | ProvideInitialViewControllers(configSelectorViewController, pluginSettingsViewController, configPreviewViewController); 21 | } 22 | } 23 | 24 | protected override void BackButtonWasPressed(ViewController _) 25 | { 26 | // Dismiss ourselves 27 | BeatSaberUI.MainFlowCoordinator.DismissFlowCoordinator(this); 28 | } 29 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/IPreviewTextEffectDidFinishEvent.cs: -------------------------------------------------------------------------------- 1 | namespace HitScoreVisualizer.UI; 2 | 3 | internal interface IPreviewTextEffectDidFinishEvent 4 | { 5 | void HandlePreviewTextEffectDidFinish(PreviewTextEffect effect); 6 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/MenuButtonManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BeatSaberMarkupLanguage.MenuButtons; 3 | using Zenject; 4 | 5 | namespace HitScoreVisualizer.UI; 6 | 7 | internal class MenuButtonManager : IInitializable, IDisposable 8 | { 9 | private readonly HsvMainFlowCoordinator hsvMainFlowCoordinator; 10 | private readonly MainFlowCoordinator mainFlowCoordinator; 11 | private readonly MenuButtons menuButtons; 12 | private readonly MenuButton hsvMenuButton; 13 | 14 | private const string Title = "Hit Score Visualizer"; 15 | private const string HoverHint = "Change your score visualizer config"; 16 | 17 | public MenuButtonManager( 18 | HsvMainFlowCoordinator hsvMainFlowCoordinator, 19 | MainFlowCoordinator mainFlowCoordinator, 20 | MenuButtons menuButtons) 21 | { 22 | this.hsvMainFlowCoordinator = hsvMainFlowCoordinator; 23 | this.mainFlowCoordinator = mainFlowCoordinator; 24 | this.menuButtons = menuButtons; 25 | hsvMenuButton = new(Title, HoverHint, OnClick); 26 | } 27 | 28 | public void Initialize() 29 | { 30 | menuButtons.RegisterButton(hsvMenuButton); 31 | } 32 | 33 | public void Dispose() 34 | { 35 | menuButtons.UnregisterButton(hsvMenuButton); 36 | } 37 | 38 | private void OnClick() 39 | { 40 | mainFlowCoordinator.PresentFlowCoordinator(hsvMainFlowCoordinator); 41 | } 42 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/PluginSettingsViewController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using BeatSaberMarkupLanguage.Attributes; 5 | using BeatSaberMarkupLanguage.ViewControllers; 6 | using HitScoreVisualizer.Models; 7 | using Zenject; 8 | 9 | namespace HitScoreVisualizer.UI; 10 | 11 | [HotReload(RelativePathToLayout = @"Views\PluginSettings.bsml")] 12 | [ViewDefinition("HitScoreVisualizer.UI.Views.PluginSettings.bsml")] 13 | internal class PluginSettingsViewController : BSMLAutomaticViewController 14 | { 15 | [Inject] private readonly PluginConfig config = null!; 16 | 17 | public List FontTypeChoices = Enum.GetNames(typeof(HsvFontType)).ToList(); 18 | 19 | public string FontType 20 | { 21 | get => config.FontType.ToString(); 22 | set => config.FontType = Enum.TryParse(value, out HsvFontType t) ? t : HsvFontType.Default; 23 | } 24 | 25 | public bool DisableItalics 26 | { 27 | get => config.DisableItalics; 28 | set => config.DisableItalics = value; 29 | } 30 | 31 | public bool OverrideNoTextsAndHuds 32 | { 33 | get => config.OverrideNoTextsAndHuds; 34 | set => config.OverrideNoTextsAndHuds = value; 35 | } 36 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/PreviewGridText.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Models; 2 | using HitScoreVisualizer.Utilities; 3 | using HitScoreVisualizer.Utilities.Extensions; 4 | using HitScoreVisualizer.Utilities.Services; 5 | using TMPro; 6 | using UnityEngine; 7 | 8 | namespace HitScoreVisualizer.UI; 9 | 10 | internal class PreviewGridText 11 | { 12 | private readonly GameObject root; 13 | private readonly TextMeshProUGUI textMesh; 14 | private readonly BasicScoreData score; 15 | 16 | public PreviewGridText(GameObject root, TextMeshProUGUI textMesh, BasicScoreData score) 17 | { 18 | this.root = root; 19 | this.textMesh = textMesh; 20 | this.score = score; 21 | } 22 | 23 | public FontStyles FontStyle 24 | { 25 | set => textMesh.fontStyle = value; 26 | } 27 | 28 | public void SetActive(bool active) 29 | { 30 | root.SetActive(active); 31 | } 32 | 33 | public void SetTextForConfig(HsvConfigModel config) 34 | { 35 | var judgmentDetails = new JudgmentDetails 36 | { 37 | BeforeCutScore = score.Before, 38 | CenterCutScore = score.Center, 39 | AfterCutScore = score.After, 40 | MaxPossibleScore = 115, 41 | TotalCutScore = score.Before + score.Center + score.After, 42 | CutInfo = DummyScores.Normal 43 | }; 44 | (textMesh.text, textMesh.color) = config.Judge(in judgmentDetails); 45 | } 46 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/PreviewTextEffect.cs: -------------------------------------------------------------------------------- 1 | using HitScoreVisualizer.Models; 2 | using TMPro; 3 | using UnityEngine; 4 | using Zenject; 5 | 6 | namespace HitScoreVisualizer.UI; 7 | 8 | internal class PreviewTextEffect : MonoBehaviour 9 | { 10 | [SerializeField] 11 | private TextMeshProUGUI textMesh = null!; 12 | 13 | [SerializeField] 14 | private AnimationCurve fadeAnimationCurve = null!; 15 | 16 | [SerializeField] 17 | private AnimationCurve moveAnimationCurve = null!; 18 | 19 | public static PreviewTextEffect Construct(GameObject go, TextMeshProUGUI textMesh, AnimationCurve fade, AnimationCurve move) 20 | { 21 | var previewTextEffect = go.AddComponent(); 22 | previewTextEffect.textMesh = textMesh; 23 | previewTextEffect.fadeAnimationCurve = fade; 24 | previewTextEffect.moveAnimationCurve = move; 25 | return previewTextEffect; 26 | } 27 | 28 | private readonly LazyCopyHashSet didFinishEvent = new(); 29 | public ILazyCopyHashSet DidFinishEvent => didFinishEvent; 30 | 31 | private bool initialized; 32 | private float elapsedTime; 33 | private float duration; 34 | private Color color; 35 | private Vector3 startPos; 36 | private Vector3 endPos; 37 | 38 | public void InitAndPresent(float duration, Vector3 startPos, Vector3 endPos, Color color, string text, bool italics) 39 | { 40 | this.duration = duration; 41 | this.color = color; 42 | this.startPos = startPos; 43 | this.endPos = endPos; 44 | elapsedTime = 0f; 45 | textMesh.text = text; 46 | textMesh.fontStyle = italics ? FontStyles.Italic : FontStyles.Normal; 47 | ManualUpdate(0f); 48 | initialized = true; 49 | enabled = true; 50 | } 51 | 52 | private void Update() 53 | { 54 | if (!initialized) 55 | { 56 | enabled = false; 57 | return; 58 | } 59 | 60 | if (elapsedTime >= duration) 61 | { 62 | foreach (var e in didFinishEvent.items) 63 | { 64 | e.HandlePreviewTextEffectDidFinish(this); 65 | } 66 | return; 67 | } 68 | var progress = elapsedTime / duration; 69 | ManualUpdate(progress); 70 | elapsedTime += Time.deltaTime; 71 | } 72 | 73 | private void ManualUpdate(float progress) 74 | { 75 | textMesh.color = color with { a = fadeAnimationCurve.Evaluate(progress) }; 76 | transform.localPosition = Vector3.Lerp(startPos, endPos, moveAnimationCurve.Evaluate(progress)); 77 | } 78 | } -------------------------------------------------------------------------------- /HitScoreVisualizer/UI/Views/ConfigPreview.bsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |