├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── BeatmapInfo.cs ├── BeatmapOffsets.cs ├── BeatmapUtils.cs ├── HarmonyPatches.cs ├── Installers └── JDFixerMenuInstaller.cs ├── Interfaces └── IBeatmapInfoUpdater.cs ├── JDFixer.csproj ├── JDFixer.sln ├── LICENSE ├── Managers └── JDFixerUIManager.cs ├── NOTICE ├── Notices ├── JFDIXER.txt └── NjsFixer.txt ├── Plugin.cs ├── PluginConfig.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.resx └── Resources1.Designer.cs ├── README.md ├── Screenshots ├── 2.0.3_menu_1.png ├── 2.0.3_menu_1_small.png ├── 2.0.3_menu_2.png ├── 2.0.3_menu_2_small.png ├── 2.1.0_menu_1.png ├── 2.1.0_menu_2.png ├── 2.1.0_menu_3.png ├── 3.0.0_menu_1.png ├── 3.0.0_menu_2.png ├── 3.0.0_menu_3.png ├── 3.0.0_menu_3_small.png ├── 3.0.0_menu_4.png ├── 3.1.0_menu_1.png ├── 3.1.0_menu_2.png ├── 4.0.0_menu_ta.png ├── 5.0.0_menu_1.png ├── 5.0.0_menu_2.png ├── 6.0.0_menu_linked_1.png ├── 6.0.0_menu_linked_1_cropped.png ├── 6.0.0_menu_linked_2.png ├── 6.0.0_menu_linked_hide_rt.png ├── 6.0.0_menu_linked_hide_rt_cropped.png ├── 6.0.0_menu_main.png ├── 6.0.0_menu_main_1.png ├── 6.0.0_menu_unlinked_1.png ├── 6.0.0_menu_unlinked_2.png ├── 6.0.0_mod_settings.png ├── 6.0.0_mp.png ├── 6.0.0_preferences.png ├── 6.0.0_ta_1.png ├── 6.0.0_ta_1_cropped.png ├── 6.0.0_ta_2.png ├── 6.0.0_ta_2_cropped.png ├── menu1_small.png ├── menu2_small.png ├── menu_2.0.1.png ├── menu_2.1.2_1.png ├── menu_2.1.2_1_large.png ├── menu_2.1.2_1_small.png ├── ui_options.png └── v7 │ ├── 7.0.0_beat_fraction.png │ ├── 7.0.0_beat_fraction_2.png │ ├── 7.0.0_beat_fraction_3.png │ ├── 7.0.0_menu_unlinked.png │ ├── 7.0.0_menu_unlinked_2.png │ ├── 7.0.0_menu_unlinked_3.png │ ├── 7.0.0_mod_settings.png │ ├── 7.0.0_mod_settings_2.png │ ├── 7.0.0_mod_settings_3.png │ └── 7.0.0_mod_settings_song_speed_1.png ├── TimeController.cs ├── TimeSetup.cs ├── UI ├── BSML │ ├── customOnlineUI.bsml │ ├── legacyModifierUI.bsml │ ├── mainMenuUI.bsml │ ├── modifierUI.bsml │ ├── preferencesList.bsml │ └── rtPreferencesList.bsml ├── CustomOnlineUI.cs ├── Donate.cs ├── LegacyModifierUI.cs ├── MainMenuUI.cs ├── ModifierUI.cs ├── PreferencesFlowCoordinator.cs ├── PreferencesListViewController.cs └── RTPreferencesListViewController.cs ├── manifest.json └── packages.config /.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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: xeph_yr 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: zeph_yr 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /BeatmapInfo.cs: -------------------------------------------------------------------------------- 1 | namespace JDFixer 2 | { 3 | public class BeatmapInfo 4 | { 5 | internal delegate void BeatmapInfoEventHandler(BeatmapInfo e); 6 | internal static event BeatmapInfoEventHandler SelectedChanged; 7 | 8 | internal static void SetSelected(BeatmapKey key, BeatmapLevel level) 9 | { 10 | var updatedMapInfo = level == null ? Empty : new BeatmapInfo(key, level); 11 | 12 | Selected = updatedMapInfo; 13 | SelectedChanged?.Invoke(updatedMapInfo); 14 | } 15 | 16 | public static BeatmapInfo Selected { get; private set; } = Empty; 17 | 18 | internal static BeatmapInfo Empty { get; } = new BeatmapInfo(); 19 | 20 | private BeatmapInfo() 21 | { 22 | // To enable Campaigns and TA to show 0 instead of values from the last selected map in Solo Mode, 23 | // Better UX as players may forget to ignore the display. 24 | JumpDistance = 0f; 25 | MinJumpDistance = 0f; 26 | ReactionTime = 0f; 27 | MinReactionTime = 0f; 28 | 29 | // Ultra hack way to prevent divide by zero in Reaction Time Display 30 | NJS = 0.001f; 31 | 32 | // Experimental 33 | MinRTSlider = 0f; 34 | MaxRTSlider = 3000f; 35 | 36 | MinJDSlider = 0f; 37 | MaxJDSlider = 50f; 38 | 39 | // 1.26.0-1.29.0 Feature update 40 | JDOffsetQuantum = 0.1f; 41 | RTOffsetQuantum = 5f; 42 | } 43 | 44 | internal BeatmapInfo(BeatmapKey key, BeatmapLevel level) 45 | { 46 | if (level == null) 47 | { 48 | return; 49 | } 50 | 51 | float bpm = level.beatsPerMinute; 52 | 53 | BeatmapBasicData beatmapBasicData; 54 | float njs = 10f; 55 | float offset = 0f; 56 | if (level.beatmapBasicData.TryGetValue((key.beatmapCharacteristic, key.difficulty), out beatmapBasicData)) 57 | { 58 | njs = beatmapBasicData.noteJumpMovementSpeed; 59 | offset = beatmapBasicData.noteJumpStartBeatOffset; 60 | } 61 | 62 | if (njs <= 0.01f) 63 | njs = 10f; 64 | 65 | JumpDistance = BeatmapUtils.CalculateJumpDistance(bpm, njs, offset); 66 | MinJumpDistance = BeatmapUtils.CalculateJumpDistance(bpm, njs, -50f); 67 | NJS = njs; 68 | ReactionTime = JumpDistance * 500 / NJS; 69 | MinReactionTime = MinJumpDistance * 500 / NJS; 70 | 71 | // Experimental 72 | if (PluginConfig.Instance.slider_setting == 0) 73 | { 74 | MinRTSlider = PluginConfig.Instance.minJumpDistance * 500 / NJS; 75 | MaxRTSlider = PluginConfig.Instance.maxJumpDistance * 500 / NJS; 76 | 77 | MinJDSlider = PluginConfig.Instance.minJumpDistance; 78 | MaxJDSlider = PluginConfig.Instance.maxJumpDistance; 79 | } 80 | else 81 | { 82 | MinRTSlider = PluginConfig.Instance.minReactionTime; 83 | MaxRTSlider = PluginConfig.Instance.maxReactionTime; 84 | 85 | MinJDSlider = PluginConfig.Instance.minReactionTime * NJS / 500; 86 | MaxJDSlider = PluginConfig.Instance.maxReactionTime * NJS / 500; 87 | } 88 | 89 | // 1.26.0-1.29.0 Feature update 90 | Offset = offset; 91 | JDOffsetQuantum = BeatmapUtils.CalculateJumpDistance(bpm, njs, offset + 1 / PluginConfig.Instance.offset_fraction) - BeatmapUtils.CalculateJumpDistance(bpm, njs, offset); 92 | RTOffsetQuantum = JDOffsetQuantum * 500 / NJS; 93 | 94 | //Plugin.Log.Debug("BeatmapInfo minJD: " + PluginConfig.Instance.minJumpDistance); 95 | //Plugin.Log.Debug("BeatmapInfo maxJD: " + PluginConfig.Instance.maxJumpDistance); 96 | //Plugin.Log.Debug("BeatmapInfo minRT: " + MinRTSlider); 97 | //Plugin.Log.Debug("BeatmapInfo maxRT: " + MaxRTSlider); 98 | } 99 | 100 | // 1.29.1 101 | internal static float speedMultiplier = 1f; 102 | 103 | public float JumpDistance { get; } 104 | public float MinJumpDistance { get; } 105 | public float NJS { get; } 106 | public float ReactionTime { get; } 107 | public float MinReactionTime { get; } 108 | 109 | // Experimental 110 | internal float MinRTSlider { get; } 111 | internal float MaxRTSlider { get; } 112 | 113 | internal float MinJDSlider { get; } 114 | internal float MaxJDSlider { get; } 115 | 116 | // 1.26.0-1.29.0 Feature update 117 | public float Offset { get; } 118 | internal float JDOffsetQuantum { get; } 119 | internal float RTOffsetQuantum { get; } 120 | } 121 | } -------------------------------------------------------------------------------- /BeatmapOffsets.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace JDFixer 4 | { 5 | internal static class BeatmapOffsets 6 | { 7 | internal static List JD_Snap_Points = new List(); 8 | internal static List RT_Snap_Points = new List(); 9 | 10 | internal static List JD_Offset_Points = new List(); 11 | internal static List RT_Offset_Points = new List(); 12 | 13 | internal static float jd_snap_value = 0f; 14 | internal static float rt_snap_value = 0f; 15 | 16 | internal static string jd_offset_snap_value = ""; 17 | internal static string rt_offset_snap_value = ""; 18 | 19 | 20 | internal static void Create_Snap_Points(ref List Snap_Points, ref List Offset_Points, float _selectedBeatmap_Offset, float _selectedBeatmap_JD_RT, float _selectedBeatmap_UnitOffset, float _selectedBeatmap_MinSlider, float _selectedBeatmap_MaxSlider) 21 | { 22 | //Plugin.Log.Debug("Create Snap Points"); 23 | //Plugin.Log.Debug("Min: " + _selectedBeatmap_MinSlider + " " + _selectedBeatmap_MaxSlider); 24 | 25 | Snap_Points.Clear(); 26 | Snap_Points.Add(_selectedBeatmap_JD_RT); 27 | 28 | Offset_Points.Clear(); 29 | Offset_Points.Add("( 0, " + _selectedBeatmap_Offset.ToString("0.##") + " )"); 30 | 31 | float point = _selectedBeatmap_JD_RT + _selectedBeatmap_UnitOffset; 32 | int multiple = 1; 33 | while (point <= _selectedBeatmap_MaxSlider) 34 | { 35 | Snap_Points.Add(point); 36 | point += _selectedBeatmap_UnitOffset; 37 | 38 | Offset_Points.Add("( " + multiple + "/" + PluginConfig.Instance.offset_fraction + ", " + (_selectedBeatmap_Offset + multiple / PluginConfig.Instance.offset_fraction).ToString("0.##") + " )"); 39 | multiple += 1; 40 | } 41 | 42 | point = _selectedBeatmap_JD_RT - _selectedBeatmap_UnitOffset; 43 | multiple = -1; 44 | while (point >= _selectedBeatmap_MinSlider) 45 | { 46 | Snap_Points.Insert(0, point); 47 | point -= _selectedBeatmap_UnitOffset; 48 | 49 | Offset_Points.Insert(0, "( " + multiple + "/" + PluginConfig.Instance.offset_fraction + ", " + (_selectedBeatmap_Offset + multiple / PluginConfig.Instance.offset_fraction).ToString("0.##") + " )"); 50 | multiple -= 1; 51 | } 52 | 53 | // Debug: 54 | /*for (int i = 0; i < Snap_Points.Count; i++) 55 | { 56 | Plugin.Log.Debug(i + ": " + Snap_Points[i]); 57 | Plugin.Log.Debug(i + ": " + Offset_Points[i]); 58 | }*/ 59 | } 60 | 61 | 62 | internal static void Calculate_Nearest_JD_Snap_Point(float JD_Value) 63 | { 64 | //Plugin.Log.Debug("Count: " + JD_Snap_Points.Count + " " + JD_Value); 65 | 66 | if (JD_Snap_Points.Count == 0) 67 | { 68 | //Plugin.Log.Debug("empty: " + JD_Value); 69 | 70 | jd_offset_snap_value = ""; 71 | jd_snap_value = JD_Value; 72 | 73 | return; 74 | } 75 | 76 | for (int i = 0; i < JD_Snap_Points.Count; i++) 77 | { 78 | //Plugin.Log.Debug(i + ": " + JD_Snap_Points[i]); 79 | 80 | if (JD_Snap_Points[i] >= JD_Value) 81 | { 82 | jd_offset_snap_value = JD_Offset_Points[i]; 83 | jd_snap_value = JD_Snap_Points[i]; 84 | 85 | return; 86 | } 87 | } 88 | 89 | jd_offset_snap_value = JD_Offset_Points[JD_Offset_Points.Count - 1]; 90 | jd_snap_value = JD_Snap_Points[JD_Snap_Points.Count - 1]; 91 | } 92 | 93 | 94 | internal static void Calculate_Nearest_RT_Snap_Point(float RT_Value) 95 | { 96 | //Plugin.Log.Debug("Count: " + RT_Snap_Points.Count + " " + RT_Value); 97 | 98 | if (RT_Snap_Points.Count == 0) 99 | { 100 | //Plugin.Log.Debug("empty: " + RT_Value); 101 | 102 | rt_offset_snap_value = ""; 103 | rt_snap_value = RT_Value; 104 | 105 | return; 106 | } 107 | 108 | for (int i = 0; i < RT_Snap_Points.Count; i++) 109 | { 110 | //Plugin.Log.Debug(i + ": " + RT_Snap_Points[i]); 111 | 112 | if (RT_Snap_Points[i] >= RT_Value) 113 | { 114 | rt_offset_snap_value = RT_Offset_Points[i]; 115 | rt_snap_value = RT_Snap_Points[i]; 116 | 117 | return; 118 | } 119 | } 120 | 121 | rt_offset_snap_value = RT_Offset_Points[RT_Offset_Points.Count - 1]; 122 | rt_snap_value = RT_Snap_Points[RT_Snap_Points.Count - 1]; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /BeatmapUtils.cs: -------------------------------------------------------------------------------- 1 | namespace JDFixer 2 | { 3 | public static class BeatmapUtils 4 | { 5 | public static float CalculateJumpDistance(float bpm, float njs, float offset) 6 | { 7 | float jumpdistance = 0f; // In case 8 | float halfjump = 4f; 9 | float num = 60f / bpm; 10 | 11 | // Need to repeat this here even tho it's in BeatmapInfo because sometimes we call this function directly 12 | if (njs <= 0.01) // Is it ok to == a 0f? 13 | njs = 10f; 14 | 15 | while (njs * num * halfjump > 17.999) 16 | halfjump /= 2; 17 | 18 | halfjump += offset; 19 | if (halfjump < 0.25f) 20 | halfjump = 0.25f; 21 | 22 | jumpdistance = njs * num * halfjump * 2; 23 | 24 | return jumpdistance; 25 | } 26 | 27 | // Cant make these public set in BeatmapInfo, crashes 28 | /*public static void RefreshSliderMinMax(float njs) 29 | { 30 | if (PluginConfig.Instance.slider_setting == 0) 31 | { 32 | BeatmapInfo.Selected.MinRTSlider = PluginConfig.Instance.minJumpDistance * 500 / njs; 33 | BeatmapInfo.Selected.MaxRTSlider = PluginConfig.Instance.maxJumpDistance * 500 / njs; 34 | 35 | BeatmapInfo.Selected.MinJDSlider = PluginConfig.Instance.minJumpDistance; 36 | BeatmapInfo.Selected.MaxJDSlider = PluginConfig.Instance.maxJumpDistance; 37 | } 38 | else 39 | { 40 | BeatmapInfo.Selected.MinRTSlider = PluginConfig.Instance.minReactionTime; 41 | BeatmapInfo.Selected.MaxRTSlider = PluginConfig.Instance.maxReactionTime; 42 | 43 | BeatmapInfo.Selected.MinJDSlider = PluginConfig.Instance.minReactionTime * njs / 500; 44 | BeatmapInfo.Selected.MaxJDSlider = PluginConfig.Instance.maxReactionTime * njs / 500; 45 | } 46 | }*/ 47 | 48 | 49 | internal static string Calculate_ReactionTime_Setpoint_String(float JD_Value, float _selectedBeatmap_NJS) 50 | { 51 | // Super hack way to prevent divide by zero and showing as "infinity" in Campaign 52 | // Realistically how many maps will have less than 0.002 NJS, and if a map does... 53 | // it wouldn't matter if you display 10^6 or 0 reaction time anyway 54 | // 0.002 gives a margin: BeatmapInfo sets null to 0.001 55 | if (_selectedBeatmap_NJS > 0.002) 56 | return "<#cc99ff>" + (JD_Value / (2 * _selectedBeatmap_NJS) * 1000).ToString("0") + " ms"; 57 | 58 | return "<#cc99ff>0 ms"; 59 | } 60 | 61 | internal static float Calculate_ReactionTime_Setpoint_Float(float JD_Value, float _selectedBeatmap_NJS) 62 | { 63 | if (_selectedBeatmap_NJS > 0.002) 64 | return JD_Value / (2 * _selectedBeatmap_NJS) * 1000; 65 | 66 | return 0f; 67 | } 68 | 69 | 70 | internal static string Calculate_JumpDistance_Setpoint_String(float RT_Value, float _selectedBeatmap_NJS) 71 | { 72 | return "<#ffff00>" + (RT_Value * (2 * _selectedBeatmap_NJS) / 1000).ToString("0.#"); 73 | } 74 | 75 | internal static float Calculate_JumpDistance_Setpoint_Float(float RT_Value, float _selectedBeatmap_NJS) 76 | { 77 | return RT_Value * (2 * _selectedBeatmap_NJS) / 1000; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /HarmonyPatches.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace JDFixer 6 | { 7 | [HarmonyPatch(typeof(VariableMovementDataProvider), "Init")] 8 | internal class VariableMovementDataProviderPatch 9 | { 10 | internal static void Prefix(ref float noteJumpMovementSpeed, float bpm, ref BeatmapObjectSpawnMovementData.NoteJumpValueType noteJumpValueType, ref float noteJumpValue) 11 | { 12 | if (PluginConfig.Instance.enabled == false) 13 | { 14 | return; 15 | } 16 | 17 | Plugin.Log.Debug("Start Map"); 18 | 19 | // BS 1.19.0 20 | noteJumpValueType = BeatmapObjectSpawnMovementData.NoteJumpValueType.BeatOffset; 21 | 22 | // Bit of an issue... to calculate the map's original JD for the heuristic, we need the map's offset 23 | // This was fine when Init had noteJumpStartBeatOffset, but that's replaced with noteJumpValue. 24 | // noteJumpValue is only equal to the offset when the base game settings is Dynamic Default. 25 | 26 | // Will just have to make a note to users as instructions. Not worth trying to find the map when in TA, Campaigns or MP 27 | float noteJumpStartBeatOffset = noteJumpValue; 28 | 29 | float mapNJS = noteJumpMovementSpeed; 30 | Plugin.Log.Debug("mapNJS:" + mapNJS.ToString()); 31 | 32 | if (mapNJS <= 0.01) // Just in case? 33 | mapNJS = 10; 34 | 35 | // JD setpoint from Slider 36 | // 1.19.1 37 | float desiredJumpDis; 38 | 39 | if (PluginConfig.Instance.slider_setting == 0) 40 | { 41 | desiredJumpDis = PluginConfig.Instance.jumpDistance; 42 | } 43 | else 44 | { 45 | desiredJumpDis = PluginConfig.Instance.reactionTime * mapNJS / 500; 46 | } 47 | 48 | // 1.26.0-1.29.0 Feature update 49 | if (PluginConfig.Instance.use_offset && PluginConfig.Instance.legacy_display_enabled && 50 | PluginConfig.Instance.use_rt_pref == false && PluginConfig.Instance.use_jd_pref == false) 51 | { 52 | if (PluginConfig.Instance.slider_setting == 0) 53 | { 54 | desiredJumpDis = BeatmapOffsets.jd_snap_value; 55 | } 56 | else 57 | { 58 | desiredJumpDis = BeatmapOffsets.rt_snap_value * mapNJS / 500; 59 | } 60 | } 61 | 62 | // NJS-RT setpoints from Preferences 63 | if (PluginConfig.Instance.use_rt_pref) 64 | { 65 | if (mapNJS <= PluginConfig.Instance.lower_threshold || mapNJS >= PluginConfig.Instance.upper_threshold) 66 | { 67 | Plugin.Log.Debug("Using Threshold"); 68 | 69 | //return; 70 | desiredJumpDis = BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset); 71 | goto SongSpeed; // Yes, a goto. 72 | } 73 | 74 | var rt_pref = PluginConfig.Instance.rt_preferredValues.FirstOrDefault(x => x.njs <= mapNJS); 75 | Plugin.Log.Debug("Using Preference"); 76 | 77 | if (rt_pref != null) 78 | desiredJumpDis = rt_pref.reactionTime * mapNJS / 500; 79 | 80 | if (BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset) <= desiredJumpDis && PluginConfig.Instance.use_heuristic == 1) 81 | { 82 | Plugin.Log.Debug("Not Fixing: Original JD below or equal setpoint"); 83 | Plugin.Log.Debug($"BPM/NJS/Offset {bpm}/{noteJumpMovementSpeed}/{noteJumpStartBeatOffset}"); 84 | 85 | //return; 86 | desiredJumpDis = BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset); 87 | goto SongSpeed; 88 | } 89 | } 90 | 91 | // NJS-JD setpoints from Preferences 92 | else if (PluginConfig.Instance.use_jd_pref) 93 | { 94 | if (mapNJS <= PluginConfig.Instance.lower_threshold || mapNJS >= PluginConfig.Instance.upper_threshold) 95 | { 96 | Plugin.Log.Debug("Using Threshold"); 97 | Plugin.Log.Debug("selected:" + BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset)); 98 | //return; 99 | desiredJumpDis = BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset); 100 | goto SongSpeed; 101 | } 102 | 103 | var pref = PluginConfig.Instance.preferredValues.FirstOrDefault(x => x.njs <= mapNJS); 104 | Plugin.Log.Debug("Using Preference"); 105 | 106 | if (pref != null) 107 | desiredJumpDis = pref.jumpDistance; 108 | 109 | // Heuristic: If map's original JD is less than the matching preference entry, play map at original JD 110 | // Rationale: I created this mod because I don't like floaty maps. If the original JD chosen by the 111 | // mapper is lower than my pick, it's probably more optimal than my pick. 112 | if (BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset) <= desiredJumpDis && PluginConfig.Instance.use_heuristic == 1) 113 | { 114 | Plugin.Log.Debug("Not Fixing: Original JD below or equal setpoint"); 115 | Plugin.Log.Debug($"BPM/NJS/Offset {bpm}/{noteJumpMovementSpeed}/{noteJumpStartBeatOffset}"); 116 | 117 | //return; 118 | desiredJumpDis = BeatmapUtils.CalculateJumpDistance(bpm, mapNJS, noteJumpStartBeatOffset); 119 | goto SongSpeed; 120 | } 121 | } 122 | 123 | // 1.29.1 124 | SongSpeed: 125 | desiredJumpDis = SpawnMovementDataUpdateHelper.Get_Modified_DesiredJD(desiredJumpDis, mapNJS); 126 | 127 | // Calculate New Offset Given Desired JD: 128 | float simOffset = 0; 129 | float numCurr = 60f / bpm; 130 | float num2Curr = 4f; 131 | 132 | while (mapNJS * numCurr * num2Curr > 17.999) 133 | num2Curr /= 2f; 134 | 135 | if (num2Curr < 0.25f) 136 | num2Curr = 0.25f; 137 | 138 | float jumpDurCurr = num2Curr * numCurr * 2f; 139 | float jumpDisCurr = mapNJS * jumpDurCurr; 140 | 141 | float desiredJumpDur = desiredJumpDis / mapNJS; 142 | float desiredHalfJumpDur = desiredJumpDur / 2f / num2Curr; 143 | float jumpDurMul = desiredJumpDur / jumpDurCurr; 144 | 145 | simOffset = (num2Curr * jumpDurMul) - num2Curr; 146 | 147 | //noteJumpStartBeatOffset = simOffset; 148 | noteJumpValue = simOffset; // 1.19.0+ 149 | 150 | //Plugin.Log.Debug($"HalfJumpCurrent: {num2Curr} | DesiredHalfJump {desiredHalfJumpDur} | DesiredJumpDis {desiredJumpDis} | CurrJumpDis {jumpDisCurr} | Simulated Offset {simOffset}"); 151 | Plugin.Log.Debug($"DesiredJumpDis {desiredJumpDis} | Simulated Offset {simOffset}"); 152 | } 153 | } 154 | 155 | 156 | [HarmonyPatch(typeof(MissionSelectionMapViewController), "SongPlayerCrossfadeToLevelAsync")] 157 | internal class MissionSelectionPatch 158 | { 159 | internal static BeatmapLevel cc_level = null; 160 | internal static void Postfix(BeatmapLevel level) 161 | { 162 | cc_level = level; 163 | } 164 | } 165 | 166 | 167 | [HarmonyPatch] 168 | internal class StandardLevelScenesTransitionSetupDataSOPatch 169 | { 170 | private static MethodBase TargetMethod() => AccessTools.FirstMethod(typeof(StandardLevelScenesTransitionSetupDataSO), 171 | m => m.Name == nameof(StandardLevelScenesTransitionSetupDataSO.Init) && 172 | m.GetParameters().All(p => p.ParameterType != typeof(IBeatmapLevelData))); 173 | 174 | internal static void Postfix(GameplayModifiers gameplayModifiers, PracticeSettings practiceSettings) 175 | { 176 | BeatmapInfo.speedMultiplier = gameplayModifiers.songSpeedMul; 177 | if (practiceSettings != null) 178 | { 179 | BeatmapInfo.speedMultiplier = practiceSettings.songSpeedMul; 180 | } 181 | } 182 | } 183 | 184 | 185 | [HarmonyPatch(typeof(MultiplayerLevelScenesTransitionSetupDataSO), "Init")] 186 | internal class MultiplayerLevelScenesTransitionSetupDataSOPatch 187 | { 188 | internal static void Postfix(GameplayModifiers gameplayModifiers) 189 | { 190 | BeatmapInfo.speedMultiplier = gameplayModifiers.songSpeedMul; 191 | } 192 | } 193 | 194 | 195 | internal class SpawnMovementDataUpdateHelper 196 | { 197 | internal static float Get_Modified_DesiredJD(float jumpDis, float mapNJS) 198 | { 199 | float new_RT = BeatmapUtils.Calculate_ReactionTime_Setpoint_Float(jumpDis, mapNJS) * BeatmapInfo.speedMultiplier; 200 | 201 | if (PluginConfig.Instance.song_speed_setting == 1) 202 | { 203 | return BeatmapUtils.Calculate_JumpDistance_Setpoint_Float(new_RT, mapNJS); 204 | } 205 | else if (PluginConfig.Instance.song_speed_setting == 2 && 206 | (PluginConfig.Instance.use_rt_pref || (PluginConfig.Instance.slider_setting == 1 && PluginConfig.Instance.use_jd_pref == false))) 207 | { 208 | return BeatmapUtils.Calculate_JumpDistance_Setpoint_Float(new_RT, mapNJS); 209 | } 210 | else 211 | { 212 | return jumpDis; 213 | } 214 | } 215 | } 216 | 217 | 218 | // Not supporting 1.19.0 anymore 219 | /*[HarmonyPatch(typeof(CoreMathUtils), "CalculateHalfJumpDurationInBeats")] 220 | internal class CoreMathPatch 221 | { 222 | public static float Postfix(float __result, float startHalfJumpDurationInBeats, float maxHalfJumpDistance, float noteJumpMovementSpeed, float oneBeatDuration, float noteJumpStartBeatOffset) 223 | { 224 | // Force override 1.19.0's BPM lock 225 | if (Plugin.game_version == "1.19.0" && __result < 1.01) 226 | { 227 | float num = startHalfJumpDurationInBeats; 228 | float num2 = noteJumpMovementSpeed * oneBeatDuration; 229 | float num3 = num2 * num; 230 | maxHalfJumpDistance -= 0.001f; 231 | 232 | while (num3 > maxHalfJumpDistance) 233 | { 234 | num /= 2f; 235 | num3 = num2 * num; 236 | } 237 | 238 | num += noteJumpStartBeatOffset; 239 | 240 | if (num < 0.25f) 241 | { 242 | num = 0.25f; 243 | } 244 | 245 | return num; 246 | } 247 | 248 | return __result; 249 | } 250 | }*/ 251 | 252 | 253 | /*internal class TimeControllerPatch 254 | { 255 | private static DateTime af = new DateTime(DateTime.Now.Year, 4, 1); 256 | internal static BeatmapObjectSpawnMovementData.NoteSpawnData Postfix(BeatmapObjectSpawnMovementData.NoteSpawnData __result) 257 | { 258 | if (DateTime.Now >= af && TimeController.audioTime.songTime >= 5f) 259 | { 260 | //float jumpDuration = __result.jumpDuration * (1 + TimeController.audioTime.songTime / TimeController.length * 0.75f); // * 1 might be more funny lol 261 | float jumpDuration = (float)(__result.jumpDuration * (1 + 0.30 * Math.Abs(Math.Sin(4 * Math.PI * (TimeController.audioTime.songTime - 5f) / TimeController.length)))); 262 | //Plugin.Log.Debug("jumpDuration: " + jumpDuration); 263 | 264 | return new BeatmapObjectSpawnMovementData.NoteSpawnData(__result.moveStartPos, __result.moveEndPos, __result.jumpEndPos, __result.jumpGravity, __result.moveDuration, jumpDuration); 265 | } 266 | 267 | return __result; 268 | } 269 | }*/ 270 | } -------------------------------------------------------------------------------- /Installers/JDFixerMenuInstaller.cs: -------------------------------------------------------------------------------- 1 | using JDFixer.Managers; 2 | using JDFixer.UI; 3 | using Zenject; 4 | 5 | namespace JDFixer.Installers 6 | { 7 | internal sealed class JDFixerMenuInstaller : Installer 8 | { 9 | public override void InstallBindings() 10 | { 11 | Container.BindInterfacesTo().AsSingle(); 12 | Container.BindInterfacesTo().AsSingle(); 13 | Container.BindInterfacesTo().AsSingle(); 14 | 15 | if (PluginConfig.Instance.legacy_display_enabled) 16 | { 17 | Container.UnbindInterfacesTo(); 18 | Container.BindInterfacesTo().AsSingle(); 19 | } 20 | else 21 | { 22 | Container.UnbindInterfacesTo(); 23 | Container.BindInterfacesTo().AsSingle(); 24 | } 25 | 26 | // Flow Coordinators need to binded like this, as a component since it is a Unity Component 27 | Container.Bind().FromNewComponentOnNewGameObject().AsSingle(); 28 | 29 | // Even though ViewControllers are also Unity Components, we bind them with this helper method provided by SiraUtil (FromNewComponentAsViewController) 30 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 31 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 32 | } 33 | } 34 | 35 | internal sealed class JDFixerTimeInstaller : Installer 36 | { 37 | public override void InstallBindings() 38 | { 39 | //Container.Bind().FromNewComponentOnNewGameObject().AsSingle(); 40 | Container.InstantiateComponentOnNewGameObject(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Interfaces/IBeatmapInfoUpdater.cs: -------------------------------------------------------------------------------- 1 | namespace JDFixer.Interfaces 2 | { 3 | internal interface IBeatmapInfoUpdater 4 | { 5 | void BeatmapInfoUpdated(BeatmapInfo beatmapInfo); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /JDFixer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {525BE9DC-73E5-41A1-A619-ED09C21C562A} 8 | Library 9 | Properties 10 | JDFixer 11 | JDFixer 12 | v4.8 13 | 512 14 | 15 | C:\Program Files (x86)\Steam\steamapps\common\Beat Saber 16 | 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | false 28 | 29 | 30 | none 31 | true 32 | bin\Release\ 33 | 34 | 35 | prompt 36 | 4 37 | false 38 | 39 | 40 | 41 | False 42 | $(BeatSaberDir)\Libs\0Harmony.dll 43 | 44 | 45 | False 46 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll 47 | 48 | 49 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatSaber.ViewSystem.dll 50 | False 51 | False 52 | 53 | 54 | $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.AppFlow.dll 55 | False 56 | 57 | 58 | False 59 | $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.UnityExtension.dll 60 | False 61 | 62 | 63 | $(BeatSaberDir)\Plugins\BSML.dll 64 | 65 | 66 | False 67 | $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll 68 | False 69 | 70 | 71 | False 72 | $(BeatSaberDir)\Beat Saber_Data\Managed\Core.dll 73 | 74 | 75 | $(BeatSaberDir)\Plugins\Custom Campaigns.dll 76 | False 77 | False 78 | 79 | 80 | False 81 | $(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll 82 | False 83 | 84 | 85 | False 86 | $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll 87 | 88 | 89 | 90 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll 91 | 92 | 93 | False 94 | $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll 95 | 96 | 97 | $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll 98 | 99 | 100 | False 101 | $(BeatSaberDir)\Beat Saber_Data\Managed\MediaLoader.dll 102 | False 103 | 104 | 105 | False 106 | $(BeatSaberDir)\Libs\Newtonsoft.Json.dll 107 | 108 | 109 | $(BeatSaberDir)\Plugins\SiraUtil.dll 110 | False 111 | False 112 | 113 | 114 | False 115 | $(BeatSaberDir)\Plugins\SongCore.dll 116 | False 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | False 136 | $(BeatSaberDir)\Beat Saber_Data\Managed\Tweening.dll 137 | False 138 | 139 | 140 | $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll 141 | 142 | 143 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll 144 | 145 | 146 | False 147 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.AnimationModule.dll 148 | 149 | 150 | False 151 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.AudioModule.dll 152 | 153 | 154 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll 155 | 156 | 157 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.InputLegacyModule.dll 158 | 159 | 160 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll 161 | 162 | 163 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll 164 | 165 | 166 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll 167 | 168 | 169 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll 170 | 171 | 172 | False 173 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.XRModule.dll 174 | 175 | 176 | False 177 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll 178 | 179 | 180 | False 181 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll 182 | False 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | True 205 | True 206 | Resources.resx 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | ResXFileCodeGenerator 215 | Resources1.Designer.cs 216 | Designer 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 2.0.0-beta4 240 | runtime; build; native; contentfiles; analyzers; buildtransitive 241 | all 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /JDFixer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31321.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JDFixer", "JDFixer.csproj", "{525BE9DC-73E5-41A1-A619-ED09C21C562A}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {525BE9DC-73E5-41A1-A619-ED09C21C562A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {525BE9DC-73E5-41A1-A619-ED09C21C562A}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {525BE9DC-73E5-41A1-A619-ED09C21C562A}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {525BE9DC-73E5-41A1-A619-ED09C21C562A}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {D0F0600F-5D8C-4886-A89F-3421C5295B39} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Managers/JDFixerUIManager.cs: -------------------------------------------------------------------------------- 1 | using CustomCampaigns.Campaign.Missions; 2 | using JDFixer.Interfaces; 3 | using System; 4 | using System.Collections.Generic; 5 | using Zenject; 6 | 7 | 8 | namespace JDFixer.Managers 9 | { 10 | internal class JDFixerUIManager : IInitializable, IDisposable 11 | { 12 | private static StandardLevelDetailViewController levelDetail; 13 | private static MissionSelectionMapViewController missionSelection; 14 | private static BeatmapLevelsModel levelsModel; 15 | private static MainMenuViewController mainMenu; 16 | 17 | private readonly List beatmapInfoUpdaters; 18 | 19 | 20 | [Inject] 21 | private JDFixerUIManager(StandardLevelDetailViewController standardLevelDetailViewController, MissionSelectionMapViewController missionSelectionMapViewController, BeatmapLevelsModel beatmapLevelsModel, MainMenuViewController mainMenuViewController, List iBeatmapInfoUpdaters) 22 | { 23 | //Plugin.Log.Debug("JDFixerUIManager()"); 24 | 25 | levelDetail = standardLevelDetailViewController; 26 | missionSelection = missionSelectionMapViewController; 27 | levelsModel = beatmapLevelsModel; 28 | mainMenu = mainMenuViewController; 29 | 30 | beatmapInfoUpdaters = iBeatmapInfoUpdaters; 31 | } 32 | 33 | 34 | public void Initialize() 35 | { 36 | //Plugin.Log.Debug("Initialize()"); 37 | 38 | levelDetail.didChangeDifficultyBeatmapEvent += LevelDetail_didChangeDifficultyBeatmapEvent; 39 | levelDetail.didChangeContentEvent += LevelDetail_didChangeContentEvent; 40 | 41 | if (Plugin.CheckForCustomCampaigns()) 42 | { 43 | missionSelection.didSelectMissionLevelEvent += MissionSelection_didSelectMissionLevelEvent_CC; 44 | } 45 | else 46 | { 47 | missionSelection.didSelectMissionLevelEvent += MissionSelection_didSelectMissionLevelEvent_Base; 48 | } 49 | 50 | mainMenu.didDeactivateEvent += MainMenu_didDeactivateEvent; ; 51 | } 52 | 53 | 54 | public void Dispose() 55 | { 56 | //Plugin.Log.Debug("Dispose()"); 57 | 58 | levelDetail.didChangeDifficultyBeatmapEvent -= LevelDetail_didChangeDifficultyBeatmapEvent; 59 | levelDetail.didChangeContentEvent -= LevelDetail_didChangeContentEvent; 60 | 61 | missionSelection.didSelectMissionLevelEvent -= MissionSelection_didSelectMissionLevelEvent_CC; 62 | missionSelection.didSelectMissionLevelEvent -= MissionSelection_didSelectMissionLevelEvent_Base; 63 | 64 | mainMenu.didDeactivateEvent -= MainMenu_didDeactivateEvent; 65 | } 66 | 67 | 68 | private void LevelDetail_didChangeDifficultyBeatmapEvent(StandardLevelDetailViewController arg1) 69 | { 70 | //Plugin.Log.Debug("LevelDetail_didChangeDifficultyBeatmapEvent()"); 71 | 72 | if (arg1 != null) 73 | { 74 | DiffcultyBeatmapUpdated(arg1.beatmapKey, arg1.beatmapLevel); 75 | } 76 | } 77 | 78 | 79 | private void LevelDetail_didChangeContentEvent(StandardLevelDetailViewController arg1, StandardLevelDetailViewController.ContentType arg2) 80 | { 81 | //Plugin.Log.Debug("LevelDetail_didChangeContentEvent()"); 82 | 83 | if (arg1 != null && arg1.beatmapLevel != null)//selectedDifficultyBeatmap != null) 84 | { 85 | //Plugin.Log.Debug("NJS: " + arg1.selectedDifficultyBeatmap.noteJumpMovementSpeed); 86 | //Plugin.Log.Debug("Offset: " + arg1.selectedDifficultyBeatmap.noteJumpStartBeatOffset); 87 | 88 | DiffcultyBeatmapUpdated(arg1.beatmapKey, arg1.beatmapLevel); //selectedDifficultyBeatmap); 89 | } 90 | } 91 | 92 | 93 | private void MissionSelection_didSelectMissionLevelEvent_CC(MissionSelectionMapViewController arg1, MissionNode arg2) 94 | { 95 | // Yes, we must check for both arg2.missionData and arg2.missionData.beatmapCharacteristic: 96 | // If a map is not dled, missionID and beatmapDifficulty will be correct, but beatmapCharacteristic will be null 97 | // Accessing any null values of arg1 or arg2 will crash CC horribly 98 | 99 | if (arg2.missionData != null && arg2.missionData.beatmapCharacteristic != null) 100 | { 101 | Plugin.Log.Debug("In CC, MissionNode exists"); 102 | 103 | //Plugin.Log.Debug("MissionNode - missionid: " + arg2.missionId); //"[STND] Holdin' Oneb28Easy-1" 104 | //Plugin.Log.Debug("MissionNode - difficulty: " + arg2.missionData.beatmapDifficulty); // "Easy" etc 105 | //Plugin.Log.Debug("MissionNode - characteristic: " + arg2.missionData.beatmapCharacteristic.serializedName); //"Standard" etc 106 | 107 | if (MissionSelectionPatch.cc_level != null) // lol null check just to print? 108 | { 109 | // If a map is not dled, this will be the previous selected node's map 110 | Plugin.Log.Debug("CC Level: " + MissionSelectionPatch.cc_level.levelID); // For cross check with arg2.missionId 111 | 112 | if (arg2.missionData is CustomMissionDataSO) 113 | { 114 | BeatmapLevel beatmapLevel = (arg2.missionData as CustomMissionDataSO).beatmapLevel; 115 | 116 | if (beatmapLevel != null) // lol null check just to print? 117 | { 118 | DiffcultyBeatmapUpdated(arg2.missionData.beatmapKey, beatmapLevel); 119 | } 120 | } 121 | } 122 | } 123 | else // Map not dled 124 | { 125 | DiffcultyBeatmapUpdated(new BeatmapKey(), null); 126 | } 127 | } 128 | 129 | 130 | private void MissionSelection_didSelectMissionLevelEvent_Base(MissionSelectionMapViewController arg1, MissionNode arg2) 131 | { 132 | // Base campaign 133 | if (arg2 != null) 134 | { 135 | DiffcultyBeatmapUpdated(arg2.missionData.beatmapKey, levelsModel.GetBeatmapLevel(arg2.missionData.beatmapKey.levelId)); 136 | } 137 | } 138 | 139 | 140 | private void MainMenu_didDeactivateEvent(bool removedFromHierarchy, bool screenSystemDisabling) 141 | { 142 | //Plugin.Log.Debug("MainMenu_didDeactivate"); 143 | 144 | if (UI.LegacyModifierUI.Instance != null) 145 | { 146 | UI.LegacyModifierUI.Instance.Refresh(); 147 | } 148 | 149 | if (UI.ModifierUI.Instance != null) 150 | { 151 | UI.ModifierUI.Instance.Refresh(); 152 | } 153 | 154 | if (UI.CustomOnlineUI.Instance != null) 155 | { 156 | UI.CustomOnlineUI.Instance.Refresh(); 157 | } 158 | } 159 | 160 | 161 | private void DiffcultyBeatmapUpdated(BeatmapKey beatmapKey, BeatmapLevel beatmapLevel) 162 | { 163 | //Plugin.Log.Debug("DiffcultyBeatmapUpdated()"); 164 | 165 | foreach (var beatmapInfoUpdater in beatmapInfoUpdaters) 166 | { 167 | beatmapInfoUpdater.BeatmapInfoUpdated(new BeatmapInfo(beatmapKey, beatmapLevel)); 168 | } 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 |  JDFixer 2 | Copyright (C) 2023 Zeph-yr 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published 6 | by the Free Software Foundation version 3 of the License with Additional Terms. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | 16 | As special exceptions the Additional Terms on the work licensed herein, 17 | pursuant to Section 7 of Affero General Public License are as follows: 18 | 19 | 1. If you distribute any portion or version of the work, you must license it 20 | under GNU AGPL version 3 with these same additional terms stated in this notice. 21 | You may not supplement the terms of the license with any additional terms. 22 | 23 | 2. Pursuant to AGPLv3, Section 7 (b): You must retain and display all copyright 24 | and attribution notices indicating the work was created "by Zephyr#9125" and the 25 | location of original repository "https://github.com/zeph-yr/JDFixer". You may 26 | not modify or remove information relating to the author, including donation 27 | information, from the work. 28 | 29 | 3. Pursuant to AGPLv3, Section 7 (d): No Trademark License - This license 30 | does not grant you rights to use any contributors' name, logo, or trademarks 31 | beyond the reasonable extent of using any contributors' names for the purpose 32 | of attribution. 33 | 34 | Contents in the "Screenshots" directory are not part of the Program and 35 | remain the exclusive property of their copyright holders. 36 | Attributions for older versions of JDFixer are in the "Notices" directory. -------------------------------------------------------------------------------- /Notices/JFDIXER.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Zeph-yr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Notices/NjsFixer.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kylemc1413 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plugin.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using IPA; 3 | using IPA.Config; 4 | using IPA.Config.Stores; 5 | using IPA.Loader; 6 | using IPALogger = IPA.Logging.Logger; 7 | using JDFixer.Installers; 8 | using SiraUtil.Zenject; 9 | 10 | namespace JDFixer 11 | { 12 | [Plugin(RuntimeOptions.DynamicInit)] 13 | public sealed class Plugin 14 | { 15 | public static Harmony harmony; 16 | //internal static string game_version = ""; 17 | 18 | internal static IPALogger Log { get; private set; } 19 | 20 | [Init] 21 | public Plugin(IPALogger logger, Config conf, Zenjector zenjector) 22 | { 23 | Plugin.Log = logger; 24 | PluginConfig.Instance = conf.Generated(); 25 | 26 | zenjector.Install(Location.Menu); 27 | //TimeSetup.Inject(zenjector); 28 | } 29 | 30 | 31 | [OnEnable] 32 | public void OnApplicationStart() 33 | { 34 | //Plugin.Log.Debug("OnApplicationStart()"); 35 | //game_version = IPA.Utilities.UnityGame.GameVersion.ToString(); 36 | //Plugin.Log.Debug(game_version); 37 | 38 | harmony = new Harmony("com.zephyr.BeatSaber.JDFixer"); 39 | //TimeSetup.Patch(); 40 | harmony.PatchAll(System.Reflection.Assembly.GetExecutingAssembly()); 41 | CheckForCustomCampaigns(); 42 | UI.Donate.Refresh_Text(); 43 | } 44 | 45 | 46 | [OnDisable] 47 | public void OnApplicationQuit() 48 | { 49 | PluginConfig.Instance.Changed(); 50 | harmony.UnpatchSelf(); 51 | } 52 | 53 | 54 | internal static bool CheckForCustomCampaigns() 55 | { 56 | var cc_installed = PluginManager.GetPluginFromId("CustomCampaigns"); 57 | Plugin.Log.Debug("CC installed: " + cc_installed); 58 | 59 | return cc_installed != null; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /PluginConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | using IPA.Config.Stores; 4 | using IPA.Config.Stores.Attributes; 5 | using IPA.Config.Stores.Converters; 6 | 7 | 8 | [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)] 9 | namespace JDFixer 10 | { 11 | internal class PluginConfig 12 | { 13 | internal static PluginConfig Instance { get; set; } 14 | 15 | internal virtual bool enabled { get; set; } = false; 16 | 17 | 18 | internal float jumpDistance { get; set; } = 24f; 19 | internal virtual int minJumpDistance { get; set; } = 12; 20 | internal virtual int maxJumpDistance { get; set; } = 35; 21 | internal virtual bool use_jd_pref { get; set; } = false; 22 | 23 | [UseConverter(typeof(ListConverter))] 24 | [NonNullable] 25 | internal virtual List preferredValues { get; set; } = new List(); 26 | 27 | 28 | internal float reactionTime { get; set; } = 500f; 29 | internal virtual int minReactionTime { get; set; } = 300; 30 | internal virtual int maxReactionTime { get; set; } = 1600; 31 | internal virtual bool use_rt_pref { get; set; } = false; 32 | 33 | [UseConverter(typeof(ListConverter))] 34 | [NonNullable] 35 | internal virtual List rt_preferredValues { get; set; } = new List(); 36 | 37 | 38 | //1.19.1 Feature update 39 | internal virtual int slider_setting { get; set; } = 0; 40 | internal virtual int pref_selected { get; set; } = 0; 41 | 42 | internal virtual int use_heuristic { get; set; } = 0; 43 | internal virtual float lower_threshold { get; set; } = 1f; 44 | internal virtual float upper_threshold { get; set; } = 100f; 45 | 46 | internal virtual bool rt_display_enabled { get; set; } = true; 47 | internal virtual bool legacy_display_enabled { get; set; } = false; 48 | 49 | //1.26.0-1.29.0 Feature update 50 | internal virtual bool use_offset { get; set; } = false; 51 | internal virtual float offset_fraction { get; set; } = 8f; 52 | 53 | // 1.29.1 54 | internal virtual int song_speed_setting { get; set; } = 0; 55 | 56 | internal bool af_enabled { get; set; } = false; 57 | 58 | 59 | /// 60 | /// Call this to force BSIPA to update the config file. This is also called by BSIPA if it detects the file was modified. 61 | /// 62 | internal virtual void Changed() 63 | { 64 | // Do stuff when the config is changed. 65 | } 66 | 67 | /// 68 | /// Call this to have BSIPA copy the values from into this config. 69 | /// 70 | /*internal virtual void CopyFrom(PluginConfig other) 71 | { 72 | // This instance's members populated from other 73 | }*/ 74 | } 75 | 76 | internal class JDPref 77 | { 78 | internal virtual float njs { get; set; } = 16f; 79 | internal virtual float jumpDistance { get; set; } = 18f; 80 | 81 | public JDPref() 82 | { 83 | 84 | } 85 | 86 | internal JDPref(float njs, float jumpDistance) 87 | { 88 | this.njs = njs; 89 | this.jumpDistance = jumpDistance; 90 | } 91 | } 92 | 93 | // Reaction Time Mode 94 | internal class RTPref 95 | { 96 | internal virtual float njs { get; set; } = 16f; 97 | internal virtual float reactionTime { get; set; } = 800f; 98 | 99 | public RTPref() 100 | { 101 | 102 | } 103 | 104 | internal RTPref(float njs, float reactionTime) 105 | { 106 | this.njs = njs; 107 | this.reactionTime = reactionTime; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("JDFixer")] 9 | [assembly: AssemblyDescription("JDFixer")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("www.xephai.com")] 12 | [assembly: AssemblyProduct("JDFixer")] 13 | [assembly: AssemblyCopyright("Copyright © 2021-2025 Zephyr")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("13df9b09-f080-4bc9-b36b-b97351033c27")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("7.4.0.0")] 36 | [assembly: AssemblyFileVersion("7.4.0.0")] 37 | -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /Properties/Resources1.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace JDFixer.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("JDFixer.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JDFixer 2 | 3 | Was once based on Kylemc1413's NjsFixer but has grown to much more. 4 | 5 | I wanted a stripped down mod that focused only on JD modification to fix floaty maps without NJS/BPM modification since I don't use those features. I felt there was a gap between Njsfixer and Leveltweaks that isn't filled for JD-focused players and this is my interpretation for meeting those needs. 6 | 7 | Supports CustomCampaigns, Tournament Assistant, all flavors of Multiplayer, OST / DLC / Base Campaign. Score posting is unaffected. For Beat Saber 1.17.1+. 8 | 9 | ## New Features 10 | - **Selected map's original JD and RT is displayed.** You can easily decide if you want to use JDFixer without having to play the map to feel it. Saves time. 11 | - **Selected map's lowest JD and RT allowed by the game is displayed.** When it seems like JDFixer "isn't working", check if you exceeded these values shown in grey. 12 | - **Automated JD and RT fixing.** Preferences has been changed to selecting the NJS-JD (or RT) pair that is equal or lower to the selected map's NJS. This allows you to cover large ranges without having to add many values and also handles the rare non-integer NJS. For RT Preferences: Automatically sets a map's JD to give your preferred reaction time for a given NJS. Ability to fix constant JD or RT across all maps (no need to worry about slider getting moved). 13 | - **Heuristic to bypass Preferences** where if the selected map by default runs closer than the JD in the matching NJS-JD pair, the map will run at its original JD (and likewise for RT). 14 | - **Upper and Lower NJS Thresholds where Preferences will be bypassed.** If a map's NJS is at or exceeds thresholds, the map will run at its original JD and RT. 15 | - **Multiple UIs and Options** with choice of linked or single sliders for JD and RT. New purpose-built UI for Tournaments. 16 | - **Mod Settings menu** to easily configure UI options and Thresholds. 17 | - **Snap JD and RT to fractions of a beat.** Run map at JDs and RTs that line up with multiples of a desired beat fraction. 18 | - **Choose whether song speed affects JD or RT settings and Automated Preferences.** Choice of keeping JD settings regardless of song speed, auto-compensating RT based on song speed, or both! Applies to Practice mode and modifiers in Solo / MP / Party. 19 | - **Preserves BeatmapV4 NJS changes.** Runs map at desired JD (or RT based on the map's primary NJS), while allowing NJS to vary over the course of the map as defined by the mapper. 20 | 21 | 22 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26/Screenshots/6.0.0_menu_main_1.png) 23 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26_Offset/Screenshots/v7/7.0.0_mod_settings_3.png) 24 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26/Screenshots/6.0.0_menu_linked_1.png) 25 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26_Offset/Screenshots/v7/7.0.0_menu_unlinked_3.png) 26 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26/Screenshots/6.0.0_menu_unlinked_2.png) 27 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26/Screenshots/6.0.0_preferences.png) 28 | 29 | 30 | ## How To Use 31 | - Place JDFixer.dll in Plugins folder 32 | - Select a map or difficulty to see its original JD and RT 33 | - Set up Automated Preferences to select JD or RT based on map's NJS with in-game menu (If you require finer decimal values for NJS, JD and RT, you can edit the preferences in `/UserData/JDFixer.json`) 34 | - To access the RT Preferences menu, set `Automated Preferences` to `ReactionTime` and click `Configure RT Preferences` 35 | - Setting `Automated Preferences` to `JumpDistance` or `ReactionTime` will override the JD and RT sliders 36 | - If you enable `Bypass Preferences if map is closer`, you **must** set base game settings to `Dynamic Default` 37 | - You can configure `Bypass Preferences if NJS is` equal to, less, or greater than `Lower` and `Upper Thresholds` in Mod Settings (or `/UserData/JDFixer.json` in ≤v5.x.x) 38 | - To enable `Snap JD and RT to beat fraction`, go to Mod Settings and toggle on `Separate JD and RT sliders` and set the fraction's denominator (eg. 1/3rd, 1/4th, 1/64th etc) using the slider 39 | - To configure song speed behavior, go to Mod Settings and choose one of the options. See [below](https://github.com/zeph-yr/JDFixer/edit/BS_1.26_Offset/README.md#understanding-song-speed-options) for a detailed explanation of how each option behaves. 40 | - Hover over menu in-game for additional explanations 41 | - **Requires BSIPA, BSML, and SiraUtil** 42 | - **Not compatible with NjsFixer and LevelTweaks.** Using with these mods may result in conflicts and unexpected behavior. 43 | 44 | ## UI Customization 45 | 1. **Linked JD and RT sliders:** The JD or RT slider will remember its last value when you click between maps. Scroll to the bottom of JDFixer's tab to swap. This is the default UI (v3.1.0+) 46 | 2. **One slider for JD or RT, and a display value for other:** Go to Mod Settings and toggle on `Unlink JD and RT sliders` (for ≤v5.x.x, change `legacy_ui_enabled` in `/UserData/JDFixer.json` to "true"). Scroll to the bottom of the JDFixer's tab to swap between JD and RT 47 | 3. **Show (or hide) the default and minimum Reaction Times for maps:** Go to Mod Settings and toggle on/off `Show map Reaction Times` (for ≤v5.x.x, change `rt_display_enabled` in `/UserData/JDFixer.json` to "false") 48 | 4. **TA and MP dedicated UI:** Choose to set your map by JD or RT. You can only set one at a time **(v4.0.0+)** 49 | 5. Min and max ranges for JD and RT sliders can be changed in `/UserData/JDFixer.json` 50 | 51 |
52 | 53 | 54 | 57 | 60 | 61 |
55 | 56 | 58 | 59 |
62 |
63 | Above Left: Show map Reaction Times is ON. Above Right is OFF. 64 | 65 | ## Understanding Preferences Behavior 66 | Suppose your Jump Distance Preferences contain these NJS-JD pairs: 22-18, 21-16, 18-15. 67 | 68 | **Example 1:** 69 | Your selected map's NJS is 22 and JD is 20. 70 | The map will run at 18 JD because there is an exact match for 22 NJS 71 | 72 | **Example 2:** 73 | Your selected map's NJS is 21.5 and JD is 20. 74 | The map will run at 16 JD because 21 NJS is the closest lower match. 75 | 76 | **Example 3:** 77 | Your selected map's NJS is 21.5 and JD is 14 *and* `Bypass Preferences if map is closer` is toggled ON. 78 | The map will run at its original 14 JD because it is lower than your matching preference (21-16). 79 | 80 | **Example 4:** 81 | Your selected map's NJS is 23 and JD is 20 *and* your `Upper Threshold` is set to 23 NJS. 82 | The map will run at its original 20 JD because it triggered the threhold. 83 | 84 | **Example 5:** 85 | To run every map at a constant JD regardless of its NJS, create a single preference with 0 NJS and your desired JD (e.g. 0 NJS - 18 JD) 86 | 87 | - If `Automated Preferences` is set to `JumpDistance` but no Preferences are set, the map will run at JD and RT slider value 88 | - If you enable the `Bypass Preferences if map is closer` heuristic, you **must** set base game settings to `Dynamic Default`. Failing to do so give you inconsistent results. 89 | - Heuristic and Thresholds override Preferences only 90 | - If you need decimal values for Preferences, you can manually edit them in JDFixer.json. 91 | 92 | **Reaction Time Preferences:** 93 | This works exactly the same as JD Preferences. The five examples above apply, except in Reaction Time. Reaction Time is a function of the map's original NJS and Jump Distance. This means that RT Preferences automatically sets the map's JD to give your preferred RT for its given NJS. 94 | 95 | ## Understanding Snapping to Beat Fractions 96 | To use this feature, toggle on `Separate JD and RT sliders`, `Snap JD and RT to fractions of a beat` and set the `Beat Fraction (1/n)` in Mod Settings. Enabling Automated Preferences overrides the snapped value. **The snap display consists of four values (A, B) JD RT.** 97 | 98 | **Given the current JD or RT slider value:** 99 | - **A:** This is how many fractions of a beat relative to the map's default offset that the map will run at. 100 | - **B:** This is the equivalent absolute offset that the will map run at. 101 | - **JD:** This is the equivalent JD that the map will snap to and run at. 102 | - **RT:** This is the equivalent RT that the map will snap to and run at. 103 | 104 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26_Offset/Screenshots/v7/7.0.0_beat_fraction_2.png) 105 | 106 | ## Understanding Song Speed Options 107 | By base game behavior, maps maintain their JD when played at different song speeds (e.g. when playing with Modifiers or 200% on Practice Mode). This is so logically, higher song speed settings make maps "play faster" and increases the difficulty by lowering the player's reaction time. 108 | 109 | **JDFixer can change this. Choose one of the options:** 110 | - **JD_Settings:** If you prefer the base game behavior or you prefer to always have a known JD (meaning you want to play at the JD you set, at any song speed). This behaves the same as JDFixer v6.0.0 and below and is the default option. 111 | 112 | - **RT_Settings:** If you prefer to always have a known reaction time (and would prefer the JD to be auto-adjusted depending on the song speed to give the RT you set). This means when the map is run at a higher song speed, the JD will be pushed back to give the same RT as if the map is being played at normal speed. Likewise, playing the map at lower song speed will bring the JD closer. 113 | 114 | - **JD_RT_Respectively:** If you prefer to have both of options, depending on whether you are using the JD or RT sliders and Automated Preferencences. This means when the JD slider is active or JD Automated Preferences is enabled, it will behave like the `JD_Settings` option, and when the RT slider is active or RT Automated Preferences is enabled, it will behave like the `RT_Settings` option. 115 | 116 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26_Offset/Screenshots/v7/7.0.0_mod_settings_song_speed_1.png) 117 | 118 | ## Understanding Variable NJS Base Game Mechanic in V4 Maps 119 | By base game behavior, maps can include NJS changes that are preset by the mapper. This allows for blocks to travel to the player at different speeds to create map effects and play experiences. 120 | 121 | **JDFixer does not override this, by design.** 122 | - JDFixer runs the entire map at the desired JD and any and all variable NJS experienced by the player is the same as if they were to play without JDFixer. 123 | - When running a map by RT, JDFixer sets the map's JD to give the desired RT with respect to the map's primary NJS value shown in the Song Info defined by the mapper. This means RT still varies accordingly if the map has variable NJS. Difficulty and speed changes are preserved. 124 | 125 | ## Tournaments and MP 126 | - **Tournament Assistant:** Supports Default, Dual Sync and AutoPause matches. You can only use one of the sliders at a time. As usual, enabling Preferences override both sliders. ***Avoid opening the Preferences menu in TA!*** *You will be stuck in it until you relaunch the game or the coordinator lets you out (this is by design in TA). However if you do choose to get yourself stuck inside just before a match, your match will still play fine when the coordinator starts it (but I hope you've set your Preferences correctly or left the slider in a sane spot lol).* 127 | - **Multiplayer:** Shares the UI with TA. As usual, you can only use one of the sliders at a time and enabling Preferences override both sliders. It is safe to open the Preferences menu here lol. 128 | - **CustomCampaigns:** Comes with display for map's default JD and RT **(v3.1.0+)** 129 | - **OST, DLC Levels and Base Campaign** are fully supported. 130 | 131 |
132 | 133 | 134 | 137 | 140 | 141 |
135 | 136 | 138 | 139 |
142 |
143 | 144 | ![screenshot](https://github.com/zeph-yr/JDFixer/blob/BS_1.26/Screenshots/6.0.0_mp.png) 145 | 146 | ## Versions 147 | - v7.4.0 for BS 1.40.0+ 148 | - v7.3.0 for BS 1.38.0 to 1.39.1 149 | - v7.2.6 for BS 1.36.2 / 1.37.0+ 150 | - v7.2.4 for BS 1.34.2 only 151 | - v7.1.0 for BS 1.26.0 to 1.29.1 152 | - v6.0.0 for BS 1.26.0+ 153 | - v5.x.x for BS 1.20.0+ only 154 | - 4.0.0 for BS 1.19.0 / 1.19.1 155 | - v3.x.x for BS 1.19.0 / 1.19.1 requires SiraUtil 3.0.0+ (Do not use 3.x.x versions for TA or MP) 156 | - ≤v2.1.6 for BS ≤1.18.3 requires BS_Utils 157 | - v2.1.3+ will import your settings file 158 | - v2.1.0 is not compatible with settings files from previous versions: Delete or rename your old JDFixer.json and allow the mod to generate a new one. Re-enter your settings in-game. If you are knowledgeable, you can copy the relevant data from the old json file to the new one. Just make sure you do it correctly. 159 | 160 | ## About 161 | Copyright © 2021 - 2025 Zephyr | www.xephai.com 162 | 163 | This is my first time writing a mod. I made it for my own needs but friends thought it useful so I think it would be beneficial to share it. I hope others find this useful. 164 | 165 | ## Donate and Support 166 | ❤️ Love JDFixer and want to show your support? [Donate on Patreon](https://www.patreon.com/xeph_yr) or [buy a me a ☕](https://ko-fi.com/zeph_yr) and get your name featured in the Supporters panel in-game!❤️ 167 | 168 | ## TYs 169 | Thanks @Shurdoof for autoupdate! 170 | Thanks Kyle for the original [NjsFixer](https://github.com/Kylemc1413/NjsFixer) and thanks to the cool peeps in BSMG for the help and advice when I first started out :) 171 | -------------------------------------------------------------------------------- /Screenshots/2.0.3_menu_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.0.3_menu_1.png -------------------------------------------------------------------------------- /Screenshots/2.0.3_menu_1_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.0.3_menu_1_small.png -------------------------------------------------------------------------------- /Screenshots/2.0.3_menu_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.0.3_menu_2.png -------------------------------------------------------------------------------- /Screenshots/2.0.3_menu_2_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.0.3_menu_2_small.png -------------------------------------------------------------------------------- /Screenshots/2.1.0_menu_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.1.0_menu_1.png -------------------------------------------------------------------------------- /Screenshots/2.1.0_menu_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.1.0_menu_2.png -------------------------------------------------------------------------------- /Screenshots/2.1.0_menu_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/2.1.0_menu_3.png -------------------------------------------------------------------------------- /Screenshots/3.0.0_menu_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.0.0_menu_1.png -------------------------------------------------------------------------------- /Screenshots/3.0.0_menu_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.0.0_menu_2.png -------------------------------------------------------------------------------- /Screenshots/3.0.0_menu_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.0.0_menu_3.png -------------------------------------------------------------------------------- /Screenshots/3.0.0_menu_3_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.0.0_menu_3_small.png -------------------------------------------------------------------------------- /Screenshots/3.0.0_menu_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.0.0_menu_4.png -------------------------------------------------------------------------------- /Screenshots/3.1.0_menu_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.1.0_menu_1.png -------------------------------------------------------------------------------- /Screenshots/3.1.0_menu_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/3.1.0_menu_2.png -------------------------------------------------------------------------------- /Screenshots/4.0.0_menu_ta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/4.0.0_menu_ta.png -------------------------------------------------------------------------------- /Screenshots/5.0.0_menu_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/5.0.0_menu_1.png -------------------------------------------------------------------------------- /Screenshots/5.0.0_menu_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/5.0.0_menu_2.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_linked_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_linked_1.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_linked_1_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_linked_1_cropped.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_linked_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_linked_2.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_linked_hide_rt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_linked_hide_rt.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_linked_hide_rt_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_linked_hide_rt_cropped.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_main.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_main_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_main_1.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_unlinked_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_unlinked_1.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_menu_unlinked_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_menu_unlinked_2.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_mod_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_mod_settings.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_mp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_mp.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_preferences.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_ta_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_ta_1.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_ta_1_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_ta_1_cropped.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_ta_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_ta_2.png -------------------------------------------------------------------------------- /Screenshots/6.0.0_ta_2_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/6.0.0_ta_2_cropped.png -------------------------------------------------------------------------------- /Screenshots/menu1_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/menu1_small.png -------------------------------------------------------------------------------- /Screenshots/menu2_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/menu2_small.png -------------------------------------------------------------------------------- /Screenshots/menu_2.0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/menu_2.0.1.png -------------------------------------------------------------------------------- /Screenshots/menu_2.1.2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/menu_2.1.2_1.png -------------------------------------------------------------------------------- /Screenshots/menu_2.1.2_1_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/menu_2.1.2_1_large.png -------------------------------------------------------------------------------- /Screenshots/menu_2.1.2_1_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/menu_2.1.2_1_small.png -------------------------------------------------------------------------------- /Screenshots/ui_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/ui_options.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_beat_fraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_beat_fraction.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_beat_fraction_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_beat_fraction_2.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_beat_fraction_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_beat_fraction_3.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_menu_unlinked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_menu_unlinked.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_menu_unlinked_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_menu_unlinked_2.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_menu_unlinked_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_menu_unlinked_3.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_mod_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_mod_settings.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_mod_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_mod_settings_2.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_mod_settings_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_mod_settings_3.png -------------------------------------------------------------------------------- /Screenshots/v7/7.0.0_mod_settings_song_speed_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeph-yr/JDFixer/8d473048ef9cbe16496bca4b50da2771e0ff2253/Screenshots/v7/7.0.0_mod_settings_song_speed_1.png -------------------------------------------------------------------------------- /TimeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TMPro; 3 | using Tweening; 4 | using UnityEngine; 5 | using Zenject; 6 | 7 | namespace JDFixer 8 | { 9 | internal class TimeController : MonoBehaviour 10 | { 11 | internal static AudioTimeSyncController audioTime; 12 | internal static float length = 0.001f; 13 | 14 | private static Canvas canvas; 15 | private static TextMeshProUGUI text; 16 | private static bool text_shown; 17 | private static TimeTweeningManager tween = null; 18 | private static bool earthday = false; 19 | 20 | [Inject] 21 | internal void Construct(AudioTimeSyncController audioTimeSyncController, TimeTweeningManager timeTweeningManager) 22 | { 23 | audioTime = audioTimeSyncController; 24 | length = audioTime.songEndTime; 25 | 26 | text_shown = false; 27 | tween = timeTweeningManager; 28 | } 29 | 30 | private void Start() 31 | { 32 | //Plugin.Log.Debug("TimeController Start"); 33 | 34 | GameObject canvasGo = new GameObject("Canvas"); 35 | canvasGo.transform.parent = transform; 36 | canvas = canvasGo.AddComponent(); 37 | canvas.renderMode = RenderMode.WorldSpace; 38 | 39 | var canvasTransform = canvas.transform; 40 | canvasTransform.position = new Vector3(0f, 3f, 3.8f); 41 | canvasTransform.localScale = Vector3.one; 42 | 43 | text = CreateText(canvas, new Vector2(0f, 0f)); 44 | } 45 | 46 | private void Update() 47 | { 48 | if (earthday && text_shown == false && audioTime.songTime >= 2) 49 | { 50 | text.gameObject.SetActive(true); 51 | tween.AddTween(new FloatTween(0, 1, value => text.alpha = value, 3.5f, EaseType.InCubic), text); 52 | text_shown = true; 53 | return; 54 | } 55 | 56 | if (text_shown == false && audioTime.songTime >= 0.25 * length) 57 | { 58 | text.gameObject.SetActive(true); 59 | tween.AddTween(new FloatTween(0, 1, value => text.alpha = value, 3.5f, EaseType.InCubic), text); 60 | text_shown = true; 61 | } 62 | else if (!earthday && text.gameObject.activeSelf && audioTime.songTime >= 0.5 * length) 63 | { 64 | //text.CrossFadeAlpha(0f, -3.5f, false); // This doesnt work 65 | tween.AddTween(new FloatTween(1, 0, value => text.alpha = value, 3.5f, EaseType.InCubic), text); 66 | } 67 | } 68 | 69 | private static TextMeshProUGUI CreateText(Canvas canvas, Vector2 position) 70 | { 71 | GameObject gameObject = new GameObject("CustomUIText"); 72 | gameObject.SetActive(false); 73 | TextMeshProUGUI tmp = gameObject.AddComponent(); 74 | 75 | tmp.rectTransform.SetParent(canvas.transform, false); 76 | tmp.rectTransform.transform.localPosition = Vector3.zero; 77 | tmp.rectTransform.anchoredPosition = position; 78 | 79 | if (DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 22)) >= 0 && DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 23)) < 1) 80 | { 81 | earthday = true; 82 | tmp.text = "Hello there.\nMaking mods is hard work. If JDFixer has helped you,\nI ask one favor in return.\n <#ffff00>Today is Earth Day. We are in a climate emergency.\nI ask you to do anything and everything you can to preserve our and your future.\nWe CAN do this together."; 83 | } 84 | else if (DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 1)) >= 0 && DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 2)) < 1) 85 | { 86 | tmp.text = "Hello, Happy April Fools and have fun with this new game mode!\nHint - If you want out, you may turn it off in the config ^^"; 87 | } 88 | else 89 | { 90 | tmp.text = ""; 91 | } 92 | tmp.fontSize = 0.12f; 93 | tmp.color = new Color(1f, 0f, 0.5f); 94 | tmp.alpha = 0f; 95 | tmp.alignment = TextAlignmentOptions.Center; 96 | 97 | return tmp; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /TimeSetup.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using JDFixer.Installers; 3 | using SiraUtil.Zenject; 4 | using System; 5 | 6 | namespace JDFixer 7 | { 8 | internal class TimeSetup 9 | { 10 | internal static void Inject(Zenjector zenjector) 11 | { 12 | if (PluginConfig.Instance.enabled && ( 13 | (DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 3, 31)) >= 0 && DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 2)) < 0 && PluginConfig.Instance.af_enabled) || 14 | (DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 21)) >= 0 && DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 23)) < 0))) 15 | 16 | //if (true) 17 | { 18 | //Plugin.Log.Debug("TimeSetup Inject"); 19 | 20 | zenjector.Install(Location.GameCore); 21 | } 22 | } 23 | 24 | /*internal static void Patch() 25 | { 26 | if (PluginConfig.Instance.enabled && 27 | DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 3, 31)) >= 0 && DateTime.Compare(DateTime.Now, new DateTime(DateTime.Now.Year, 4, 2)) < 0 && 28 | PluginConfig.Instance.af_enabled) 29 | 30 | //if (true) 31 | { 32 | //Plugin.Log.Debug("TimeSetup Patch"); 33 | 34 | var original = AccessTools.Method(typeof(BeatmapObjectSpawnMovementData), nameof(BeatmapObjectSpawnMovementData.GetJumpingNoteSpawnData)); 35 | var postfix = AccessTools.Method(typeof(TimeControllerPatch), nameof(TimeControllerPatch.Postfix)); 36 | Plugin.harmony.Patch(original, null, new HarmonyMethod(postfix)); 37 | } 38 | }*/ 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /UI/BSML/customOnlineUI.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 | 25 | 26 | 27 | 28 | 29 | 30 | 47 | 48 | -------------------------------------------------------------------------------- /UI/BSML/legacyModifierUI.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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 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 | 87 | 88 | -------------------------------------------------------------------------------- /UI/BSML/mainMenuUI.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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /UI/BSML/modifierUI.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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 62 | 63 | -------------------------------------------------------------------------------- /UI/BSML/preferencesList.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /UI/BSML/rtPreferencesList.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /UI/CustomOnlineUI.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using BeatSaberMarkupLanguage.Components.Settings; 3 | using BeatSaberMarkupLanguage.GameplaySetup; 4 | using Zenject; 5 | using System; 6 | using System.ComponentModel; 7 | using HMUI; 8 | using BeatSaberMarkupLanguage.Parser; 9 | 10 | namespace JDFixer.UI 11 | { 12 | internal sealed class CustomOnlineUI : IInitializable, IDisposable, INotifyPropertyChanged 13 | { 14 | internal static CustomOnlineUI Instance { get; private set; } 15 | private readonly MainFlowCoordinator _mainFlow; 16 | private readonly PreferencesFlowCoordinator _prefFlow; 17 | 18 | public event PropertyChangedEventHandler PropertyChanged; 19 | 20 | 21 | public void Initialize() 22 | { 23 | GameplaySetup.Instance.AddTab("JDFixer-TA/MP", "JDFixer.UI.BSML.customOnlineUI.bsml", this, MenuType.Custom | MenuType.Online); 24 | Donate.Refresh_Text(); 25 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Update_Dynamic))); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | if (GameplaySetup.Instance != null) 31 | { 32 | PluginConfig.Instance.Changed(); 33 | GameplaySetup.Instance.RemoveTab("JDFixer-TA/MP"); 34 | } 35 | } 36 | 37 | // To get the flow coordinators using zenject, we use a constructor 38 | private CustomOnlineUI(MainFlowCoordinator mainFlowCoordinator, PreferencesFlowCoordinator preferencesFlowCoordinator) 39 | { 40 | Instance = this; 41 | _mainFlow = mainFlowCoordinator; 42 | _prefFlow = preferencesFlowCoordinator; 43 | Donate.Refresh_Text(); 44 | } 45 | 46 | // For updating UI values to match those last used in Solo, when coming from Solo to Online 47 | internal void Refresh() 48 | { 49 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Slider_Setting_Value))); 50 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Increment_Value))); 51 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pref_Button))); 52 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Heuristic_Increment_Value))); 53 | 54 | PostParse(); 55 | } 56 | 57 | 58 | //============================================================================================= 59 | 60 | [UIValue("enabled")] 61 | private bool Enabled 62 | { 63 | get => PluginConfig.Instance.enabled; 64 | set 65 | { 66 | PluginConfig.Instance.enabled = value; 67 | } 68 | } 69 | [UIAction("set_enabled")] 70 | private void SetEnabled(bool value) 71 | { 72 | Enabled = value; 73 | } 74 | 75 | 76 | [UIValue("slider_setting_value")] 77 | private int Slider_Setting_Value 78 | { 79 | get => PluginConfig.Instance.slider_setting; 80 | set 81 | { 82 | PluginConfig.Instance.slider_setting = value; 83 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Slider_Setting_Value))); 84 | 85 | PostParse(); 86 | } 87 | } 88 | 89 | [UIAction("slider_setting_increment_formatter")] 90 | private string Slider_Setting_Increment_Formatter(int value) => ((SliderSettingEnum)value).ToString(); 91 | 92 | 93 | //============================================================================================= 94 | // JD and RT Sliders 95 | 96 | [UIValue("jd_text")] 97 | private string JD_Text => Get_JD_Text(); 98 | 99 | private string Get_JD_Text() 100 | { 101 | if (PluginConfig.Instance.slider_setting == 0 && PluginConfig.Instance.pref_selected == 0) 102 | { 103 | return "Desired Jump Distance"; 104 | } 105 | else if (PluginConfig.Instance.slider_setting == 0 && PluginConfig.Instance.pref_selected != 0) 106 | { 107 | return "<#555555dd>Desired Jump Distance"; 108 | } 109 | else 110 | { 111 | return "<#555555dd>Inactive JD"; 112 | } 113 | } 114 | 115 | [UIValue("min_jd_slider")] 116 | private float Min_JD_Slider => PluginConfig.Instance.minJumpDistance; 117 | [UIValue("max_jd_slider")] 118 | private float Max_JD_Slider => PluginConfig.Instance.maxJumpDistance; 119 | 120 | [UIComponent("jd_slider")] 121 | private SliderSetting JD_Slider; 122 | 123 | [UIValue("jd_value")] 124 | private float JD_Value 125 | { 126 | get => PluginConfig.Instance.jumpDistance; 127 | set 128 | { 129 | PluginConfig.Instance.jumpDistance = value; 130 | } 131 | } 132 | 133 | [UIAction("set_jd_value")] 134 | private void Set_JD_Value(float value) 135 | { 136 | JD_Value = value; 137 | } 138 | 139 | [UIAction("jd_slider_formatter")] 140 | private string JD_Slider_Formatter(float value) => value.ToString("0.##"); 141 | 142 | 143 | [UIValue("rt_text")] 144 | private string RT_Text => Get_RT_Text(); 145 | 146 | private string Get_RT_Text() 147 | { 148 | if (PluginConfig.Instance.slider_setting == 1 && PluginConfig.Instance.pref_selected == 0) 149 | { 150 | return "Desired Reaction Time"; 151 | } 152 | else if (PluginConfig.Instance.slider_setting == 1 && PluginConfig.Instance.pref_selected != 0) 153 | { 154 | return "<#555555dd>Desired Reaction Time"; 155 | } 156 | else 157 | { 158 | return "<#555555dd>Inactive RT"; 159 | } 160 | } 161 | 162 | [UIValue("min_rt_slider")] 163 | private float Min_RT_Slider => PluginConfig.Instance.minReactionTime; 164 | 165 | [UIValue("max_rt_slider")] 166 | private float Max_RT_Slider => PluginConfig.Instance.maxReactionTime; 167 | 168 | [UIComponent("rt_slider")] 169 | private SliderSetting RT_Slider; 170 | 171 | [UIValue("rt_value")] 172 | private float RT_Value 173 | { 174 | get => PluginConfig.Instance.reactionTime; 175 | set 176 | { 177 | PluginConfig.Instance.reactionTime = value; 178 | } 179 | } 180 | 181 | [UIAction("set_rt_value")] 182 | private void Set_RT_Value(float value) 183 | { 184 | RT_Value = value; 185 | } 186 | 187 | [UIAction("rt_slider_formatter")] 188 | private string RT_Slider_Formatter(float value) => value.ToString("0") + " ms"; 189 | 190 | 191 | 192 | //============================================================================================= 193 | 194 | [UIValue("increment_value")] 195 | private int Increment_Value 196 | { 197 | get => PluginConfig.Instance.pref_selected; 198 | set 199 | { 200 | PluginConfig.Instance.pref_selected = value; 201 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Increment_Value))); 202 | 203 | Set_Preference_Mode(); 204 | PostParse(); 205 | } 206 | } 207 | 208 | [UIAction("increment_formatter")] 209 | private string Increment_Formatter(int value) => ((PreferenceEnum)value).ToString(); 210 | 211 | private void Set_Preference_Mode() 212 | { 213 | if (PluginConfig.Instance.pref_selected == 2) 214 | { 215 | PluginConfig.Instance.use_jd_pref = false; 216 | PluginConfig.Instance.use_rt_pref = true; 217 | } 218 | else if (PluginConfig.Instance.pref_selected == 1) 219 | { 220 | PluginConfig.Instance.use_jd_pref = true; 221 | PluginConfig.Instance.use_rt_pref = false; 222 | } 223 | else 224 | { 225 | PluginConfig.Instance.use_jd_pref = false; 226 | PluginConfig.Instance.use_rt_pref = false; 227 | } 228 | 229 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pref_Button))); 230 | } 231 | 232 | 233 | [UIValue("pref_button")] 234 | private string Pref_Button => Get_Pref_Button(); 235 | 236 | private string Get_Pref_Button() 237 | { 238 | if (PluginConfig.Instance.pref_selected == 2) 239 | { 240 | return "<#00000000>----<#cc99ff>Configure RT Preferences<#00000000>----"; //#8c1aff 241 | } 242 | else if (PluginConfig.Instance.pref_selected == 1) 243 | { 244 | return "<#00000000>----<#ffff00>Configure JD Preferences<#00000000>----"; 245 | } 246 | else 247 | { 248 | return "Configure JD and RT Preferences"; 249 | } 250 | } 251 | 252 | [UIAction("pref_button_clicked")] 253 | private void Pref_Button_Clicked() 254 | { 255 | /* Kyle used to have a helper function which you also used (DeepestChildFlowCoordinator). 256 | * Beat Games has added this to the game since, so we can just use something they helpfully provided us 257 | */ 258 | FlowCoordinator currentFlow = _mainFlow.YoungestChildFlowCoordinatorOrSelf(); 259 | // We need to give our current flow coordinator to the pref flow so it can exit 260 | _prefFlow._parentFlow = currentFlow; 261 | currentFlow.PresentFlowCoordinator(_prefFlow); 262 | } 263 | 264 | 265 | // Changed to Increment Setting for 1.26.0 266 | /*[UIValue("use_heuristic")] 267 | private bool Use_Heuristic 268 | { 269 | get => PluginConfig.Instance.use_heuristic; 270 | set 271 | { 272 | PluginConfig.Instance.use_heuristic = value; 273 | } 274 | } 275 | 276 | [UIAction("set_use_heuristic")] 277 | private void Set_Use_Heuristic(bool value) 278 | { 279 | Use_Heuristic = value; 280 | }*/ 281 | 282 | [UIValue("heuristic_increment_value")] 283 | private int Heuristic_Increment_Value 284 | { 285 | get => PluginConfig.Instance.use_heuristic; 286 | set 287 | { 288 | PluginConfig.Instance.use_heuristic = value; 289 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Heuristic_Increment_Value))); 290 | 291 | PostParse(); 292 | } 293 | } 294 | 295 | [UIAction("heuristic_increment_formatter")] 296 | private string Heuristic_Increment_Formatter(int value) => ((HeuristicEnum)value).ToString(); 297 | 298 | 299 | [UIValue("thresholds")] 300 | private string Thresholds 301 | { 302 | get => "≤ " + PluginConfig.Instance.lower_threshold.ToString() + " or ≥ " + PluginConfig.Instance.upper_threshold.ToString(); 303 | } 304 | 305 | 306 | //============================================================================================= 307 | 308 | private CurvedTextMeshPro jd_slider_text; 309 | private CurvedTextMeshPro rt_slider_text; 310 | 311 | [UIAction("#post-parse")] 312 | private void PostParse() 313 | { 314 | if (JD_Slider == null || RT_Slider == null) 315 | { 316 | return; 317 | } 318 | 319 | jd_slider_text = JD_Slider.Slider.GetComponentInChildren(); 320 | rt_slider_text = RT_Slider.Slider.GetComponentInChildren(); 321 | 322 | if (jd_slider_text != null && rt_slider_text != null) 323 | { 324 | if (PluginConfig.Instance.use_jd_pref || PluginConfig.Instance.use_rt_pref) 325 | { 326 | jd_slider_text.color = new UnityEngine.Color(0.3f, 0.3f, 0.3f); 327 | rt_slider_text.color = new UnityEngine.Color(0.3f, 0.3f, 0.3f); 328 | } 329 | 330 | else if (PluginConfig.Instance.slider_setting == 0) 331 | { 332 | jd_slider_text.color = new UnityEngine.Color(1f, 1f, 0f); 333 | rt_slider_text.color = new UnityEngine.Color(0.3f, 0.3f, 0.3f); 334 | } 335 | 336 | else // PluginConfig.Instance.slider_setting == 1 337 | { 338 | jd_slider_text.color = new UnityEngine.Color(0.3f, 0.3f, 0.3f); 339 | rt_slider_text.color = new UnityEngine.Color(204f / 255f, 153f / 255f, 1f); 340 | } 341 | } 342 | 343 | // These are critical: 344 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Text))); 345 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Text))); 346 | 347 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Value))); 348 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Value))); 349 | } 350 | 351 | 352 | //=============================================================== 353 | 354 | [UIValue("open_donate_text")] 355 | private string Open_Donate_Text => Donate.donate_clickable_text; 356 | 357 | [UIValue("open_donate_hint")] 358 | private string Open_Donate_Hint => Donate.donate_clickable_hint; 359 | 360 | [UIParams] 361 | private BSMLParserParams parserParams; 362 | 363 | [UIAction("open_donate_modal")] 364 | private void Open_Donate_Modal() 365 | { 366 | parserParams.EmitEvent("hide_donate_modal"); 367 | Donate.Refresh_Text(); 368 | parserParams.EmitEvent("show_donate_modal"); 369 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Modal_Text_Dynamic))); 370 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Modal_Hint_Dynamic))); 371 | } 372 | 373 | private void Open_Donate_Patreon() 374 | { 375 | Donate.Patreon(); 376 | } 377 | private void Open_Donate_Kofi() 378 | { 379 | Donate.Kofi(); 380 | } 381 | 382 | [UIValue("donate_modal_text_static_1")] 383 | private string Donate_Modal_Text_Static_1 => Donate.donate_modal_text_static_1; 384 | 385 | [UIValue("donate_modal_text_static_2")] 386 | private string Donate_Modal_Text_Static_2 => Donate.donate_modal_text_static_2; 387 | 388 | [UIValue("donate_modal_text_dynamic")] 389 | private string Donate_Modal_Text_Dynamic => Donate.donate_modal_text_dynamic; 390 | 391 | [UIValue("donate_modal_hint_dynamic")] 392 | private string Donate_Modal_Hint_Dynamic => Donate.donate_modal_hint_dynamic; 393 | 394 | [UIValue("donate_update_dynamic")] 395 | private string Donate_Update_Dynamic => Donate.donate_update_dynamic; 396 | } 397 | } -------------------------------------------------------------------------------- /UI/Donate.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | 5 | namespace JDFixer.UI 6 | { 7 | internal class Donate 8 | { 9 | internal static string donate_clickable_text = "<#00000000>------------<#ff0080ff>♡ Donate"; 10 | internal static string donate_clickable_hint = "If you'd like to support my work"; 11 | 12 | internal static string donate_modal_text_static_1 = "<#ffff00ff>Support JDFixer<#cc99ffff>\nHave you have been enjoying my creations and\nyou wish to support me?"; 13 | internal static string donate_modal_text_static_2 = "<#ff0080ff>With much love, ♡ Zeph<#00000000>------"; 14 | 15 | internal static string donate_modal_text_dynamic = ""; 16 | internal static string donate_modal_hint_dynamic = ""; 17 | internal static string donate_update_dynamic = ""; 18 | 19 | internal static void Refresh_Text() 20 | { 21 | if (donate_modal_text_dynamic == "") 22 | { 23 | _ = Get_Donate_Modal_Text(); 24 | } 25 | } 26 | 27 | internal static void Patreon() 28 | { 29 | Process.Start("https://www.patreon.com/xeph_yr"); 30 | } 31 | 32 | internal static void Kofi() 33 | { 34 | Process.Start("https://ko-fi.com/zeph_yr"); 35 | } 36 | 37 | private static async Task Get_Donate_Modal_Text() 38 | { 39 | //Plugin.Log.Debug("reply: " + donate_modal_text_dynamic); 40 | string reply_text = "Loading..."; 41 | string reply_hint = ""; 42 | string reply_update = ""; 43 | 44 | using (WebClient client = new WebClient()) 45 | { 46 | try 47 | { 48 | reply_text = await client.DownloadStringTaskAsync("https://www.xephai.com/jd/?a=JDFIXER&b=text"); 49 | } 50 | catch 51 | { 52 | reply_text = await client.DownloadStringTaskAsync("https://raw.githubusercontent.com/zeph-yr/Shoutouts/main/README.md"); 53 | Plugin.Log.Debug("Failed to fetch Donate info"); 54 | } 55 | try 56 | { 57 | reply_hint = await client.DownloadStringTaskAsync("https://www.xephai.com/jd/?a=JDFIXER&b=hint"); 58 | } 59 | catch 60 | { 61 | reply_hint = await client.DownloadStringTaskAsync("https://raw.githubusercontent.com/zeph-yr/Shoutouts/main/hoverhints.txt"); 62 | Plugin.Log.Debug("Failed to fetch Donate info"); 63 | } 64 | try 65 | { 66 | reply_update = await client.DownloadStringTaskAsync("https://www.xephai.com/jd/?a=JDFIXER&b=update"); 67 | } 68 | catch 69 | { 70 | reply_update = await client.DownloadStringTaskAsync("https://raw.githubusercontent.com/zeph-yr/Shoutouts/main/whatsnew.txt"); 71 | Plugin.Log.Debug("Failed to fetch Donate info"); 72 | } 73 | } 74 | 75 | donate_modal_text_dynamic = reply_text; 76 | 77 | int hint_start = reply_hint.IndexOf("[JDFIXER]"); 78 | int hint_end = reply_hint.IndexOf("###", hint_start); 79 | 80 | if (hint_start != -1) 81 | { 82 | //Plugin.Log.Debug("reply: " + reply_hint); 83 | //Plugin.Log.Debug("start: " + hint_start + " end: " + hint_end); 84 | donate_modal_hint_dynamic = reply_hint.Substring(hint_start + 9, hint_end - hint_start - 9); // Yes. And no, it's not wrong. 85 | } 86 | 87 | int update_start = reply_update.IndexOf("[JDFIXER]"); 88 | int update_end = reply_update.IndexOf("###", update_start); 89 | if (update_start != -1) 90 | { 91 | donate_update_dynamic = reply_update.Substring(update_start + 9, update_end - update_start - 9); 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /UI/LegacyModifierUI.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using BeatSaberMarkupLanguage.Components.Settings; 3 | using BeatSaberMarkupLanguage.GameplaySetup; 4 | using HMUI; 5 | using Zenject; 6 | using System; 7 | using System.ComponentModel; 8 | using JDFixer.Interfaces; 9 | using BeatSaberMarkupLanguage.Parser; 10 | 11 | namespace JDFixer.UI 12 | { 13 | internal sealed class LegacyModifierUI : IInitializable, IDisposable, INotifyPropertyChanged, IBeatmapInfoUpdater 14 | { 15 | internal static LegacyModifierUI Instance { get; set; } 16 | private readonly MainFlowCoordinator _mainFlow; 17 | private readonly PreferencesFlowCoordinator _prefFlow; 18 | 19 | public event PropertyChangedEventHandler PropertyChanged; 20 | private BeatmapInfo _selectedBeatmap = BeatmapInfo.Empty; 21 | 22 | 23 | public void Initialize() 24 | { 25 | GameplaySetup.Instance.AddTab("JDFixer", "JDFixer.UI.BSML.legacyModifierUI.bsml", this, MenuType.Solo | MenuType.Campaign); 26 | Donate.Refresh_Text(); 27 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Update_Dynamic))); 28 | } 29 | 30 | public void Dispose() 31 | { 32 | if (GameplaySetup.Instance != null) 33 | { 34 | PluginConfig.Instance.Changed(); 35 | GameplaySetup.Instance.RemoveTab("JDFixer"); 36 | } 37 | } 38 | 39 | // To get the flow coordinators using zenject, we use a constructor 40 | private LegacyModifierUI(MainFlowCoordinator mainFlowCoordinator, PreferencesFlowCoordinator preferencesFlowCoordinator) 41 | { 42 | Instance = this; 43 | _mainFlow = mainFlowCoordinator; 44 | _prefFlow = preferencesFlowCoordinator; 45 | Donate.Refresh_Text(); 46 | } 47 | 48 | public void BeatmapInfoUpdated(BeatmapInfo beatmapInfo) 49 | { 50 | _selectedBeatmap = beatmapInfo; 51 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Map_Default_JD))); 52 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Map_Min_JD))); 53 | 54 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Display))); 55 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Display))); 56 | 57 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_JD_Slider))); 58 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_RT_Slider))); 59 | 60 | if (PluginConfig.Instance.use_offset) 61 | { 62 | //Plugin.Log.Debug("Map JD: " + _selectedBeatmap.JumpDistance + " " + _selectedBeatmap.MinJDSlider + " " + _selectedBeatmap.MaxJDSlider); 63 | //Plugin.Log.Debug("Map RT: " + _selectedBeatmap.ReactionTime + " " + _selectedBeatmap.MinRTSlider + " " + _selectedBeatmap.MaxRTSlider); 64 | 65 | BeatmapOffsets.Create_Snap_Points(ref BeatmapOffsets.JD_Snap_Points, ref BeatmapOffsets.JD_Offset_Points, _selectedBeatmap.Offset, _selectedBeatmap.JumpDistance, _selectedBeatmap.JDOffsetQuantum, _selectedBeatmap.MinJDSlider, _selectedBeatmap.MaxJDSlider); 66 | BeatmapOffsets.Create_Snap_Points(ref BeatmapOffsets.RT_Snap_Points, ref BeatmapOffsets.RT_Offset_Points, _selectedBeatmap.Offset, _selectedBeatmap.ReactionTime, _selectedBeatmap.RTOffsetQuantum, _selectedBeatmap.MinRTSlider, _selectedBeatmap.MaxRTSlider); 67 | 68 | Refresh_BeatmapOffsets(); 69 | } 70 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_JD_Display))); 71 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_RT_Display))); 72 | 73 | PostParse(); 74 | } 75 | 76 | internal void Refresh() 77 | { 78 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Slider_Setting_Value))); 79 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Increment_Value))); 80 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pref_Button))); 81 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Heuristic_Increment_Value))); 82 | 83 | if (PluginConfig.Instance.use_offset) 84 | { 85 | Refresh_BeatmapOffsets(); 86 | } 87 | } 88 | 89 | internal void Refresh_BeatmapOffsets() 90 | { 91 | //Plugin.Log.Debug("Refresh_BeatmapOffsets"); 92 | 93 | BeatmapOffsets.Calculate_Nearest_JD_Snap_Point(JD_Value); 94 | BeatmapOffsets.Calculate_Nearest_RT_Snap_Point(RT_Value); 95 | 96 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Snapped_JD))); 97 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Snapped_RT))); 98 | 99 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_Snapped_JD))); 100 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_Snapped_RT))); 101 | } 102 | 103 | 104 | //============================================================================================= 105 | 106 | 107 | [UIValue("enabled")] 108 | private bool Enabled 109 | { 110 | get => PluginConfig.Instance.enabled; 111 | set 112 | { 113 | PluginConfig.Instance.enabled = value; 114 | } 115 | } 116 | [UIAction("set_enabled")] 117 | private void SetEnabled(bool value) 118 | { 119 | Enabled = value; 120 | } 121 | 122 | 123 | [UIValue("map_jd_rt")] 124 | private string Map_JD_RT => Get_Map_JD_RT(); 125 | private string Get_Map_JD_RT() 126 | { 127 | if (PluginConfig.Instance.rt_display_enabled) 128 | { 129 | return "Map JD and RT"; 130 | } 131 | return "Map JD"; 132 | } 133 | 134 | 135 | [UIValue("map_default_jd")] 136 | private string Map_Default_JD => Get_Map_Default_JD(); 137 | private string Get_Map_Default_JD() 138 | { 139 | if (PluginConfig.Instance.rt_display_enabled) 140 | return "<#ffff00>" + _selectedBeatmap.JumpDistance.ToString("0.##") + " <#8c1aff>" + _selectedBeatmap.ReactionTime.ToString("0") + " ms"; 141 | 142 | return "<#ffff00>" + _selectedBeatmap.JumpDistance.ToString("0.##"); 143 | } 144 | 145 | 146 | [UIValue("map_min_jd")] 147 | private string Map_Min_JD => Get_Map_Min_JD(); 148 | private string Get_Map_Min_JD() 149 | { 150 | if (PluginConfig.Instance.rt_display_enabled) 151 | return "<#8c8c8c>" + _selectedBeatmap.MinJumpDistance.ToString("0.##") + " <#8c8c8c>" + _selectedBeatmap.MinReactionTime.ToString("0") + " ms"; 152 | 153 | return "<#8c8c8c>" + _selectedBeatmap.MinJumpDistance.ToString("0.##"); 154 | } 155 | 156 | 157 | [UIValue("snapped_jd")] 158 | private string Snapped_JD => Get_Snapped_JD(); 159 | private string Get_Snapped_JD() 160 | { 161 | BeatmapOffsets.Calculate_Nearest_JD_Snap_Point(JD_Value); 162 | return "<#8c8c8c>" + BeatmapOffsets.jd_offset_snap_value + " <#ffff00>" + BeatmapOffsets.jd_snap_value.ToString("0.##") + " " + BeatmapUtils.Calculate_ReactionTime_Setpoint_String(BeatmapOffsets.jd_snap_value, _selectedBeatmap.NJS); 163 | } 164 | [UIValue("show_snapped_jd")] 165 | private bool Show_Snapped_JD => PluginConfig.Instance.use_offset && Show_JD_Slider; 166 | 167 | 168 | [UIValue("snapped_rt")] 169 | private string Snapped_RT => Get_Snapped_RT(); 170 | private string Get_Snapped_RT() 171 | { 172 | BeatmapOffsets.Calculate_Nearest_RT_Snap_Point(RT_Value); 173 | return "<#8c8c8c>" + BeatmapOffsets.rt_offset_snap_value + " " + BeatmapUtils.Calculate_JumpDistance_Setpoint_String(BeatmapOffsets.rt_snap_value, _selectedBeatmap.NJS) + " <#cc99ff>" + BeatmapOffsets.rt_snap_value.ToString("0") + " ms"; 174 | } 175 | [UIValue("show_snapped_rt")] 176 | private bool Show_Snapped_RT => PluginConfig.Instance.use_offset && Show_RT_Slider; 177 | 178 | 179 | //============================================================================================= 180 | 181 | 182 | [UIValue("min_jd_slider")] 183 | private float Min_JD_Slider => PluginConfig.Instance.minJumpDistance; 184 | [UIValue("max_jd_slider")] 185 | private float Max_JD_Slider => PluginConfig.Instance.maxJumpDistance; 186 | 187 | [UIComponent("jd_slider")] 188 | private SliderSetting JD_Slider; 189 | 190 | [UIValue("jd_value")] 191 | private float JD_Value 192 | { 193 | get => PluginConfig.Instance.jumpDistance; 194 | set 195 | { 196 | PluginConfig.Instance.jumpDistance = value; 197 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Display))); 198 | 199 | if (PluginConfig.Instance.use_offset) 200 | { 201 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Snapped_JD))); 202 | } 203 | } 204 | } 205 | [UIAction("set_jd_value")] 206 | private void Set_JD_Value(float value) 207 | { 208 | JD_Value = value; 209 | } 210 | [UIAction("jd_slider_formatter")] 211 | private string JD_Slider_Formatter(float value) => value.ToString("0.##"); 212 | 213 | 214 | [UIValue("jd_display")] 215 | private string JD_Display => BeatmapUtils.Calculate_JumpDistance_Setpoint_String(RT_Value, _selectedBeatmap.NJS); //"<#ffff00>" + (PluginConfig.Instance.reactionTime * (2 * _selectedBeatmap.NJS) / 1000).ToString("0.##"); 216 | [UIValue("show_jd_display")] 217 | private bool Show_JD_Display => PluginConfig.Instance.use_offset == false && Show_RT_Slider; 218 | 219 | 220 | [UIValue("min_rt_slider")] 221 | private float Min_RT_Slider => PluginConfig.Instance.minReactionTime; 222 | 223 | [UIValue("max_rt_slider")] 224 | private float Max_RT_Slider => PluginConfig.Instance.maxReactionTime; 225 | 226 | [UIComponent("rt_slider")] 227 | private SliderSetting RT_Slider; 228 | 229 | [UIValue("rt_value")] 230 | private float RT_Value 231 | { 232 | get => PluginConfig.Instance.reactionTime; 233 | set 234 | { 235 | PluginConfig.Instance.reactionTime = value; 236 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Display))); 237 | 238 | if (PluginConfig.Instance.use_offset) 239 | { 240 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Snapped_RT))); 241 | } 242 | } 243 | } 244 | [UIAction("set_rt_value")] 245 | private void Set_RT_Value(float value) 246 | { 247 | RT_Value = value; 248 | } 249 | [UIAction("rt_slider_formatter")] 250 | private string RT_Slider_Formatter(float value) => value.ToString("0") + " ms"; 251 | 252 | 253 | [UIValue("rt_display")] 254 | private string RT_Display => BeatmapUtils.Calculate_ReactionTime_Setpoint_String(JD_Value, _selectedBeatmap.NJS); 255 | [UIValue("show_rt_display")] 256 | private bool Show_RT_Display => PluginConfig.Instance.use_offset == false && Show_JD_Slider; 257 | 258 | 259 | //############################################## 260 | // New for BS 1.19.0 261 | 262 | [UIValue("increment_value")] 263 | private int Increment_Value 264 | { 265 | get => PluginConfig.Instance.pref_selected; 266 | set 267 | { 268 | PluginConfig.Instance.pref_selected = value; 269 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Increment_Value))); 270 | 271 | Set_Preference_Mode(); 272 | } 273 | } 274 | 275 | [UIAction("increment_formatter")] 276 | private string Increment_Formatter(int value) => ((PreferenceEnum)value).ToString(); 277 | 278 | private void Set_Preference_Mode() 279 | { 280 | if (PluginConfig.Instance.pref_selected == 2) 281 | { 282 | PluginConfig.Instance.use_jd_pref = false; 283 | PluginConfig.Instance.use_rt_pref = true; 284 | } 285 | else if (PluginConfig.Instance.pref_selected == 1) 286 | { 287 | PluginConfig.Instance.use_jd_pref = true; 288 | PluginConfig.Instance.use_rt_pref = false; 289 | } 290 | else 291 | { 292 | PluginConfig.Instance.use_jd_pref = false; 293 | PluginConfig.Instance.use_rt_pref = false; 294 | } 295 | 296 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pref_Button))); 297 | } 298 | //############################################## 299 | 300 | 301 | [UIValue("pref_button")] 302 | private string Pref_Button => Get_Pref_Button(); 303 | 304 | private string Get_Pref_Button() 305 | { 306 | if (PluginConfig.Instance.pref_selected == 2) 307 | { 308 | return "<#00000000>----<#cc99ff>Configure RT Preferences<#00000000>----"; //#8c1aff 309 | } 310 | else if (PluginConfig.Instance.pref_selected == 1) 311 | { 312 | return "<#00000000>----<#ffff00>Configure JD Preferences<#00000000>----"; 313 | } 314 | else 315 | { 316 | return "Configure JD and RT Preferences"; 317 | } 318 | } 319 | 320 | [UIAction("pref_button_clicked")] 321 | private void Pref_Button_Clicked() 322 | { 323 | /* Kyle used to have a helper function which you also used (DeepestChildFlowCoordinator). 324 | * Beat Games has added this to the game since, so we can just use something they helpfully provided us 325 | */ 326 | FlowCoordinator currentFlow = _mainFlow.YoungestChildFlowCoordinatorOrSelf(); 327 | // We need to give our current flow coordinator to the pref flow so it can exit 328 | _prefFlow._parentFlow = currentFlow; 329 | currentFlow.PresentFlowCoordinator(_prefFlow); 330 | } 331 | 332 | 333 | // Changed to Increment Setting for 1.26.0 334 | /*[UIValue("use_heuristic")] 335 | private bool Use_Heuristic 336 | { 337 | get => PluginConfig.Instance.use_heuristic; 338 | set 339 | { 340 | PluginConfig.Instance.use_heuristic = value; 341 | } 342 | } 343 | 344 | [UIAction("set_use_heuristic")] 345 | private void Set_Use_Heuristic(bool value) 346 | { 347 | Use_Heuristic = value; 348 | }*/ 349 | 350 | [UIValue("heuristic_increment_value")] 351 | private int Heuristic_Increment_Value 352 | { 353 | get => PluginConfig.Instance.use_heuristic; 354 | set 355 | { 356 | PluginConfig.Instance.use_heuristic = value; 357 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Heuristic_Increment_Value))); 358 | 359 | PostParse(); 360 | } 361 | } 362 | 363 | [UIAction("heuristic_increment_formatter")] 364 | private string Heuristic_Increment_Formatter(int value) => ((HeuristicEnum)value).ToString(); 365 | 366 | 367 | [UIValue("thresholds")] 368 | private string Thresholds 369 | { 370 | get => "≤ " + PluginConfig.Instance.lower_threshold.ToString() + " or ≥ " + PluginConfig.Instance.upper_threshold.ToString(); 371 | } 372 | 373 | 374 | //################################### 375 | // KEEP: In case 376 | /*[UIValue("lowerthreshold")] 377 | public string lowerthreshold 378 | { 379 | get => PluginConfig.Instance.lower_threshold.ToString(); 380 | } 381 | 382 | // Thresholds Display 383 | [UIValue("upperthreshold")] 384 | public string upperthreshold 385 | { 386 | get => PluginConfig.Instance.upper_threshold.ToString(); 387 | }*/ 388 | //################################### 389 | 390 | 391 | private CurvedTextMeshPro jd_slider_text; 392 | private CurvedTextMeshPro rt_slider_text; 393 | 394 | [UIAction("#post-parse")] 395 | private void PostParse() 396 | { 397 | if (JD_Slider == null || RT_Slider == null) 398 | { 399 | return; 400 | } 401 | 402 | jd_slider_text = JD_Slider.Slider.GetComponentInChildren(); 403 | if (jd_slider_text != null) 404 | { 405 | jd_slider_text.color = new UnityEngine.Color(1f, 1f, 0f); 406 | } 407 | 408 | rt_slider_text = RT_Slider.Slider.GetComponentInChildren(); 409 | if (rt_slider_text != null) 410 | { 411 | rt_slider_text.color = new UnityEngine.Color(204f / 255f, 153f / 255f, 1f); 412 | } 413 | 414 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Value))); 415 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Value))); 416 | } 417 | 418 | 419 | //1.20.0 Feature update 420 | [UIValue("slider_setting_value")] 421 | private int Slider_Setting_Value 422 | { 423 | get => PluginConfig.Instance.slider_setting; 424 | set 425 | { 426 | PluginConfig.Instance.slider_setting = value; 427 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Slider_Setting_Value))); 428 | 429 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Value))); 430 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Value))); 431 | 432 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Display))); 433 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Display))); 434 | 435 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_JD_Slider))); 436 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_RT_Slider))); 437 | 438 | // 1.26.0-1.29.0 Feature update 439 | if (PluginConfig.Instance.use_offset) 440 | { 441 | Refresh_BeatmapOffsets(); 442 | } 443 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_JD_Display))); 444 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Show_RT_Display))); 445 | } 446 | } 447 | [UIAction("slider_setting_increment_formatter")] 448 | private string Slider_Setting_Increment_Formatter(int value) => ((SliderSettingEnum)value).ToString(); 449 | 450 | 451 | [UIValue("show_jd_slider")] 452 | private bool Show_JD_Slider => Get_JD_Slider(); 453 | 454 | private bool Get_JD_Slider() 455 | { 456 | if (PluginConfig.Instance.slider_setting == 0) 457 | { 458 | return true; 459 | } 460 | else 461 | { 462 | return false; 463 | } 464 | } 465 | 466 | [UIValue("show_rt_slider")] 467 | private bool Show_RT_Slider => Get_RT_Slider(); 468 | 469 | private bool Get_RT_Slider() 470 | { 471 | if (PluginConfig.Instance.slider_setting == 1) 472 | { 473 | return true; 474 | } 475 | else 476 | { 477 | return false; 478 | } 479 | } 480 | 481 | 482 | //=============================================================== 483 | 484 | 485 | [UIValue("open_donate_text")] 486 | private string Open_Donate_Text => Donate.donate_clickable_text; 487 | 488 | [UIValue("open_donate_hint")] 489 | private string Open_Donate_Hint => Donate.donate_clickable_hint; 490 | 491 | [UIParams] 492 | private BSMLParserParams parserParams; 493 | 494 | [UIAction("open_donate_modal")] 495 | private void Open_Donate_Modal() 496 | { 497 | parserParams.EmitEvent("hide_donate_modal"); 498 | Donate.Refresh_Text(); 499 | parserParams.EmitEvent("show_donate_modal"); 500 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Modal_Text_Dynamic))); 501 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Modal_Hint_Dynamic))); 502 | } 503 | 504 | private void Open_Donate_Patreon() 505 | { 506 | Donate.Patreon(); 507 | } 508 | private void Open_Donate_Kofi() 509 | { 510 | Donate.Kofi(); 511 | } 512 | 513 | [UIValue("donate_modal_text_static_1")] 514 | private string Donate_Modal_Text_Static_1 => Donate.donate_modal_text_static_1; 515 | 516 | [UIValue("donate_modal_text_static_2")] 517 | private string Donate_Modal_Text_Static_2 => Donate.donate_modal_text_static_2; 518 | 519 | [UIValue("donate_modal_text_dynamic")] 520 | private string Donate_Modal_Text_Dynamic => Donate.donate_modal_text_dynamic; 521 | 522 | [UIValue("donate_modal_hint_dynamic")] 523 | private string Donate_Modal_Hint_Dynamic => Donate.donate_modal_hint_dynamic; 524 | 525 | [UIValue("donate_update_dynamic")] 526 | private string Donate_Update_Dynamic => Donate.donate_update_dynamic; 527 | } 528 | } -------------------------------------------------------------------------------- /UI/MainMenuUI.cs: -------------------------------------------------------------------------------- 1 | using Zenject; 2 | using System; 3 | using BeatSaberMarkupLanguage.Attributes; 4 | using BeatSaberMarkupLanguage.Components.Settings; 5 | 6 | namespace JDFixer.UI 7 | { 8 | internal sealed class MainMenuUI : IInitializable, IDisposable 9 | { 10 | private MainMenuUI() 11 | { 12 | 13 | } 14 | 15 | public void Initialize() 16 | { 17 | BeatSaberMarkupLanguage.Settings.BSMLSettings.Instance.AddSettingsMenu("JDFixer", "JDFixer.UI.BSML.mainMenuUI.bsml", this); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | if (BeatSaberMarkupLanguage.Settings.BSMLSettings.Instance != null) 23 | { 24 | BeatSaberMarkupLanguage.Settings.BSMLSettings.Instance.RemoveSettingsMenu(this); 25 | } 26 | } 27 | 28 | 29 | [UIValue("rt_display_value")] 30 | private bool RT_Display_Value 31 | { 32 | get => PluginConfig.Instance.rt_display_enabled; 33 | set 34 | { 35 | PluginConfig.Instance.rt_display_enabled = value; 36 | } 37 | } 38 | [UIAction("set_rt_display")] 39 | private void Set_RT_Display(bool value) 40 | { 41 | RT_Display_Value = value; 42 | } 43 | 44 | 45 | [UIValue("legacy_display_value")] 46 | private bool Legacy_Display_Value 47 | { 48 | get => PluginConfig.Instance.legacy_display_enabled; 49 | set 50 | { 51 | PluginConfig.Instance.legacy_display_enabled = value; 52 | } 53 | } 54 | [UIAction("set_legacy_display")] 55 | private void Set_Legacy_Display(bool value) 56 | { 57 | Legacy_Display_Value = value; 58 | } 59 | 60 | 61 | [UIValue("song_speed_increment_value")] 62 | private int Song_Speed_Increment_Value 63 | { 64 | get => PluginConfig.Instance.song_speed_setting; 65 | set 66 | { 67 | PluginConfig.Instance.song_speed_setting = value; 68 | } 69 | } 70 | [UIAction("song_speed_increment_formatter")] 71 | private string Song_Speed_Increment_Formatter(int value) => ((SongSpeedEnum)value).ToString(); 72 | 73 | 74 | [UIValue("use_offset_value")] 75 | private bool Use_Offset_Value 76 | { 77 | get => PluginConfig.Instance.use_offset; 78 | set 79 | { 80 | PluginConfig.Instance.use_offset = value; 81 | } 82 | } 83 | [UIAction("set_use_offset")] 84 | private void Set_Use_Offset(bool value) 85 | { 86 | Use_Offset_Value = value; 87 | } 88 | 89 | 90 | [UIComponent("offset_fraction_slider")] 91 | private SliderSetting Offset_Fraction_Slider; 92 | 93 | [UIValue("offset_fraction_value")] 94 | private float Offset_Fraction_Value 95 | { 96 | get => PluginConfig.Instance.offset_fraction; 97 | set 98 | { 99 | PluginConfig.Instance.offset_fraction = value; 100 | } 101 | } 102 | [UIAction("set_offset_fraction")] 103 | private void Set_Offset_Fraction(float value) 104 | { 105 | Offset_Fraction_Value = value; 106 | } 107 | 108 | 109 | [UIComponent("lower_threshold_slider")] 110 | private SliderSetting Lower_Threshold_Slider; 111 | 112 | [UIValue("lower_threshold_value")] 113 | private float Lower_Threshold_Value 114 | { 115 | get => PluginConfig.Instance.lower_threshold; 116 | set 117 | { 118 | PluginConfig.Instance.lower_threshold = value; 119 | } 120 | } 121 | [UIAction("set_lower_threshold")] 122 | private void Set_Lower_Threshold(float value) 123 | { 124 | Lower_Threshold_Value = value; 125 | } 126 | 127 | 128 | [UIComponent("upper_threshold_slider")] 129 | private SliderSetting Upper_Threshold_Slider; 130 | 131 | [UIValue("upper_threshold_value")] 132 | private float Upper_Threshold_Value 133 | { 134 | get => PluginConfig.Instance.upper_threshold; 135 | set 136 | { 137 | PluginConfig.Instance.upper_threshold = value; 138 | } 139 | } 140 | [UIAction("set_upper_threshold")] 141 | private void Set_Upper_Threshold(float value) 142 | { 143 | Upper_Threshold_Value = value; 144 | } 145 | 146 | [UIValue("press_ok_text_1")] 147 | private string Press_Ok_Text_1 = "<#ffffffff>Press OK to apply settings <#ff0080ff>♡"; 148 | [UIValue("press_ok_text_2")] 149 | private string Press_Ok_Text_2 = "<#ff0080ff>v7.4.0 by Zephyr9125"; 150 | [UIValue("press_ok_hint_2")] 151 | private string Press_Ok_Hint_2 = ""; 152 | } 153 | 154 | internal enum SongSpeedEnum 155 | { 156 | JD_Settings = 0, 157 | RT_Settings = 1, 158 | JD_RT_Respectively 159 | } 160 | } -------------------------------------------------------------------------------- /UI/ModifierUI.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using BeatSaberMarkupLanguage.Components.Settings; 3 | using BeatSaberMarkupLanguage.GameplaySetup; 4 | using HMUI; 5 | using Zenject; 6 | using System; 7 | using System.ComponentModel; 8 | using JDFixer.Interfaces; 9 | using BeatSaberMarkupLanguage.Parser; 10 | 11 | namespace JDFixer.UI 12 | { 13 | internal sealed class ModifierUI : IInitializable, IDisposable, INotifyPropertyChanged, IBeatmapInfoUpdater 14 | { 15 | internal static ModifierUI Instance { get; set; } 16 | private readonly MainFlowCoordinator _mainFlow; 17 | private readonly PreferencesFlowCoordinator _prefFlow; 18 | 19 | public event PropertyChangedEventHandler PropertyChanged; 20 | private BeatmapInfo _selectedBeatmap = BeatmapInfo.Empty; 21 | 22 | 23 | public void Initialize() 24 | { 25 | GameplaySetup.Instance.AddTab("JDFixer", "JDFixer.UI.BSML.modifierUI.bsml", this, MenuType.Solo | MenuType.Campaign); 26 | Donate.Refresh_Text(); 27 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Update_Dynamic))); 28 | } 29 | 30 | public void Dispose() 31 | { 32 | if (GameplaySetup.Instance != null) 33 | { 34 | PluginConfig.Instance.Changed(); 35 | GameplaySetup.Instance.RemoveTab("JDFixer"); 36 | } 37 | } 38 | 39 | // To get the flow coordinators using zenject, we use a constructor 40 | private ModifierUI(MainFlowCoordinator mainFlowCoordinator, PreferencesFlowCoordinator preferencesFlowCoordinator) 41 | { 42 | Instance = this; 43 | _mainFlow = mainFlowCoordinator; 44 | _prefFlow = preferencesFlowCoordinator; 45 | Donate.Refresh_Text(); 46 | } 47 | 48 | public void BeatmapInfoUpdated(BeatmapInfo beatmapInfo) 49 | { 50 | _selectedBeatmap = beatmapInfo; 51 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Map_Default_JD))); 52 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Map_Min_JD))); 53 | //PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReactionTimeText))); // For old RT Display 54 | 55 | PostParse(); 56 | } 57 | 58 | internal void Refresh() 59 | { 60 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Slider_Setting_Value))); 61 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Increment_Value))); 62 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pref_Button))); 63 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Heuristic_Increment_Value))); 64 | } 65 | 66 | 67 | //============================================================================================= 68 | 69 | [UIValue("enabled")] 70 | private bool Enabled 71 | { 72 | get => PluginConfig.Instance.enabled; 73 | set 74 | { 75 | PluginConfig.Instance.enabled = value; 76 | } 77 | } 78 | [UIAction("set_enabled")] 79 | private void SetEnabled(bool value) 80 | { 81 | Enabled = value; 82 | } 83 | 84 | 85 | [UIValue("map_jd_rt")] 86 | private string Map_JD_RT => Get_Map_JD_RT(); 87 | private string Get_Map_JD_RT() 88 | { 89 | if (PluginConfig.Instance.rt_display_enabled) 90 | { 91 | return "Map JD and RT"; 92 | } 93 | return "Map JD"; 94 | } 95 | 96 | 97 | [UIValue("map_default_jd")] 98 | private string Map_Default_JD => Get_Map_Default_JD(); 99 | private string Get_Map_Default_JD() 100 | { 101 | if (PluginConfig.Instance.rt_display_enabled) 102 | return "<#ffff00>" + _selectedBeatmap.JumpDistance.ToString("0.##") + " <#8c1aff>" + _selectedBeatmap.ReactionTime.ToString("0") + " ms"; 103 | 104 | return "<#ffff00>" + _selectedBeatmap.JumpDistance.ToString("0.##"); 105 | } 106 | 107 | 108 | [UIValue("map_min_jd")] 109 | private string Map_Min_JD => Get_Map_Min_JD(); 110 | private string Get_Map_Min_JD() 111 | { 112 | if (PluginConfig.Instance.rt_display_enabled) 113 | return "<#8c8c8c>" + _selectedBeatmap.MinJumpDistance.ToString("0.##") + " <#8c8c8c>" + _selectedBeatmap.MinReactionTime.ToString("0" + " ms"); 114 | 115 | return "<#8c8c8c>" + _selectedBeatmap.MinJumpDistance.ToString("0.##"); 116 | } 117 | 118 | 119 | [UIValue("min_jd_slider")] 120 | private float Min_JD_Slider => _selectedBeatmap.MinJDSlider; //PluginConfig.Instance.minJumpDistance; 121 | [UIValue("max_jd_slider")] 122 | private float Max_JD_Slider => _selectedBeatmap.MaxJDSlider; //PluginConfig.Instance.maxJumpDistance; 123 | 124 | [UIComponent("jd_slider")] 125 | private SliderSetting JD_Slider; 126 | 127 | [UIValue("jd_value")] 128 | private float JD_Value 129 | { 130 | get => Get_Jump_Distance(); //PluginConfig.Instance.jumpDistance; //GetJumpDistance(); 131 | set 132 | { 133 | /*if (PluginConfig.Instance.use_offset) 134 | { 135 | PluginConfig.Instance.jumpDistance = BeatmapUtils.Calculate_ReactionTime_Nearest_Offset(value); 136 | } 137 | 138 | else*/ 139 | if (PluginConfig.Instance.slider_setting == 0) 140 | { 141 | PluginConfig.Instance.jumpDistance = value; 142 | } 143 | else 144 | { 145 | if (_selectedBeatmap.NJS > 0.002) 146 | { 147 | PluginConfig.Instance.reactionTime = value / (2 * _selectedBeatmap.NJS) * 1000; 148 | } 149 | } 150 | 151 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Value))); 152 | //PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReactionTimeText))); // For old RT Display 153 | } 154 | } 155 | 156 | /*private float GetJumpDistance() 157 | { 158 | return PluginConfig.Instance.jumpDistance; 159 | }*/ 160 | 161 | // 1.19.1 162 | private float Get_Jump_Distance() 163 | { 164 | if (PluginConfig.Instance.slider_setting == 0) 165 | { 166 | return PluginConfig.Instance.jumpDistance; 167 | } 168 | else 169 | { 170 | return PluginConfig.Instance.reactionTime * (2 * _selectedBeatmap.NJS) / 1000; 171 | } 172 | } 173 | 174 | [UIAction("set_jd_value")] 175 | private void Set_JD_Value(float value) 176 | { 177 | JD_Value = value; 178 | } 179 | 180 | [UIAction("jd_slider_formatter")] 181 | private string JD_Slider_Formatter(float value) => value.ToString("0.##"); 182 | 183 | 184 | [UIValue("min_rt_slider")] 185 | private float Min_RT_Slider => _selectedBeatmap.MinRTSlider; //Get_Min_RT(); 186 | 187 | [UIValue("max_rt_slider")] 188 | private float Max_RT_Slider => _selectedBeatmap.MaxRTSlider; //Get_Max_RT(); 189 | 190 | /*public float Get_Min_RT() 191 | { 192 | return _selectedBeatmap.MinRTSlider; 193 | } 194 | public float Get_Max_RT() 195 | { 196 | return _selectedBeatmap.MaxRTSlider; 197 | }*/ 198 | 199 | //============================================================= 200 | // Old Reaction Time Display: Replaced by RT Slider (KEEP THIS) 201 | 202 | //[UIValue("reactionTime")] 203 | //public string ReactionTimeText => CalculateReactionTime(); 204 | 205 | // 206 | // 207 | // 208 | // 209 | // 210 | // 211 | // 212 | 213 | //============================================================= 214 | 215 | [UIComponent("rt_slider")] 216 | private SliderSetting RT_Slider; 217 | 218 | [UIValue("rt_value")] 219 | private float RT_Value 220 | { 221 | get => Get_Reaction_Time(); //CalculateReactionTime_Float(PluginConfig.Instance.jumpDistance); 222 | set 223 | { 224 | if (PluginConfig.Instance.slider_setting == 0) // Fixed JD 225 | { 226 | if (_selectedBeatmap.NJS > 0.002) 227 | { 228 | PluginConfig.Instance.jumpDistance = value / 1000 * (2 * _selectedBeatmap.NJS); 229 | } 230 | } 231 | else 232 | { 233 | PluginConfig.Instance.reactionTime = value; 234 | } 235 | 236 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Value))); 237 | //PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReactionTimeText))); // For validation 238 | } 239 | } 240 | 241 | // 1.19.1 242 | private float Get_Reaction_Time() 243 | { 244 | if (PluginConfig.Instance.slider_setting == 0) 245 | { 246 | return BeatmapUtils.Calculate_ReactionTime_Setpoint_Float(PluginConfig.Instance.jumpDistance, _selectedBeatmap.NJS); 247 | } 248 | else 249 | { 250 | return PluginConfig.Instance.reactionTime; 251 | } 252 | } 253 | 254 | [UIAction("set_rt_value")] 255 | private void Set_RT_Value(float value) 256 | { 257 | RT_Value = value; 258 | } 259 | 260 | [UIAction("rt_slider_formatter")] 261 | private string RT_Slider_Formatter(float value) => value.ToString("0") + " ms"; 262 | 263 | 264 | //############################################## 265 | // New for BS 1.19.0 266 | 267 | [UIValue("increment_value")] 268 | private int Increment_Value 269 | { 270 | get => PluginConfig.Instance.pref_selected; 271 | set 272 | { 273 | PluginConfig.Instance.pref_selected = value; 274 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Increment_Value))); 275 | 276 | Set_Preference_Mode(); 277 | } 278 | } 279 | 280 | [UIAction("increment_formatter")] 281 | private string Increment_Formatter(int value) => ((PreferenceEnum)value).ToString(); 282 | 283 | private void Set_Preference_Mode() 284 | { 285 | if (PluginConfig.Instance.pref_selected == 2) 286 | { 287 | PluginConfig.Instance.use_jd_pref = false; 288 | PluginConfig.Instance.use_rt_pref = true; 289 | } 290 | else if (PluginConfig.Instance.pref_selected == 1) 291 | { 292 | PluginConfig.Instance.use_jd_pref = true; 293 | PluginConfig.Instance.use_rt_pref = false; 294 | } 295 | else 296 | { 297 | PluginConfig.Instance.use_jd_pref = false; 298 | PluginConfig.Instance.use_rt_pref = false; 299 | } 300 | 301 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pref_Button))); 302 | } 303 | 304 | 305 | //============================================================= 306 | // Old JD Preferences and RT Preferences Toggles: Replaced with Increment Setting 307 | 308 | // 309 | // 310 | 311 | /*[UIValue("usePrefJumpValues")] 312 | public bool usePrefJumpValues 313 | { 314 | get => PluginConfig.Instance.usePreferredJumpDistanceValues; 315 | set 316 | { 317 | PluginConfig.Instance.usePreferredJumpDistanceValues = value; 318 | } 319 | } 320 | [UIAction("setUsePrefJumpValues")] 321 | public void SetUsePrefJumpValues(bool value) 322 | { 323 | usePrefJumpValues = value; 324 | 325 | //if (value) 326 | //{ 327 | // PluginConfig.Instance.rt_enabled = false; 328 | // NotifyPropertyChanged(nameof(RTEnabled)); 329 | //} 330 | } 331 | 332 | 333 | // Reaction Time Mode 334 | [UIValue("rtEnabled")] 335 | public bool RTEnabled 336 | { 337 | get => PluginConfig.Instance.rt_enabled; 338 | set 339 | { 340 | PluginConfig.Instance.rt_enabled = value; 341 | } 342 | } 343 | [UIAction("setRTEnabled")] 344 | public void SetRTEnabled(bool value) 345 | { 346 | RTEnabled = value; 347 | 348 | //if (value) 349 | //{ 350 | // PluginConfig.Instance.usePreferredJumpDistanceValues = false; 351 | // NotifyPropertyChanged(nameof(usePrefJumpValues)); 352 | //} 353 | }*/ 354 | //============================================================= 355 | 356 | [UIValue("pref_button")] 357 | private string Pref_Button => Get_Pref_Button(); 358 | 359 | private string Get_Pref_Button() 360 | { 361 | if (PluginConfig.Instance.pref_selected == 2) 362 | { 363 | return "<#00000000>----<#cc99ff>Configure RT Preferences<#00000000>----"; //#8c1aff 364 | } 365 | else if (PluginConfig.Instance.pref_selected == 1) 366 | { 367 | return "<#00000000>----<#ffff00>Configure JD Preferences<#00000000>----"; 368 | } 369 | else 370 | { 371 | return "Configure JD and RT Preferences"; 372 | } 373 | } 374 | 375 | [UIAction("pref_button_clicked")] 376 | private void Pref_Button_Clicked() 377 | { 378 | /* Kyle used to have a helper function which you also used (DeepestChildFlowCoordinator). 379 | * Beat Games has added this to the game since, so we can just use something they helpfully provided us 380 | */ 381 | FlowCoordinator currentFlow = _mainFlow.YoungestChildFlowCoordinatorOrSelf(); 382 | // We need to give our current flow coordinator to the pref flow so it can exit 383 | _prefFlow._parentFlow = currentFlow; 384 | currentFlow.PresentFlowCoordinator(_prefFlow); 385 | } 386 | 387 | 388 | // Changed to Increment Setting for 1.26.0 389 | // 390 | 391 | /*[UIValue("use_heuristic")] 392 | private bool Use_Heuristic 393 | { 394 | get => PluginConfig.Instance.use_heuristic; 395 | set 396 | { 397 | PluginConfig.Instance.use_heuristic = value; 398 | } 399 | } 400 | 401 | [UIAction("set_use_heuristic")] 402 | private void Set_Use_Heuristic(bool value) 403 | { 404 | Use_Heuristic = value; 405 | }*/ 406 | 407 | [UIValue("heuristic_increment_value")] 408 | private int Heuristic_Increment_Value 409 | { 410 | get => PluginConfig.Instance.use_heuristic; 411 | set 412 | { 413 | PluginConfig.Instance.use_heuristic = value; 414 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Heuristic_Increment_Value))); 415 | 416 | PostParse(); 417 | } 418 | } 419 | 420 | [UIAction("heuristic_increment_formatter")] 421 | private string Heuristic_Increment_Formatter(int value) => ((HeuristicEnum)value).ToString(); 422 | 423 | 424 | [UIValue("thresholds")] 425 | private string Thresholds 426 | { 427 | get => "≤ " + PluginConfig.Instance.lower_threshold.ToString() + " or ≥ " + PluginConfig.Instance.upper_threshold.ToString(); 428 | } 429 | 430 | 431 | //################################### 432 | // KEEP: In case 433 | /*[UIValue("lowerthreshold")] 434 | public string lowerthreshold 435 | { 436 | get => PluginConfig.Instance.lower_threshold.ToString(); 437 | } 438 | 439 | // Thresholds Display 440 | [UIValue("upperthreshold")] 441 | public string upperthreshold 442 | { 443 | get => PluginConfig.Instance.upper_threshold.ToString(); 444 | }*/ 445 | //################################### 446 | 447 | 448 | private CurvedTextMeshPro jd_slider_text; 449 | private CurvedTextMeshPro rt_slider_text; 450 | 451 | private HMUI.CustomFormatRangeValuesSlider rt_slider_range; 452 | private HMUI.CustomFormatRangeValuesSlider jd_slider_range; 453 | 454 | [UIAction("#post-parse")] 455 | private void PostParse() 456 | { 457 | if (JD_Slider == null || RT_Slider == null) 458 | { 459 | return; 460 | } 461 | 462 | jd_slider_text = JD_Slider.Slider.GetComponentInChildren(); 463 | if (jd_slider_text != null) 464 | { 465 | jd_slider_text.color = new UnityEngine.Color(1f, 1f, 0f); 466 | } 467 | 468 | rt_slider_text = RT_Slider.Slider.GetComponentInChildren(); 469 | if (rt_slider_text != null) 470 | { 471 | rt_slider_text.color = new UnityEngine.Color(204f / 255f, 153f / 255f, 1f); 472 | } 473 | 474 | rt_slider_range = RT_Slider.Slider.GetComponentInChildren(); 475 | rt_slider_range.minValue = _selectedBeatmap.MinRTSlider; 476 | rt_slider_range.maxValue = _selectedBeatmap.MaxRTSlider; 477 | 478 | jd_slider_range = JD_Slider.Slider.GetComponentInChildren(); 479 | jd_slider_range.minValue = _selectedBeatmap.MinJDSlider; 480 | jd_slider_range.maxValue = _selectedBeatmap.MaxJDSlider; 481 | 482 | 483 | // These are critical: 484 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Min_RT_Slider))); 485 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Max_RT_Slider))); 486 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Value))); 487 | 488 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Min_JD_Slider))); 489 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Max_JD_Slider))); 490 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Value))); 491 | } 492 | 493 | 494 | //1.19.1 Feature update 495 | [UIValue("slider_setting_value")] 496 | private int Slider_Setting_Value 497 | { 498 | get => PluginConfig.Instance.slider_setting; 499 | set 500 | { 501 | PluginConfig.Instance.slider_setting = value; 502 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Slider_Setting_Value))); 503 | 504 | // This doesnt work because the MinRTSlider etc can't be publically set, crashes 505 | //BeatmapUtils.RefreshSliderMinMax(_selectedBeatmap.NJS); 506 | 507 | // This is critcal! 508 | RefreshSliderMinMax(); 509 | } 510 | } 511 | 512 | [UIAction("slider_setting_increment_formatter")] 513 | private string Slider_Setting_Increment_Formatter(int value) => ((SliderSettingEnum)value).ToString(); 514 | 515 | 516 | // This function is critical: 517 | // Without this function, when slider setting is flipped, the slider min maxes will be wrong because they are/were set in BeatmapInfo 518 | // Ex: When JD flips to RT, sliders will be draw as if set to JD (with JD min-max) until a new map is clicked that triggers BeatmapInfo 519 | // and PostParse to run again with the new setting. 520 | // Must "recalculate" them here then trigger everything to update 521 | private void RefreshSliderMinMax() 522 | { 523 | Plugin.Log.Debug("Refresh Slider Min Max"); 524 | rt_slider_range = RT_Slider.Slider.GetComponentInChildren(); 525 | jd_slider_range = JD_Slider.Slider.GetComponentInChildren(); 526 | 527 | if (PluginConfig.Instance.slider_setting == 0) 528 | { 529 | rt_slider_range.minValue = PluginConfig.Instance.minJumpDistance * 500 / _selectedBeatmap.NJS; 530 | rt_slider_range.maxValue = PluginConfig.Instance.maxJumpDistance * 500 / _selectedBeatmap.NJS; 531 | 532 | jd_slider_range.minValue = PluginConfig.Instance.minJumpDistance; 533 | jd_slider_range.maxValue = PluginConfig.Instance.maxJumpDistance; 534 | } 535 | else 536 | { 537 | rt_slider_range.minValue = PluginConfig.Instance.minReactionTime; 538 | rt_slider_range.maxValue = PluginConfig.Instance.maxReactionTime; 539 | 540 | jd_slider_range.minValue = PluginConfig.Instance.minReactionTime * _selectedBeatmap.NJS / 500; 541 | jd_slider_range.maxValue = PluginConfig.Instance.maxReactionTime * _selectedBeatmap.NJS / 500; 542 | } 543 | 544 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Min_RT_Slider))); 545 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Max_RT_Slider))); 546 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RT_Value))); 547 | 548 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Min_JD_Slider))); 549 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Max_JD_Slider))); 550 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(JD_Value))); 551 | } 552 | 553 | 554 | //=============================================================== 555 | 556 | [UIValue("open_donate_text")] 557 | private string Open_Donate_Text => Donate.donate_clickable_text; 558 | 559 | [UIValue("open_donate_hint")] 560 | private string Open_Donate_Hint => Donate.donate_clickable_hint; 561 | 562 | [UIParams] 563 | private BSMLParserParams parserParams; 564 | 565 | [UIAction("open_donate_modal")] 566 | private void Open_Donate_Modal() 567 | { 568 | parserParams.EmitEvent("hide_donate_modal"); 569 | Donate.Refresh_Text(); 570 | parserParams.EmitEvent("show_donate_modal"); 571 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Modal_Text_Dynamic))); 572 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Donate_Modal_Hint_Dynamic))); 573 | } 574 | 575 | private void Open_Donate_Patreon() 576 | { 577 | Donate.Patreon(); 578 | } 579 | private void Open_Donate_Kofi() 580 | { 581 | Donate.Kofi(); 582 | } 583 | 584 | [UIValue("donate_modal_text_static_1")] 585 | private string Donate_Modal_Text_Static_1 => Donate.donate_modal_text_static_1; 586 | 587 | [UIValue("donate_modal_text_static_2")] 588 | private string Donate_Modal_Text_Static_2 => Donate.donate_modal_text_static_2; 589 | 590 | [UIValue("donate_modal_text_dynamic")] 591 | private string Donate_Modal_Text_Dynamic => Donate.donate_modal_text_dynamic; 592 | 593 | [UIValue("donate_modal_hint_dynamic")] 594 | private string Donate_Modal_Hint_Dynamic => Donate.donate_modal_hint_dynamic; 595 | 596 | [UIValue("donate_update_dynamic")] 597 | private string Donate_Update_Dynamic => Donate.donate_update_dynamic; 598 | } 599 | 600 | 601 | internal enum SliderSettingEnum 602 | { 603 | JumpDistance = 0, 604 | ReactionTime = 1 605 | } 606 | 607 | internal enum PreferenceEnum 608 | { 609 | Off = 0, 610 | JumpDistance = 1, 611 | ReactionTime = 2 612 | } 613 | 614 | internal enum HeuristicEnum 615 | { 616 | Off = 0, 617 | On = 1 618 | } 619 | } -------------------------------------------------------------------------------- /UI/PreferencesFlowCoordinator.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage; 2 | using HMUI; 3 | using Zenject; 4 | 5 | namespace JDFixer.UI 6 | { 7 | internal sealed class PreferencesFlowCoordinator : FlowCoordinator 8 | { 9 | internal FlowCoordinator _parentFlow; 10 | private PreferencesListViewController _prefListView; 11 | private RTPreferencesListViewController _rtPrefListView; 12 | 13 | /* Since this is binded as a unity component, our "Constructor" is actually a method called Construct (with an inject attribute) 14 | * We would do the same for ViewControllers if we wanna ask for stuff from Zenject 15 | */ 16 | [Inject] 17 | private void Construct(PreferencesListViewController preferencesListViewController, RTPreferencesListViewController rTPreferencesListViewController) 18 | { 19 | _prefListView = preferencesListViewController; 20 | _rtPrefListView = rTPreferencesListViewController; 21 | } 22 | 23 | protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 24 | { 25 | showBackButton = true; 26 | SetTitle("JDFixer Preferences"); 27 | 28 | if (PluginConfig.Instance.use_rt_pref) 29 | ProvideInitialViewControllers(_rtPrefListView); 30 | else 31 | ProvideInitialViewControllers(_prefListView); 32 | } 33 | 34 | protected override void BackButtonWasPressed(ViewController topViewController) 35 | { 36 | _parentFlow?.DismissFlowCoordinator(this); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /UI/PreferencesListViewController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using HMUI; 3 | using BeatSaberMarkupLanguage.Attributes; 4 | using BeatSaberMarkupLanguage.Components; 5 | using BeatSaberMarkupLanguage.ViewControllers; 6 | using BeatSaberMarkupLanguage.Components.Settings; 7 | using System.ComponentModel; 8 | 9 | namespace JDFixer.UI 10 | { 11 | internal sealed class PreferencesListViewController : BSMLResourceViewController, INotifyPropertyChanged 12 | { 13 | public override string ResourceName => "JDFixer.UI.BSML.preferencesList.bsml"; 14 | 15 | 16 | [UIComponent("njs_slider")] 17 | private SliderSetting NJS_Slider; 18 | 19 | private float New_NJS_Value = 16f; 20 | 21 | [UIValue("njs_value")] 22 | private float NJS_Value 23 | { 24 | get => New_NJS_Value; 25 | set 26 | { 27 | New_NJS_Value = value; 28 | } 29 | } 30 | [UIAction("set_njs_value")] 31 | private void Set_NJS_Value(float value) 32 | { 33 | NJS_Value = value; 34 | } 35 | 36 | 37 | [UIValue("min_jd_slider")] 38 | private float Min_JD_Slider => PluginConfig.Instance.minJumpDistance; 39 | [UIValue("max_jd_slider")] 40 | private float Max_JD_Slider => PluginConfig.Instance.maxJumpDistance; 41 | 42 | [UIComponent("jd_slider")] 43 | private SliderSetting JD_Slider; 44 | 45 | private float New_JD_Value = 18f; 46 | 47 | [UIValue("jd_value")] 48 | private float JD_Value 49 | { 50 | get => New_JD_Value; 51 | set 52 | { 53 | New_JD_Value = value; 54 | } 55 | } 56 | [UIAction("set_jd_value")] 57 | private void Set_JD_Value(float value) 58 | { 59 | JD_Value = value; 60 | } 61 | 62 | 63 | [UIComponent("pref_list")] 64 | private CustomListTableData Pref_List; 65 | private JDPref Selected_Pref = null; 66 | 67 | 68 | [UIAction("select_pref")] 69 | private void Select_Pref(TableView tableView, int row) 70 | { 71 | Selected_Pref = PluginConfig.Instance.preferredValues[row]; 72 | } 73 | 74 | 75 | [UIAction("add_pressed")] 76 | private void Add_Pressed() 77 | { 78 | if (PluginConfig.Instance.preferredValues.Any(x => x.njs == New_NJS_Value)) 79 | { 80 | PluginConfig.Instance.preferredValues.RemoveAll(x => x.njs == New_NJS_Value); 81 | } 82 | PluginConfig.Instance.preferredValues.Add(new JDPref(New_NJS_Value, New_JD_Value)); 83 | Reload_List_From_Config(); 84 | } 85 | 86 | 87 | [UIAction("remove_pressed")] 88 | private void Remove_Pressed() 89 | { 90 | if (Selected_Pref == null) 91 | { 92 | return; 93 | } 94 | PluginConfig.Instance.preferredValues.RemoveAll(x => x == Selected_Pref); 95 | Reload_List_From_Config(); 96 | } 97 | 98 | 99 | private void Reload_List_From_Config() 100 | { 101 | Pref_List.Data.Clear(); 102 | 103 | if (PluginConfig.Instance.preferredValues == null) 104 | { 105 | return; 106 | } 107 | 108 | PluginConfig.Instance.preferredValues.Sort((x, y) => y.njs.CompareTo(x.njs)); 109 | 110 | foreach (var pref in PluginConfig.Instance.preferredValues) 111 | { 112 | Pref_List.Data.Add(new CustomListTableData.CustomCellInfo($"{pref.njs} NJS | {pref.jumpDistance} Jump Distance")); 113 | } 114 | 115 | Pref_List.TableView.ReloadData(); 116 | Pref_List.TableView.ClearSelection(); 117 | Selected_Pref = null; 118 | } 119 | 120 | 121 | protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 122 | { 123 | base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); 124 | if (!firstActivation) 125 | { 126 | Reload_List_From_Config(); 127 | } 128 | } 129 | 130 | protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) 131 | { 132 | base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); 133 | } 134 | 135 | 136 | [UIAction("#post-parse")] 137 | private void PostParse() 138 | { 139 | Reload_List_From_Config(); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /UI/RTPreferencesListViewController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using HMUI; 3 | using BeatSaberMarkupLanguage.Attributes; 4 | using BeatSaberMarkupLanguage.Components; 5 | using BeatSaberMarkupLanguage.ViewControllers; 6 | using BeatSaberMarkupLanguage.Components.Settings; 7 | using System.ComponentModel; 8 | 9 | namespace JDFixer.UI 10 | { 11 | internal sealed class RTPreferencesListViewController : BSMLResourceViewController, INotifyPropertyChanged 12 | { 13 | public override string ResourceName => "JDFixer.UI.BSML.rtPreferencesList.bsml"; 14 | 15 | 16 | [UIComponent("njs_slider")] 17 | private SliderSetting NJS_Slider; 18 | 19 | private float New_NJS_Value = 16f; 20 | 21 | [UIValue("njs_value")] 22 | private float NJS_Value 23 | { 24 | get => New_NJS_Value; 25 | set 26 | { 27 | New_NJS_Value = value; 28 | } 29 | } 30 | [UIAction("set_njs_value")] 31 | private void Set_NJS_Value(float value) 32 | { 33 | NJS_Value = value; 34 | } 35 | 36 | 37 | [UIValue("min_rt_slider")] 38 | private float Min_RT_Slider => PluginConfig.Instance.minReactionTime; 39 | [UIValue("max_rt_slider")] 40 | private float Max_RT_Slider => PluginConfig.Instance.maxReactionTime; 41 | 42 | [UIComponent("rt_slider")] 43 | private SliderSetting RT_Slider; 44 | 45 | private float New_RT_Value = 500f; 46 | 47 | [UIValue("rt_value")] 48 | private float RT_Value 49 | { 50 | get => New_RT_Value; 51 | set 52 | { 53 | New_RT_Value = value; 54 | } 55 | } 56 | [UIAction("set_rt_value")] 57 | private void Set_RT_Value(float value) 58 | { 59 | RT_Value = value; 60 | } 61 | 62 | [UIAction("rt_slider_formatter")] 63 | private string RT_Slider_Formatter(float value) => value.ToString("0") + " ms"; 64 | 65 | 66 | [UIComponent("pref_list")] 67 | private CustomListTableData Pref_List; 68 | private RTPref Selected_Pref = null; 69 | 70 | [UIAction("select_pref")] 71 | private void Select_Pref(TableView tableView, int row) 72 | { 73 | Selected_Pref = PluginConfig.Instance.rt_preferredValues[row]; 74 | } 75 | 76 | 77 | [UIAction("add_pressed")] 78 | private void Add_Pressed() 79 | { 80 | if (PluginConfig.Instance.rt_preferredValues.Any(x => x.njs == New_NJS_Value)) 81 | { 82 | PluginConfig.Instance.rt_preferredValues.RemoveAll(x => x.njs == New_NJS_Value); 83 | } 84 | PluginConfig.Instance.rt_preferredValues.Add(new RTPref(New_NJS_Value, New_RT_Value)); 85 | Reload_List_From_Config(); 86 | } 87 | 88 | 89 | [UIAction("remove_pressed")] 90 | private void Remove_Pressed() 91 | { 92 | if (Selected_Pref == null) 93 | return; 94 | 95 | PluginConfig.Instance.rt_preferredValues.RemoveAll(x => x == Selected_Pref); 96 | Reload_List_From_Config(); 97 | } 98 | 99 | 100 | private void Reload_List_From_Config() 101 | { 102 | Pref_List.Data.Clear(); 103 | 104 | if (PluginConfig.Instance.rt_preferredValues == null) 105 | return; 106 | 107 | PluginConfig.Instance.rt_preferredValues.Sort((x, y) => y.njs.CompareTo(x.njs)); 108 | 109 | foreach (var pref in PluginConfig.Instance.rt_preferredValues) 110 | { 111 | Pref_List.Data.Add(new CustomListTableData.CustomCellInfo($"{pref.njs} NJS | {pref.reactionTime} ms")); 112 | } 113 | 114 | Pref_List.TableView.ReloadData(); 115 | Pref_List.TableView.ClearSelection(); 116 | Selected_Pref = null; 117 | } 118 | 119 | 120 | //---------------------------------------------------------------------------- 121 | 122 | protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 123 | { 124 | base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); 125 | if (!firstActivation) 126 | { 127 | Reload_List_From_Config(); 128 | } 129 | } 130 | 131 | protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) 132 | { 133 | base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); 134 | } 135 | 136 | 137 | [UIAction("#post-parse")] 138 | private void PostParse() 139 | { 140 | Reload_List_From_Config(); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/nike4613/ModSaber-MetadataFileSchema/master/Schema.json", 3 | "author": "Zephyr", 4 | "description": "Automatically change jump distance and reaction time according to customized preferences", 5 | "gameVersion": "1.40.0", 6 | "id": "JDFixer", 7 | "name": "JDFixer", 8 | "version": "7.4.0", 9 | "dependsOn": { 10 | "BSIPA": "^4.3.5", 11 | "BeatSaberMarkupLanguage": "^1.12.2", 12 | "SiraUtil": "^3.1.12" 13 | }, 14 | "loadBefore": ["Chroma", "AccessAbility", "PlayFirst"], 15 | "links": { 16 | "donate": "https://www.patreon.com/xeph_yr" 17 | } 18 | } -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------