├── .gitattributes ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── counters--feature-request.md │ └── counters--issue-report.md ├── .gitignore ├── Counters+.sln ├── Counters+ ├── .gitignore ├── BuildTargets.targets ├── ConfigModels │ ├── ConfigModel.cs │ ├── Counters │ │ ├── CutConfigModel.cs │ │ ├── FailConfigModel.cs │ │ ├── MissedConfigModel.cs │ │ ├── MultiplayerRankConfigModel.cs │ │ ├── NoteConfigModel.cs │ │ ├── NotesLeftConfigModel.cs │ │ ├── PBConfigModel.cs │ │ ├── ProgressConfigModel.cs │ │ ├── ScoreConfigModel.cs │ │ ├── SpeedConfigModel.cs │ │ └── SpinometerConfigModel.cs │ ├── HUDConfigModel.cs │ ├── MainConfigModel.cs │ └── SettableSettings │ │ ├── CountersPlusSettableSettings.cs │ │ └── CountersPlusWrapperSetting.cs ├── Counters+.csproj ├── Counters │ ├── Counter.cs │ ├── Custom │ │ ├── BasicCustomCounter.cs │ │ └── CanvasCustomCounter.cs │ ├── CutCounter.cs │ ├── Event Broadcasters │ │ ├── CounterEventBroadcaster.cs │ │ ├── EventBroadcaster.cs │ │ ├── NoteEventBroadcaster.cs │ │ └── ScoreEventBroadcaster.cs │ ├── FailCounter.cs │ ├── Interfaces │ │ ├── ICounter.cs │ │ ├── IEventHandler.cs │ │ ├── INoteEventHandler.cs │ │ └── IScoreEventHandler.cs │ ├── MissedCounter.cs │ ├── MultiplayerRankCounter.cs │ ├── Note Count Processors │ │ ├── CustomJSONDataNoteCountProcessor.cs │ │ ├── GenericNoteCountProcessor.cs │ │ └── NoteCountProcessor.cs │ ├── NotesCounter.cs │ ├── NotesLeftCounter.cs │ ├── PBCounter.cs │ ├── ProgressBaseGameCounter.cs │ ├── ProgressCounter.cs │ ├── ScoreCounter.cs │ ├── SpeedCounter.cs │ └── Spinometer.cs ├── Custom │ ├── CustomConfigModel.cs │ ├── CustomCounter.cs │ └── CustomCounterFeature.cs ├── Harmony │ └── CoreGameHUDControllerPatch.cs ├── Installers │ ├── CoreInstaller.cs │ ├── CountersInstaller.cs │ ├── MenuUIInstaller.cs │ └── MultiplayerCountersInstaller.cs ├── Multiplayer │ └── CanvasIntroFadeController.cs ├── Plugin.cs ├── Properties │ └── AssemblyInfo.cs ├── UI │ ├── BSML │ │ ├── BlankScreen.bsml │ │ ├── Config │ │ │ ├── Cut.bsml │ │ │ ├── Error.bsml │ │ │ ├── Fail.bsml │ │ │ ├── Missed.bsml │ │ │ ├── Multiplayer Rank.bsml │ │ │ ├── Notes Left.bsml │ │ │ ├── Notes.bsml │ │ │ ├── Personal Best.bsml │ │ │ ├── Progress.bsml │ │ │ ├── Score.bsml │ │ │ ├── Speed.bsml │ │ │ └── Spinometer.bsml │ │ ├── Credits.bsml │ │ ├── EditBase.bsml │ │ ├── HUDs │ │ │ ├── HUDEdit.bsml │ │ │ └── HUDList.bsml │ │ ├── MainSettings.bsml │ │ ├── SettingsBase.bsml │ │ └── SettingsSectionSelection.bsml │ ├── CountersPlusListTableCell.cs │ ├── FlowCoordinators │ │ └── CountersPlusSettingsFlowCoordinator.cs │ ├── Images │ │ ├── Contributors.png │ │ ├── Counters │ │ │ ├── Custom.png │ │ │ ├── Cut.png │ │ │ ├── Fail.png │ │ │ ├── Missed.png │ │ │ ├── Multiplayer Rank.png │ │ │ ├── Notes Left.png │ │ │ ├── Notes.png │ │ │ ├── Personal Best.png │ │ │ ├── Progress.png │ │ │ ├── Score.png │ │ │ ├── Speed.png │ │ │ └── Spinometer.png │ │ ├── Credits.png │ │ ├── Donators.png │ │ ├── GetStickbuggedLOL.apng │ │ ├── HUDs │ │ │ ├── Add.png │ │ │ ├── Manage.png │ │ │ └── Remove.png │ │ ├── Logo.png │ │ └── MainSettings.png │ ├── MenuButtonManager.cs │ ├── MockCounter.cs │ ├── SettingGroups │ │ ├── CountersSettingsGroup.cs │ │ ├── HUDsSettingsGroup.cs │ │ ├── MainSettingsGroup.cs │ │ └── SettingsGroup.cs │ └── ViewControllers │ │ ├── CountersPlusBlankViewController.cs │ │ ├── CountersPlusCreditsViewController.cs │ │ ├── CountersPlusHUDListViewController.cs │ │ ├── CountersPlusMainScreenNavigationController.cs │ │ ├── CountersPlusSettingSectionSelectionViewController.cs │ │ └── Editing │ │ ├── CountersPlusCounterEditViewController.cs │ │ ├── CountersPlusHUDEditViewController.cs │ │ └── CountersPlusMainSettingsEditViewController.cs ├── Utils │ ├── Accessors.cs │ ├── AprilFools.cs │ ├── CanvasUtility.cs │ ├── EnumerableExtensions.cs │ ├── ImagesUtility.cs │ ├── SharedCoroutineStarter.cs │ ├── SoftParent.cs │ └── VersionUtility.cs └── manifest.json ├── LICENSE └── README.md /.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: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: Caeden117 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/counters--feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Counters+ Feature Request 3 | about: Suggest an idea for Counters+! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is it because of an issue?** 11 | If it is, tell me what's up! 12 | 13 | **Describe your solution** 14 | Give me what you want. Short descriptions, long novels, don't matter to me. However, the more ideas I get in my head, the better! 15 | 16 | **A programmer yourself?** 17 | If you have experience in programming (Preferably C#), and either have or have not gotten into Beat Saber modding, feel free to fork Counters+, and try to implement this idea for yourself. I'm always open to pull requests! 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/counters--issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Counters+ Issue Report 3 | about: Create a helpful issue for me to view and attempt to fix! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the Bug** 11 | A clear and concise description of what the problem is. A simple "Counters+ doesn't work" aint gonna cut it, chief! 12 | 13 | **Attach or Link Files** 14 | To help me solve this issue, I might need access to more then what you say or provide. 15 | 16 | A helpful start is to attach your `latest.log` file from the `/Logs` folder. If you feel it is necessary, you can also attach the configuration file found in `/UserData/CountersPlus.json`. 17 | 18 | **Screenshots or Video** 19 | If applicable, add screenshots or videos to help me out. Sometimes, words ain't enough! 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /Counters+.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29209.62 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution items", "{68603FE6-2E4E-4062-A149-F685F27251A5}" 7 | ProjectSection(SolutionItems) = preProject 8 | Counters+\Counters+.csproj.user = Counters+\Counters+.csproj.user 9 | README.md = README.md 10 | EndProjectSection 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Counters+", "Counters+\Counters+.csproj", "{A5DBF7F3-FE93-4BB1-8023-2DD50DFD3350}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {A5DBF7F3-FE93-4BB1-8023-2DD50DFD3350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {A5DBF7F3-FE93-4BB1-8023-2DD50DFD3350}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {A5DBF7F3-FE93-4BB1-8023-2DD50DFD3350}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {A5DBF7F3-FE93-4BB1-8023-2DD50DFD3350}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {47A61869-FDC2-4AF8-A1D0-29EAF1AB19CE} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Counters+/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore References folder 2 | References/ 3 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/ConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using System; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | 7 | namespace CountersPlus.ConfigModels 8 | { 9 | /// 10 | /// The base config class for every single Counter in Counters+. 11 | /// As part of creating a new Counter, you will need to make a class that inherits ConfigModel. 12 | /// For adding new options to an existing Counter, add them to their respective ConfigModel. 13 | /// 14 | public abstract class ConfigModel 15 | { 16 | [Ignore] 17 | public virtual string DisplayName { get; set; } = "Unknown"; 18 | 19 | [UIValue(nameof(Enabled))] 20 | public virtual bool Enabled { get; set; } = false; 21 | 22 | [UseConverter] 23 | [UIValue(nameof(Position))] 24 | public virtual CounterPositions Position { get; set; } = CounterPositions.BelowCombo; 25 | 26 | [UIValue(nameof(Distance))] 27 | public virtual int Distance { get; set; } = 2; 28 | 29 | // Default Canvas will be the main Counters+ one 30 | // Oh yeah baby, we're gonna support multiple canvases 31 | [UIValue(nameof(CanvasID))] 32 | public virtual int CanvasID { get; set; } = -1; 33 | 34 | #region UI 35 | [UIValue(nameof(Distances))] 36 | public List Distances => AllDistances.Cast().ToList(); 37 | 38 | [UIValue(nameof(Positions))] 39 | public List Positions => PositionToNames.Keys.Cast().ToList(); 40 | 41 | [UIAction(nameof(PositionsFormat))] 42 | public string PositionsFormat(CounterPositions pos) => PositionToNames[pos]; 43 | 44 | private static List AllDistances = new List() { -1, 0, 1, 2, 3, 4 }; 45 | 46 | private static Dictionary PositionToNames = new Dictionary() 47 | { 48 | {CounterPositions.BelowCombo, "Below Combo" }, 49 | {CounterPositions.AboveCombo, "Above Combo" }, 50 | {CounterPositions.BelowMultiplier, "Below Multiplier" }, 51 | {CounterPositions.AboveMultiplier, "Above Multiplier" }, 52 | {CounterPositions.BelowEnergy, "Below Energy" }, 53 | {CounterPositions.AboveHighway, "Over Highway" } 54 | }; 55 | 56 | 57 | [UIValue(nameof(DecimalPrecisions))] 58 | public List DecimalPrecisions => new List() { 0, 1, 2, 3, 4 }; 59 | 60 | // I'll admit, these function fields are a bit gross, but they allow the CountersPlusCounterEditViewController to pass in data that the ConfigModel can't usually see. 61 | [Ignore] public Func GetCanvasFromID; 62 | [Ignore] public Func GetCanvasIDFromCanvasSettings; 63 | [Ignore] public Func> GetAllCanvases; 64 | 65 | [UIValue(nameof(AttachedCanvas))] 66 | [Ignore] 67 | public virtual HUDCanvas AttachedCanvas 68 | { 69 | get => GetCanvasFromID(CanvasID); 70 | set => CanvasID = GetCanvasIDFromCanvasSettings(value); 71 | } 72 | 73 | [Ignore] 74 | public List AllCanvases => GetAllCanvases().Cast().ToList(); 75 | 76 | [UIAction(nameof(CanvasesFormat))] 77 | public string CanvasesFormat(HUDCanvas canvas) => canvas.Name; 78 | 79 | #endregion UI 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/CutConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | 4 | namespace CountersPlus.ConfigModels 5 | { 6 | internal class CutConfigModel : ConfigModel 7 | { 8 | [Ignore] 9 | public override string DisplayName => "Cut"; 10 | 11 | public override bool Enabled { get; set; } = false; 12 | [UseConverter] 13 | public override CounterPositions Position { get; set; } = CounterPositions.AboveHighway; 14 | public override int Distance { get; set; } = 1; 15 | 16 | [UIValue(nameof(SeparateSaberCounts))] 17 | public virtual bool SeparateSaberCounts { get; set; } = false; 18 | 19 | [UIValue(nameof(SeparateCutValues))] 20 | public virtual bool SeparateCutValues { get; set; } = false; 21 | 22 | [UIValue(nameof(AveragePrecision))] 23 | public virtual int AveragePrecision { get; set; } = 1; 24 | 25 | [UIValue(nameof(IncludeArcs))] 26 | public virtual bool IncludeArcs { get; set; } = false; 27 | 28 | [UIValue(nameof(IncludeChains))] 29 | public virtual bool IncludeChains { get; set; } = false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/FailConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | 4 | namespace CountersPlus.ConfigModels 5 | { 6 | internal class FailConfigModel : ConfigModel 7 | { 8 | [Ignore] 9 | public override string DisplayName => "Fail"; 10 | 11 | public override bool Enabled { get; set; } = false; 12 | [UseConverter] 13 | public override CounterPositions Position { get; set; } = CounterPositions.AboveCombo; 14 | public override int Distance { get; set; } = 0; 15 | 16 | [UIValue(nameof(ShowRestartsInstead))] 17 | public virtual bool ShowRestartsInstead { get; set; } = false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/MissedConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | 4 | namespace CountersPlus.ConfigModels 5 | { 6 | internal class MissedConfigModel : ConfigModel 7 | { 8 | [Ignore] 9 | public override string DisplayName => "Missed"; 10 | 11 | public override bool Enabled { get; set; } = true; 12 | [UseConverter] 13 | public override CounterPositions Position { get; set; } = CounterPositions.BelowCombo; 14 | public override int Distance { get; set; } = 0; 15 | 16 | [UIValue(nameof(CountBadCuts))] 17 | public virtual bool CountBadCuts { get; set; } = true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/MultiplayerRankConfigModel.cs: -------------------------------------------------------------------------------- 1 | using IPA.Config.Stores.Attributes; 2 | 3 | namespace CountersPlus.ConfigModels 4 | { 5 | internal class MultiplayerRankConfigModel : ConfigModel 6 | { 7 | [Ignore] 8 | public override string DisplayName => "Multiplayer Rank"; 9 | 10 | 11 | public override bool Enabled { get; set; } = false; 12 | 13 | [UseConverter] 14 | public override CounterPositions Position { get; set; } = CounterPositions.AboveHighway; 15 | 16 | public override int Distance { get; set; } = 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/NoteConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | 4 | namespace CountersPlus.ConfigModels 5 | { 6 | internal class NoteConfigModel : ConfigModel 7 | { 8 | [Ignore] 9 | public override string DisplayName => "Notes"; 10 | 11 | public override bool Enabled { get; set; } = false; 12 | [UseConverter] 13 | public override CounterPositions Position { get; set; } = CounterPositions.BelowCombo; 14 | public override int Distance { get; set; } = 1; 15 | 16 | [UIValue(nameof(ShowPercentage))] 17 | public virtual bool ShowPercentage { get; set; } = false; 18 | [UIValue(nameof(DecimalPrecision))] 19 | public virtual int DecimalPrecision { get; set; } = 2; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/NotesLeftConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | 4 | namespace CountersPlus.ConfigModels 5 | { 6 | internal class NotesLeftConfigModel : ConfigModel 7 | { 8 | [Ignore] 9 | public override string DisplayName => "Notes Left"; 10 | 11 | public override bool Enabled { get; set; } = false; 12 | [UseConverter] 13 | public override CounterPositions Position { get; set; } = CounterPositions.AboveHighway; 14 | public override int Distance { get; set; } = -1; 15 | 16 | [UIValue(nameof(LabelAboveCount))] 17 | public virtual bool LabelAboveCount { get; set; } = false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/PBConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using IPA.Config.Stores.Converters; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | 7 | namespace CountersPlus.ConfigModels 8 | { 9 | internal class PBConfigModel : ConfigModel 10 | { 11 | [Ignore] 12 | public override string DisplayName => "Personal Best"; 13 | 14 | public override bool Enabled { get; set; } = true; 15 | [UseConverter] 16 | public override CounterPositions Position { get; set; } = CounterPositions.BelowMultiplier; 17 | public override int Distance { get; set; } = 1; 18 | 19 | [UseConverter] 20 | [UIValue(nameof(Mode))] 21 | public virtual PBMode Mode { get; set; } = PBMode.Absolute; 22 | 23 | [UseConverter(typeof(HexColorConverter))] 24 | [UIValue(nameof(BetterColor))] 25 | public virtual Color BetterColor { get; set; } = Color.red; 26 | [UseConverter(typeof(HexColorConverter))] 27 | [UIValue(nameof(DefaultColor))] 28 | public virtual Color DefaultColor { get; set; } = new Color(1.0f, (float)0xa5 / 255, 0.0f, 1.0f); 29 | 30 | [UIValue(nameof(DecimalPrecision))] 31 | public virtual int DecimalPrecision { get; set; } = 2; 32 | [UIValue(nameof(TextSize))] 33 | public virtual int TextSize { get; set; } = 2; 34 | [UIValue(nameof(UnderScore))] 35 | public virtual bool UnderScore { get; set; } = true; 36 | [UIValue(nameof(HideFirstScore))] 37 | public virtual bool HideFirstScore { get; set; } = false; 38 | 39 | [UIValue(nameof(TextSizes))] 40 | public List TextSizes => new List() { 2, 3, 4, 5 }; 41 | 42 | [UIValue(nameof(Modes))] 43 | public List Modes => new List() { PBMode.Absolute, PBMode.Relative }; 44 | } 45 | 46 | public enum PBMode { Absolute, Relative } 47 | } 48 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/ProgressConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | 6 | namespace CountersPlus.ConfigModels 7 | { 8 | internal class ProgressConfigModel : ConfigModel 9 | { 10 | [Ignore] 11 | public override string DisplayName => "Progress"; 12 | 13 | public override bool Enabled { get; set; } = true; 14 | [UseConverter] 15 | public override CounterPositions Position { get; set; } = CounterPositions.BelowEnergy; 16 | public override int Distance { get; set; } = 0; 17 | 18 | [UseConverter] 19 | [UIValue(nameof(Mode))] 20 | public virtual ProgressMode Mode { get; set; } = ProgressMode.Original; 21 | [UIValue(nameof(ProgressTimeLeft))] 22 | public virtual bool ProgressTimeLeft { get; set; } = false; 23 | [UIValue(nameof(IncludeRing))] 24 | public virtual bool IncludeRing { get; set; } = false; 25 | 26 | [UIValue(nameof(Modes))] 27 | public List Modes => ModeToNames.Keys.Cast().ToList(); 28 | 29 | [UIAction(nameof(ModeFormat))] 30 | public string ModeFormat(ProgressMode pos) => ModeToNames[pos]; 31 | 32 | private static Dictionary ModeToNames = new Dictionary() 33 | { 34 | { ProgressMode.Original, "Original" }, 35 | { ProgressMode.BaseGame, "Base Game" }, 36 | { ProgressMode.TimeInBeats, "Time in Beats" }, 37 | { ProgressMode.Percent, "Percentage" }, 38 | }; 39 | } 40 | 41 | public enum ProgressMode { Original, BaseGame, TimeInBeats, Percent } 42 | } 43 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/ScoreConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using IPA.Config.Stores.Converters; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | using UnityEngine; 7 | 8 | namespace CountersPlus.ConfigModels 9 | { 10 | internal class ScoreConfigModel : ConfigModel 11 | { 12 | [Ignore] 13 | public override string DisplayName => "Score"; 14 | 15 | public override bool Enabled { get; set; } = true; 16 | [UseConverter] 17 | public override CounterPositions Position { get; set; } = CounterPositions.BelowMultiplier; 18 | public override int Distance { get; set; } = 0; 19 | 20 | [UseConverter] 21 | [UIValue(nameof(Mode))] 22 | public virtual ScoreMode Mode { get; set; } = ScoreMode.Original; 23 | [UIValue(nameof(DecimalPrecision))] 24 | public virtual int DecimalPrecision { get; set; } = 2; 25 | [UIValue(nameof(DisplayRank))] 26 | public virtual bool DisplayRank { get; set; } = true; 27 | [UIValue(nameof(CustomRankColors))] 28 | public virtual bool CustomRankColors { get; set; } = true; 29 | [UseConverter(typeof(HexColorConverter))] 30 | [UIValue(nameof(SSColor))] 31 | public virtual Color SSColor { get; set; } = Color.cyan; 32 | [UseConverter(typeof(HexColorConverter))] 33 | [UIValue(nameof(SColor))] 34 | public virtual Color SColor { get; set; } = Color.white; 35 | [UseConverter(typeof(HexColorConverter))] 36 | [UIValue(nameof(AColor))] 37 | public virtual Color AColor { get; set; } = Color.green; 38 | [UseConverter(typeof(HexColorConverter))] 39 | [UIValue(nameof(BColor))] 40 | public virtual Color BColor { get; set; } = Color.yellow; 41 | [UseConverter(typeof(HexColorConverter))] 42 | [UIValue(nameof(CColor))] 43 | public virtual Color CColor { get; set; } = new Color(1, 0.5f, 0); 44 | [UseConverter(typeof(HexColorConverter))] 45 | [UIValue(nameof(DColor))] 46 | public virtual Color DColor { get; set; } = Color.red; 47 | [UseConverter(typeof(HexColorConverter))] 48 | [UIValue(nameof(EColor))] 49 | public virtual Color EColor { get; set; } = Color.red; 50 | 51 | [UIValue(nameof(Modes))] 52 | public List Modes => ModeToNames.Keys.Cast().ToList(); 53 | 54 | [UIAction(nameof(ModeFormat))] 55 | public string ModeFormat(ScoreMode pos) => ModeToNames[pos]; 56 | 57 | private static Dictionary ModeToNames = new Dictionary() 58 | { 59 | { ScoreMode.Original, "Original" }, 60 | { ScoreMode.LeavePoints, "Dont Move Points" }, 61 | { ScoreMode.ScoreOnly, "Remove Rank" }, 62 | { ScoreMode.RankOnly, "Remove Percentage" }, 63 | }; 64 | 65 | public Color GetRankColorFromRank(RankModel.Rank rank) => rank switch 66 | { 67 | RankModel.Rank.S => SColor, 68 | RankModel.Rank.A => AColor, 69 | RankModel.Rank.B => BColor, 70 | RankModel.Rank.C => CColor, 71 | RankModel.Rank.D => DColor, 72 | RankModel.Rank.E => EColor, 73 | _ => SSColor, 74 | }; 75 | } 76 | 77 | public enum ScoreMode { Original, ScoreOnly, LeavePoints, RankOnly } 78 | } 79 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/SpeedConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | 6 | namespace CountersPlus.ConfigModels 7 | { 8 | internal class SpeedConfigModel : ConfigModel 9 | { 10 | [Ignore] 11 | public override string DisplayName => "Speed"; 12 | 13 | public override bool Enabled { get; set; } = false; 14 | [UseConverter] 15 | public override CounterPositions Position { get; set; } = CounterPositions.BelowMultiplier; 16 | public override int Distance { get; set; } = 2; 17 | 18 | [UIValue(nameof(DecimalPrecision))] 19 | public virtual int DecimalPrecision { get; set; } = 2; 20 | [UseConverter] 21 | [UIValue(nameof(Mode))] 22 | public virtual SpeedMode Mode { get; set; } = SpeedMode.Average; 23 | 24 | [UIValue(nameof(Modes))] 25 | public List Modes => ModeToNames.Keys.Cast().ToList(); 26 | 27 | [UIAction(nameof(ModeFormat))] 28 | public string ModeFormat(SpeedMode pos) => ModeToNames[pos]; 29 | 30 | private static Dictionary ModeToNames = new Dictionary() 31 | { 32 | { SpeedMode.Average, "Average" }, 33 | { SpeedMode.Top5Sec, "Top from 5 Seconds" }, 34 | { SpeedMode.Both, "Both Metrics" }, 35 | { SpeedMode.SplitAverage, "Split Average" }, 36 | { SpeedMode.SplitBoth, "Split Both Metrics" }, 37 | }; 38 | } 39 | 40 | public enum SpeedMode { Average, Top5Sec, Both, SplitAverage, SplitBoth } 41 | } 42 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/Counters/SpinometerConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace CountersPlus.ConfigModels 7 | { 8 | internal class SpinometerConfigModel : ConfigModel 9 | { 10 | [Ignore] 11 | public override string DisplayName => "Spinometer"; 12 | 13 | public override bool Enabled { get; set; } = false; 14 | [UseConverter] 15 | public override CounterPositions Position { get; set; } = CounterPositions.AboveMultiplier; 16 | public override int Distance { get; set; } = 0; 17 | 18 | [UseConverter] 19 | public virtual SpinometerMode Mode { get; set; } = SpinometerMode.Highest; 20 | 21 | [UIValue(nameof(Modes))] 22 | public List Modes => ModeToNames.Keys.Cast().ToList(); 23 | 24 | [UIAction(nameof(ModeFormat))] 25 | public string ModeFormat(SpinometerMode pos) => ModeToNames[pos]; 26 | 27 | private static Dictionary ModeToNames = new Dictionary() 28 | { 29 | { SpinometerMode.Average, "Average" }, 30 | { SpinometerMode.SplitAverage, "Split Average" }, 31 | { SpinometerMode.Highest, "Highest" }, 32 | }; 33 | } 34 | 35 | public enum SpinometerMode { Average, SplitAverage, Highest } 36 | } 37 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/HUDConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using IPA.Config.Stores.Attributes; 3 | using IPA.Config.Stores.Converters; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | 9 | namespace CountersPlus.ConfigModels 10 | { 11 | public class HUDConfigModel 12 | { 13 | public string DisplayName => "HUD Settings"; 14 | 15 | public HUDCanvas MainCanvasSettings { get; set; } = new HUDCanvas() 16 | { 17 | Name = "Main", 18 | IsMainCanvas = true, 19 | MatchBaseGameHUDDepth = true 20 | }; 21 | 22 | [UseConverter(typeof(ListConverter))] 23 | public List OtherCanvasSettings { get; set; } = new List(); 24 | } 25 | 26 | public class HUDCanvas 27 | { 28 | [UIValue(nameof(Name))] 29 | public virtual string Name { get; set; } = "New Canvas"; 30 | 31 | [Ignore] 32 | public bool IsMainCanvas = false; 33 | 34 | [UIValue(nameof(ParentedToBaseGameHUD))] 35 | public virtual bool ParentedToBaseGameHUD { get; set; } = true; 36 | 37 | [UIValue(nameof(IgnoreNoTextAndHUDOption))] 38 | public virtual bool IgnoreNoTextAndHUDOption { get; set; } = false; 39 | 40 | [UIValue(nameof(Size))] 41 | public virtual float Size { get; set; } = 10; 42 | 43 | [UIValue(nameof(PositionScale))] 44 | public virtual float PositionScale { get; set; } = 10; 45 | 46 | [Ignore] 47 | public virtual Vector3 Position => new Vector3(Pos_X, Pos_Y, Pos_Z); 48 | 49 | [UIValue(nameof(Pos_X))] 50 | public virtual float Pos_X { get; set; } = 0; 51 | 52 | [UIValue(nameof(Pos_Y))] 53 | public virtual float Pos_Y { get; set; } = 0; 54 | 55 | [UIValue(nameof(Pos_Z))] 56 | public virtual float Pos_Z { get; set; } = 7; 57 | 58 | [UIValue(nameof(MatchBaseGameHUDDepth))] 59 | public virtual bool MatchBaseGameHUDDepth { get; set; } = false; 60 | 61 | [Ignore] 62 | public virtual Vector3 Rotation => new Vector3(Rot_X, Rot_Y, Rot_Z); 63 | 64 | [UIValue(nameof(Rot_X))] 65 | public virtual float Rot_X { get; set; } = 0; 66 | 67 | [UIValue(nameof(Rot_Y))] 68 | public virtual float Rot_Y { get; set; } = 0; 69 | 70 | [UIValue(nameof(Rot_Z))] 71 | public virtual float Rot_Z { get; set; } = 0; 72 | 73 | [UIValue(nameof(AttachHUDToCamera))] 74 | public virtual bool AttachHUDToCamera { get; set; } = false; 75 | 76 | public virtual string AttachedCamera { get; set; } = "Main Camera"; 77 | 78 | [UIValue(nameof(IgnoreShockwaveEffect))] 79 | public virtual bool IgnoreShockwaveEffect { get; set; } = true; 80 | 81 | [UIValue(nameof(CurveRadius))] 82 | public virtual float CurveRadius { get; set; } = 0; 83 | 84 | [UIValue(nameof(DistanceModifier))] 85 | public virtual float DistanceModifier { get; set; } = 1f; 86 | 87 | #region UI 88 | public event Action OnCanvasSettingsChanged; 89 | public event Action OnCanvasSettingsApply; 90 | 91 | [UIAction("fire-update")] 92 | public void OnChanged(object _) 93 | { 94 | Utils.SharedCoroutineStarter.instance.StartCoroutine(DelayedFire(OnCanvasSettingsChanged)); 95 | } 96 | 97 | [UIAction("fire-apply")] 98 | public void OnApply() 99 | { 100 | Utils.SharedCoroutineStarter.instance.StartCoroutine(DelayedFire(OnCanvasSettingsApply)); 101 | } 102 | 103 | private IEnumerator DelayedFire(Action action) 104 | { 105 | yield return new WaitForEndOfFrame(); 106 | action?.Invoke(); 107 | } 108 | #endregion 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/MainConfigModel.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using CountersPlus.Custom; 3 | using IPA.Config.Stores.Attributes; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | 9 | namespace CountersPlus.ConfigModels 10 | { 11 | /// 12 | /// Main class for Counters+ config. 13 | /// For adding new Counters, add their ConfigModels as a field in this class, making sure that "Config" is in the name. 14 | /// 15 | internal class MainConfigModel 16 | { 17 | public string DisplayName => "Main"; 18 | [UIValue(nameof(Enabled))] 19 | public virtual bool Enabled { get; set; } = true; 20 | [UIValue(nameof(HideCombo))] 21 | public virtual bool HideCombo { get; set; } = false; 22 | [UIValue(nameof(HideMultiplier))] 23 | public virtual bool HideMultiplier { get; set; } = false; 24 | [UIValue(nameof(HideMultiplayerRank))] 25 | public virtual bool HideMultiplayerRank { get; set; } = false; 26 | [UIValue(nameof(ComboOffset))] 27 | public virtual float ComboOffset { get; set; } = 0.2f; 28 | [UIValue(nameof(MultiplierOffset))] 29 | public virtual float MultiplierOffset { get; set; } = 0.4f; 30 | [UIValue(nameof(ItalicText))] 31 | public virtual bool ItalicText { get; set; } = false; 32 | [UIValue(nameof(AprilFoolsTomfoolery))] 33 | public virtual bool AprilFoolsTomfoolery { get; set; } = true; 34 | public virtual HUDConfigModel HUDConfig { get; set; } = new HUDConfigModel(); 35 | public virtual MissedConfigModel MissedConfig { get; set; } = new MissedConfigModel(); 36 | public virtual NoteConfigModel NoteConfig { get; set; } = new NoteConfigModel(); 37 | public virtual ProgressConfigModel ProgressConfig { get; set; } = new ProgressConfigModel(); 38 | public virtual ScoreConfigModel ScoreConfig { get; set; } = new ScoreConfigModel(); 39 | public virtual PBConfigModel PBConfig { get; set; } = new PBConfigModel(); 40 | public virtual SpeedConfigModel SpeedConfig { get; set; } = new SpeedConfigModel(); 41 | public virtual CutConfigModel CutConfig { get; set; } = new CutConfigModel(); 42 | public virtual SpinometerConfigModel SpinometerConfig { get; set; } = new SpinometerConfigModel(); 43 | public virtual NotesLeftConfigModel NotesLeftConfig { get; set; } = new NotesLeftConfigModel(); 44 | public virtual FailConfigModel FailsConfig { get; set; } = new FailConfigModel(); 45 | public virtual MultiplayerRankConfigModel MultiplayerRankConfig { get; set; } = new MultiplayerRankConfigModel(); 46 | 47 | [UseConverter] 48 | public virtual Dictionary CustomCounters { get; set; } = new Dictionary(); 49 | 50 | public event Action OnConfigChanged; 51 | 52 | public virtual void Changed() 53 | { 54 | Utils.SharedCoroutineStarter.instance.StartCoroutine(DelayedFire(OnConfigChanged)); 55 | } 56 | 57 | public List Offsets => new List { 0, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1 }; 58 | 59 | [UIValue("IsAprilFools")] 60 | public bool IsAprilFools => DateTime.Now.Month == 4 && DateTime.Now.Day == 1; 61 | 62 | private IEnumerator DelayedFire(Action action) 63 | { 64 | yield return new WaitForEndOfFrame(); 65 | action?.Invoke(); 66 | } 67 | } 68 | 69 | public enum CounterPositions { BelowCombo, AboveCombo, BelowMultiplier, AboveMultiplier, BelowEnergy, AboveHighway } 70 | } 71 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/SettableSettings/CountersPlusSettableSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using Heck.SettingsSetter; 7 | using IPA.Config.Stores.Attributes; 8 | using static CountersPlus.Utils.EnumerableExtensions; 9 | 10 | namespace CountersPlus.ConfigModels.SettableSettings 11 | { 12 | internal class CountersPlusSettableSettings : IDisposable 13 | { 14 | public static bool HasExecutedBefore { get; private set; } = false; 15 | 16 | private const string countersPlusIdentifier = "_countersPlus"; 17 | 18 | private readonly BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; 19 | 20 | private static List settableSettings = new(); 21 | 22 | // ALRIGHT this is going to be a dousy 23 | // We have hundreds of settings to expose to Heck, aint no way in heck (haha) that I'm manually registering all of them. 24 | // Time to Reflection this bitch! 25 | public CountersPlusSettableSettings(List configs, MainConfigModel mainConfigModel, HUDConfigModel hudConfig) 26 | { 27 | if (HasExecutedBefore) return; 28 | 29 | HasExecutedBefore = true; 30 | 31 | // Grab all of the objects i'll make configurable to the mapper 32 | // (All counter config models, plus main settings and main canvas) 33 | // and ensure only one of each is in the collection 34 | var configurableObjects = configs 35 | .DistinctBy(x => x.DisplayName) // DistinctBy LINQ method might not be needed, zenject supposedly doesn't return duplicate objects but it currently acts as a safe-guard. 36 | .Cast() 37 | .Append(mainConfigModel) 38 | .Append(hudConfig.MainCanvasSettings); 39 | 40 | // Caching these before the loop 41 | var settableSettingType = typeof(CountersPlusWrapperSetting); 42 | var ignoreAttributeType = typeof(IgnoreAttribute); 43 | 44 | // Iterate through all of the settings 45 | foreach (var configurableObj in configurableObjects) 46 | { 47 | var type = configurableObj.GetType(); 48 | 49 | // Grab their display name 50 | var displayNameType = type.GetProperty("DisplayName", bindingFlags) ?? type.GetProperty("Name", bindingFlags); 51 | var displayName = displayNameType.GetValue(configurableObj) as string; 52 | 53 | // Grab the properties I want to register 54 | var applicableProperties = type 55 | .GetProperties(bindingFlags) 56 | .Where(x => 57 | // Filter types assigned by BSIPA Config (virtual read/write properties) 58 | x.CanWrite && x.CanRead && x.GetMethod.IsVirtual 59 | // as well as types that Heck supports (structs/value types) 60 | && x.PropertyType.IsValueType 61 | // and types that arent ignored by de/serialization. 62 | && x.GetCustomAttribute(ignoreAttributeType) == null 63 | // and I guess not ending with #? Might be a BSIPA config thing. 64 | && !x.Name.EndsWith("#")); 65 | 66 | // Iterate over these properties 67 | foreach (var applicableProperty in applicableProperties) 68 | { 69 | var propertyName = applicableProperty.Name; 70 | 71 | // Dynamically create settable setting object 72 | var settableSetting = Activator.CreateInstance(settableSettingType, 73 | $"Counters+ | {displayName}", propertyName, 74 | applicableProperty, configurableObj) as ISettableSetting; 75 | 76 | settableSettings.Add(settableSetting); 77 | 78 | var fieldName = GetFieldName(displayName, propertyName); 79 | 80 | // Haha register 81 | SettingSetterSettableSettingsManager.RegisterSettableSetting(countersPlusIdentifier, fieldName, settableSetting); 82 | 83 | //Plugin.Logger.Debug($"Registered {fieldName}."); 84 | } 85 | } 86 | 87 | Plugin.Logger.Notice($"Registered {settableSettings.Count} settings to Heck's settable settings system."); 88 | } 89 | 90 | public void Dispose() 91 | { 92 | settableSettings.ForEach(x => x.SetTemporary(null)); 93 | } 94 | 95 | private string GetFieldName(string displayName, string propertyName) 96 | => $"_{CleanUpString(displayName.ToLowerInvariant())}{CleanUpString(propertyName)}"; 97 | 98 | private string CleanUpString(string original) 99 | { 100 | var builder = new StringBuilder(); 101 | 102 | var nextCharacterUppercase = false; 103 | 104 | foreach (var ch in original) 105 | { 106 | if (ch == '_' || ch == '#') continue; 107 | 108 | if (ch == ' ') 109 | { 110 | nextCharacterUppercase = true; 111 | continue; 112 | } 113 | 114 | if (nextCharacterUppercase) 115 | { 116 | builder.Append(ch.ToString().ToUpperInvariant()); 117 | nextCharacterUppercase = false; 118 | continue; 119 | } 120 | 121 | builder.Append(ch); 122 | } 123 | 124 | return builder.ToString(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Counters+/ConfigModels/SettableSettings/CountersPlusWrapperSetting.cs: -------------------------------------------------------------------------------- 1 | using Heck.SettingsSetter; 2 | using System; 3 | using System.Reflection; 4 | using UnityEngine; 5 | 6 | namespace CountersPlus.ConfigModels.SettableSettings 7 | { 8 | public class CountersPlusWrapperSetting : ISettableSetting 9 | { 10 | private readonly PropertyInfo settingsProperty; 11 | private readonly object settingsInstance; 12 | 13 | private object originalValue; 14 | 15 | public CountersPlusWrapperSetting(string groupName, string fieldName, 16 | PropertyInfo settingsProperty, object settingsInstance) 17 | { 18 | GroupName = groupName; 19 | FieldName = fieldName; 20 | 21 | this.settingsProperty = settingsProperty; 22 | this.settingsInstance = settingsInstance; 23 | } 24 | 25 | public string GroupName { get; } 26 | 27 | public string FieldName { get; } 28 | 29 | public object TrueValue => settingsProperty.GetValue(settingsInstance); 30 | 31 | public void SetTemporary(object tempValue) 32 | { 33 | if (tempValue != null) 34 | { 35 | originalValue = settingsProperty.GetValue(settingsInstance); 36 | 37 | if (settingsProperty.PropertyType.IsEnum) 38 | { 39 | tempValue = Enum.Parse(settingsProperty.PropertyType, tempValue.ToString()); 40 | } 41 | else if (settingsProperty.PropertyType == typeof(Color)) 42 | { 43 | ColorUtility.TryParseHtmlString(tempValue.ToString(), out var tempColorValue); 44 | tempValue = tempColorValue; 45 | } 46 | else if (settingsProperty.PropertyType == typeof(int)) 47 | { 48 | tempValue = Convert.ToInt32(tempValue); 49 | } 50 | 51 | settingsProperty.SetValue(settingsInstance, tempValue); 52 | } 53 | else if (originalValue != null) 54 | { 55 | settingsProperty.SetValue(settingsInstance, originalValue); 56 | originalValue = null; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Counters+/Counters/Counter.cs: -------------------------------------------------------------------------------- 1 | using Zenject; 2 | using CountersPlus.Counters.Interfaces; 3 | using CountersPlus.ConfigModels; 4 | using CountersPlus.Utils; 5 | using TMPro; 6 | using UnityEngine; 7 | 8 | namespace CountersPlus.Counters 9 | { 10 | /// 11 | /// The base class for a counter, which supplies basic utilities for that counter to use. 12 | /// This is barebones, and only contains events pertaining to the initialization and destruction of a counter. 13 | /// To access more events, inherit from s. 14 | /// 15 | internal class Counter : ICounter where T : ConfigModel 16 | { 17 | [Inject] protected T Settings; 18 | [Inject] protected CanvasUtility CanvasUtility; 19 | 20 | public virtual void CounterInit() { } 21 | 22 | public virtual void CounterDestroy() { } 23 | 24 | protected void GenerateBasicText(string labelText, out TMP_Text count) 25 | { 26 | TMP_Text label = CanvasUtility.CreateTextFromSettings(Settings); 27 | label.fontSize = 3; 28 | label.text = labelText; 29 | 30 | HUDCanvas canvas = CanvasUtility.GetCanvasSettingsFromID(Settings.CanvasID); 31 | 32 | count = CanvasUtility.CreateTextFromSettings(Settings, new Vector3(0, -0.4f * (10 / canvas.PositionScale), 0)); 33 | count.text = "0"; 34 | count.fontSize = 4; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Counters+/Counters/Custom/BasicCustomCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Counters.Interfaces; 2 | using CountersPlus.Custom; 3 | using CountersPlus.Utils; 4 | using Zenject; 5 | 6 | namespace CountersPlus.Counters.Custom 7 | { 8 | /// 9 | /// A barebones Custom Counter class that provides utilities for creating custom text. 10 | /// 11 | public abstract class BasicCustomCounter : ICounter 12 | { 13 | /// 14 | /// Helper class for creating text within Counters+'s system. 15 | /// Not recommended for creating text belonging outside of Counters+. 16 | /// 17 | [Inject] protected CanvasUtility CanvasUtility; 18 | 19 | /// 20 | /// The for your Custom Counter. 21 | /// Use it to help position your text with . 22 | /// 23 | [Inject] protected CustomConfigModel Settings; 24 | 25 | /// 26 | /// Called when the Counter has initialized; it is ready to create text and subscribe to events. 27 | /// 28 | public abstract void CounterInit(); 29 | 30 | /// 31 | /// Called when a Counter is destroyed; it is ready to cleanup and unsubscribe from events. 32 | /// 33 | public abstract void CounterDestroy(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Counters+/Counters/Custom/CanvasCustomCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using System.Collections; 3 | using UnityEngine; 4 | 5 | namespace CountersPlus.Counters.Custom 6 | { 7 | /// 8 | /// A custom counter that re-parents an existing into the Custom Counter system. 9 | /// 10 | public class CanvasCustomCounter : BasicCustomCounter 11 | { 12 | /// 13 | /// The name of the Canvas that this Custom Counter will look for. 14 | /// Use this if Counters+ does not immediately find the Canvas on startup. 15 | /// 16 | public virtual string CanvasObjectName => null; 17 | 18 | /// 19 | /// A direct reference to the Canvas object. You should try using this first. 20 | /// 21 | public virtual Canvas CanvasReference => null; 22 | 23 | public override void CounterInit() 24 | { 25 | PreReparent(); 26 | if (CanvasReference != null) 27 | { 28 | ReparentCanvas(CanvasReference); 29 | } 30 | else if (!string.IsNullOrEmpty(CanvasObjectName)) 31 | { 32 | Utils.SharedCoroutineStarter.instance.StartCoroutine(FindCanvasByString()); 33 | } 34 | else 35 | { 36 | Plugin.Logger.Warn($"Custom Counter ({Settings.AttachedCustomCounter.Name}) has no method of obtaining a Canvas to reparent."); 37 | } 38 | } 39 | 40 | /// 41 | /// Called before Counters+ attempts to reparent the defined . 42 | /// 43 | public virtual void PreReparent() { } 44 | 45 | /// 46 | /// Called after Counters+ has successfully reparented the defined . 47 | /// 48 | public virtual void PostReparent() { } 49 | 50 | private void ReparentCanvas(Canvas canvas) 51 | { 52 | Canvas currentCanvas = CanvasUtility.GetCanvasFromID(Settings.CanvasID); 53 | HUDCanvas currentSettings = CanvasUtility.GetCanvasSettingsFromCanvas(currentCanvas); 54 | float positionScale = currentSettings?.PositionScale ?? 10; 55 | Vector2 anchoredPos = CanvasUtility.GetAnchoredPositionFromConfig(Settings) * positionScale; 56 | 57 | canvas.transform.SetParent(currentCanvas.transform, true); 58 | 59 | RectTransform canvasRect = canvas.transform as RectTransform; 60 | canvasRect.anchoredPosition = anchoredPos; 61 | canvasRect.localPosition = new Vector3(canvasRect.localPosition.x, canvasRect.localPosition.y, 0); 62 | canvasRect.localEulerAngles = Vector3.zero; 63 | 64 | PostReparent(); 65 | } 66 | 67 | private IEnumerator FindCanvasByString() 68 | { 69 | int tries = 1; 70 | Canvas canvas = null; 71 | while (tries <= 10) 72 | { 73 | yield return new WaitForSeconds(tries * 0.1f); 74 | canvas = GameObject.Find(CanvasObjectName)?.GetComponent(); 75 | if (canvas != null) break; 76 | tries++; 77 | } 78 | if (canvas != null) ReparentCanvas(canvas); 79 | else Plugin.Logger.Warn($"Custom Counter ({Settings.AttachedCustomCounter.Name}) could not find its Canvas in 10 tries."); 80 | } 81 | 82 | public override void CounterDestroy() { } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Counters+/Counters/Event Broadcasters/CounterEventBroadcaster.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Counters.Interfaces; 2 | 3 | namespace CountersPlus.Counters.Event_Broadcasters 4 | { 5 | /// 6 | /// A simple that broadcasts the creation and destruction of counters. 7 | /// 8 | internal class CounterEventBroadcaster : EventBroadcaster 9 | { 10 | public override void Initialize() 11 | { 12 | foreach (ICounter counter in EventHandlers) 13 | { 14 | counter.CounterInit(); 15 | } 16 | } 17 | 18 | public override void Dispose() 19 | { 20 | foreach (ICounter counter in EventHandlers) 21 | { 22 | counter.CounterDestroy(); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Counters+/Counters/Event Broadcasters/EventBroadcaster.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Counters.Interfaces; 2 | using System.Collections.Generic; 3 | using System; 4 | using Zenject; 5 | 6 | namespace CountersPlus.Counters.Event_Broadcasters 7 | { 8 | /// 9 | /// An abstract class that broadcasts in-game events across Counters that inherit s. 10 | /// This was designed such that changes to these in-game events will only break the Event Broadcasters. 11 | /// 12 | /// Event handler that will receive these broadcasts. 13 | internal abstract class EventBroadcaster: IInitializable, IDisposable where T : IEventHandler 14 | { 15 | [Inject] protected List EventHandlers = new List(); 16 | 17 | public abstract void Initialize(); 18 | 19 | public abstract void Dispose(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Counters+/Counters/Event Broadcasters/NoteEventBroadcaster.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Counters.Interfaces; 2 | using Zenject; 3 | 4 | namespace CountersPlus.Counters.Event_Broadcasters 5 | { 6 | /// 7 | /// A that broadcasts events relating to note cutting and missing. 8 | /// 9 | internal class NoteEventBroadcaster : EventBroadcaster 10 | { 11 | [Inject] private BeatmapObjectManager beatmapObjectManager; 12 | 13 | public override void Initialize() 14 | { 15 | beatmapObjectManager.noteWasCutEvent += NoteWasCutEvent; 16 | beatmapObjectManager.noteWasMissedEvent += NoteWasMissedEvent; 17 | } 18 | 19 | private void NoteWasCutEvent(NoteController data, in NoteCutInfo noteCutInfo) 20 | { 21 | foreach (INoteEventHandler noteEventHandler in EventHandlers) 22 | { 23 | noteEventHandler?.OnNoteCut(data.noteData, noteCutInfo); 24 | } 25 | } 26 | 27 | private void NoteWasMissedEvent(NoteController data) 28 | { 29 | foreach (INoteEventHandler noteEventHandler in EventHandlers) 30 | { 31 | noteEventHandler?.OnNoteMiss(data.noteData); 32 | } 33 | } 34 | 35 | public override void Dispose() 36 | { 37 | beatmapObjectManager.noteWasCutEvent -= NoteWasCutEvent; 38 | beatmapObjectManager.noteWasMissedEvent -= NoteWasMissedEvent; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Counters+/Counters/Event Broadcasters/ScoreEventBroadcaster.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Counters.Interfaces; 2 | using Zenject; 3 | 4 | namespace CountersPlus.Counters.Event_Broadcasters 5 | { 6 | /// 7 | /// A that broadcasts score-related events. 8 | /// 9 | internal class ScoreEventBroadcaster : EventBroadcaster 10 | { 11 | [Inject] private ScoreController scoreController; 12 | 13 | public override void Initialize() 14 | { 15 | scoreController.scoreDidChangeEvent += ScoreDidChangeEvent; 16 | } 17 | 18 | private void ScoreDidChangeEvent(int rawScore, int modifiedScore) 19 | { 20 | foreach (IScoreEventHandler scoreEventHandler in EventHandlers) 21 | { 22 | scoreEventHandler?.ScoreUpdated(modifiedScore); 23 | scoreEventHandler?.MaxScoreUpdated(modifiedScore); 24 | } 25 | } 26 | 27 | public override void Dispose() 28 | { 29 | scoreController.scoreDidChangeEvent -= ScoreDidChangeEvent; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Counters+/Counters/FailCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using System; 3 | using System.Collections; 4 | using System.Linq; 5 | using TMPro; 6 | using UnityEngine; 7 | using Zenject; 8 | 9 | namespace CountersPlus.Counters 10 | { 11 | internal class FailCounter : Counter 12 | { 13 | private static long difficulty = 0; 14 | private static int restarts = 0; 15 | 16 | [Inject] private GameEnergyCounter energyCounter; 17 | [Inject] private PlayerDataModel playerData; 18 | [Inject] private BeatmapLevel beatmap; 19 | [Inject] private BeatmapKey beatmapKey; 20 | private int count = 0; 21 | private TMP_Text counter; 22 | 23 | public override void CounterInit() 24 | { 25 | long currentHash = LongExponent(beatmap.levelID.ToCharArray().Sum(x => (long)x), (int)beatmapKey.difficulty); 26 | if (Settings.ShowRestartsInstead) 27 | { 28 | if (difficulty == currentHash) 29 | { 30 | restarts++; 31 | count = restarts; 32 | } 33 | else 34 | { 35 | restarts = count = 0; 36 | difficulty = currentHash; 37 | } 38 | } 39 | else 40 | { 41 | count = playerData.playerData.playerAllOverallStatsData.allOverallStatsData.failedLevelsCount; 42 | energyCounter.gameEnergyDidReach0Event += SlowlyAnnoyThePlayerBecauseTheyFailed; 43 | } 44 | GenerateBasicText($"{(Settings.ShowRestartsInstead ? "Restarts" : "Fails")}", out counter); 45 | counter.text = count.ToString(); 46 | } 47 | 48 | public override void CounterDestroy() 49 | { 50 | energyCounter.gameEnergyDidReach0Event -= SlowlyAnnoyThePlayerBecauseTheyFailed; 51 | } 52 | 53 | private void SlowlyAnnoyThePlayerBecauseTheyFailed() 54 | { 55 | counter.text = (count + 1).ToString(); 56 | Utils.SharedCoroutineStarter.instance.StartCoroutine(ChangeTextColorToAnnoyThePlayerEvenMore()); 57 | } 58 | 59 | private IEnumerator ChangeTextColorToAnnoyThePlayerEvenMore() 60 | { 61 | float t = 0; 62 | while (t <= 1) 63 | { 64 | yield return new WaitForEndOfFrame(); 65 | t += Time.deltaTime; 66 | counter.color = Color.Lerp(Color.white, Color.red, t); 67 | } 68 | counter.color = Color.red; 69 | } 70 | 71 | private long LongExponent(long x, int pow) 72 | { 73 | string binary = Convert.ToString(pow, 2); 74 | 75 | int[] arr = new int[binary.Length]; 76 | int i = 0; 77 | foreach (var ch in binary) 78 | { 79 | arr[i++] = Convert.ToInt32(ch.ToString()); 80 | } 81 | 82 | // We use a nifty trick to calculate exponent in as little as 2 long2(n) multiplications. 83 | long res = x; 84 | for (int j = 1; j < arr.Length; j++) 85 | { 86 | switch (arr[j]) 87 | { 88 | case 0: 89 | res *= res; 90 | break; 91 | case 1: 92 | res *= res; 93 | res *= x; 94 | break; 95 | } 96 | } 97 | 98 | return res; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Counters+/Counters/Interfaces/ICounter.cs: -------------------------------------------------------------------------------- 1 | namespace CountersPlus.Counters.Interfaces 2 | { 3 | /// 4 | /// The base interface for a counter. 5 | /// This is barebones, and only contains events pertaining to the initialization and destruction of a counter. 6 | /// 7 | public interface ICounter : IEventHandler 8 | { 9 | void CounterInit(); 10 | 11 | void CounterDestroy(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Counters+/Counters/Interfaces/IEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace CountersPlus.Counters.Interfaces 2 | { 3 | /// 4 | /// Blank interface used for . 5 | /// 6 | public interface IEventHandler 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Counters+/Counters/Interfaces/INoteEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace CountersPlus.Counters.Interfaces 2 | { 3 | /// 4 | /// An interface that exposes certain note-related events. Used in conjunction with an . 5 | /// 6 | public interface INoteEventHandler : IEventHandler 7 | { 8 | void OnNoteCut(NoteData data, NoteCutInfo info); 9 | 10 | void OnNoteMiss(NoteData data); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Counters+/Counters/Interfaces/IScoreEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace CountersPlus.Counters.Interfaces 2 | { 3 | /// 4 | /// An interface that exposes certain scoring-related events. Used in conjunction with an . 5 | /// 6 | public interface IScoreEventHandler : IEventHandler 7 | { 8 | void ScoreUpdated(int modifiedScore); 9 | 10 | void MaxScoreUpdated(int maxModifiedScore); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Counters+/Counters/MissedCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Counters.Interfaces; 3 | using TMPro; 4 | 5 | namespace CountersPlus.Counters 6 | { 7 | internal class MissedCounter : Counter, INoteEventHandler 8 | { 9 | private int notesMissed = 0; 10 | private TMP_Text counter; 11 | 12 | public override void CounterInit() 13 | { 14 | GenerateBasicText("Misses", out counter); 15 | } 16 | 17 | public void OnNoteCut(NoteData data, NoteCutInfo info) 18 | { 19 | if (Settings.CountBadCuts && !info.allIsOK && data.colorType != ColorType.None) counter.text = (++notesMissed).ToString(); 20 | } 21 | 22 | public void OnNoteMiss(NoteData data) 23 | { 24 | if (data.colorType != ColorType.None && data.gameplayType != NoteData.GameplayType.BurstSliderElement) counter.text = (++notesMissed).ToString(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Counters+/Counters/MultiplayerRankCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using HMUI; 3 | using TMPro; 4 | using UnityEngine; 5 | using Zenject; 6 | 7 | namespace CountersPlus.Counters 8 | { 9 | internal class MultiplayerRankCounter : Counter 10 | { 11 | private readonly Vector3 offset = new Vector3(0, -0.25f, 0); 12 | 13 | [Inject] private MainConfigModel mainConfig; 14 | [Inject] private MultiplayerPositionHUDController multiplayerPositionHUDController; 15 | 16 | public override void CounterInit() 17 | { 18 | // Reposition/reparent rank counter to destination canvas 19 | var currentCanvas = CanvasUtility.GetCanvasFromID(Settings.CanvasID); 20 | var currentSettings = CanvasUtility.GetCanvasSettingsFromCanvas(currentCanvas); 21 | var positionScale = currentSettings?.PositionScale ?? 10; 22 | var anchoredPos = (CanvasUtility.GetAnchoredPositionFromConfig(Settings) + offset) * positionScale; 23 | 24 | multiplayerPositionHUDController.transform.SetParent(currentCanvas.transform, true); 25 | 26 | var rect = multiplayerPositionHUDController.transform as RectTransform; 27 | rect.anchoredPosition = anchoredPos; 28 | rect.localPosition = new Vector3(rect.localPosition.x, rect.localPosition.y, 0); 29 | rect.localEulerAngles = Vector3.zero; 30 | 31 | // Iterate existing text and set italic shit 32 | if (!mainConfig.ItalicText) 33 | { 34 | foreach (var childText in multiplayerPositionHUDController.GetComponentsInChildren(true)) 35 | { 36 | childText.fontStyle = FontStyles.Normal; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Counters+/Counters/Note Count Processors/CustomJSONDataNoteCountProcessor.cs: -------------------------------------------------------------------------------- 1 | /*using CustomJSONData; 2 | using CustomJSONData.CustomBeatmap; 3 | using System.Collections.Generic; 4 | 5 | namespace CountersPlus.Counters.NoteCountProcessors 6 | { 7 | /// 8 | /// This is a NoteCountProcessor that filters out notes that have certain CustomJSONData applied to them. 9 | /// 10 | public class CustomJSONDataNoteCountProcessor : NoteCountProcessor 11 | { 12 | // If there is any mods that implement CustomJSONData that would filter notes from the total note count, 13 | // PLEASE for the love of ALL THAT IS HOLY, add them to this list!!! 14 | // Key = CustomJSONData to search for, Value = value needed to ignore it 15 | private readonly Dictionary filteredNoteData = new Dictionary() 16 | { 17 | { "_fake", true }, // Noodle Extensions 18 | }; 19 | 20 | public override bool ShouldIgnoreNote(NoteData data) 21 | { 22 | if (data is CustomNoteData) 23 | { 24 | dynamic customObjectData = data; 25 | dynamic dynData = customObjectData.customData; 26 | foreach (var kvp in filteredNoteData) 27 | { 28 | bool? fake = Trees.at(dynData, kvp.Key); 29 | if (fake.HasValue && fake.Value == kvp.Value) 30 | { 31 | return true; 32 | } 33 | } 34 | } 35 | return false; 36 | } 37 | } 38 | }*/ 39 | -------------------------------------------------------------------------------- /Counters+/Counters/Note Count Processors/GenericNoteCountProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace CountersPlus.Counters.NoteCountProcessors 2 | { 3 | /// 4 | /// Generic processor for use in the vanilla game. 5 | /// 6 | public class GenericNoteCountProcessor : NoteCountProcessor 7 | { 8 | public override bool ShouldIgnoreNote(NoteData data) => false; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Counters+/Counters/Note Count Processors/NoteCountProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Zenject; 4 | 5 | namespace CountersPlus.Counters.NoteCountProcessors 6 | { 7 | /// 8 | /// Helper class to correctly obtain all notes in a song. 9 | /// This should handle the Vanilla game (all notes are valid), and modded (some notes are not valid). 10 | /// 11 | /// Perhaps in the future, these could be expanded upon to filter notes on miss/cut events as well. 12 | /// 13 | public abstract class NoteCountProcessor 14 | { 15 | public List Data => data ??= GetNoteData(beatmapData); 16 | 17 | public int NoteCount => Data.Count; 18 | 19 | private List data; 20 | 21 | 22 | [Inject] private IReadonlyBeatmapData beatmapData; 23 | 24 | protected List GetNoteData(IReadonlyBeatmapData data) 25 | { 26 | return data 27 | .GetBeatmapDataItems(0) 28 | .Where(noteData => noteData.gameplayType != NoteData.GameplayType.Bomb && !ShouldIgnoreNote(noteData)) 29 | .ToList(); 30 | } 31 | 32 | public abstract bool ShouldIgnoreNote(NoteData data); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Counters+/Counters/NotesCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Counters.Interfaces; 3 | using TMPro; 4 | 5 | namespace CountersPlus.Counters 6 | { 7 | internal class NotesCounter : Counter, INoteEventHandler 8 | { 9 | private int goodCuts = 0; 10 | private int allCuts = 0; 11 | private TMP_Text counter; 12 | 13 | public override void CounterInit() 14 | { 15 | GenerateBasicText("Notes", out counter); 16 | } 17 | 18 | public void OnNoteCut(NoteData data, NoteCutInfo info) 19 | { 20 | allCuts++; 21 | if (data.colorType != ColorType.None && info.allIsOK) goodCuts++; 22 | RefreshText(); 23 | } 24 | 25 | public void OnNoteMiss(NoteData data) 26 | { 27 | if (data.colorType == ColorType.None) return; 28 | allCuts++; 29 | RefreshText(); 30 | } 31 | 32 | private void RefreshText() 33 | { 34 | counter.text = $"{goodCuts} / {allCuts}"; 35 | if (Settings.ShowPercentage) 36 | { 37 | float percentage = (float)goodCuts / allCuts * 100.0f; 38 | counter.text += $" - {percentage.ToString($"F{Settings.DecimalPrecision}")}%"; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Counters+/Counters/NotesLeftCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Counters.Interfaces; 3 | using CountersPlus.Counters.NoteCountProcessors; 4 | using System.Linq; 5 | using TMPro; 6 | using Zenject; 7 | 8 | namespace CountersPlus.Counters 9 | { 10 | internal class NotesLeftCounter : Counter, INoteEventHandler 11 | { 12 | [Inject] private GameplayCoreSceneSetupData setupData; 13 | [Inject] private NoteCountProcessor noteCountProcessor; 14 | 15 | private int notesLeft = 0; 16 | private TMP_Text counter; 17 | 18 | public override void CounterInit() 19 | { 20 | if (setupData.practiceSettings != null && setupData.practiceSettings.startInAdvanceAndClearNotes) 21 | { 22 | float startTime = setupData.practiceSettings.startSongTime; 23 | // This LINQ statement is to ensure compatibility with Practice Mode / Practice Plugin 24 | notesLeft = noteCountProcessor.Data.Count(x => x.time > startTime); 25 | } 26 | else 27 | { 28 | notesLeft = noteCountProcessor.NoteCount; 29 | } 30 | 31 | if (Settings.LabelAboveCount) 32 | { 33 | GenerateBasicText("Notes Remaining", out counter); 34 | counter.text = notesLeft.ToString(); 35 | } 36 | else 37 | { 38 | counter = CanvasUtility.CreateTextFromSettings(Settings); 39 | counter.text = $"Notes Remaining: {notesLeft}"; 40 | counter.fontSize = 2; 41 | } 42 | } 43 | 44 | public void OnNoteCut(NoteData data, NoteCutInfo info) 45 | { 46 | if (ShouldProcessNote(data) && !noteCountProcessor.ShouldIgnoreNote(data)) DecrementCounter(); 47 | } 48 | 49 | public void OnNoteMiss(NoteData data) 50 | { 51 | if (ShouldProcessNote(data) && !noteCountProcessor.ShouldIgnoreNote(data)) DecrementCounter(); 52 | } 53 | 54 | private bool ShouldProcessNote(NoteData data) 55 | => data.gameplayType switch 56 | { 57 | NoteData.GameplayType.Normal => true, 58 | NoteData.GameplayType.BurstSliderHead => true, 59 | _ => false, 60 | }; 61 | 62 | private void DecrementCounter() 63 | { 64 | --notesLeft; 65 | if (Settings.LabelAboveCount) counter.text = notesLeft.ToString(); 66 | else counter.text = $"Notes Remaining: {notesLeft}"; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Counters+/Counters/PBCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Counters.Interfaces; 3 | using CountersPlus.Counters.NoteCountProcessors; 4 | using System.Linq; 5 | using TMPro; 6 | using UnityEngine; 7 | using Zenject; 8 | 9 | namespace CountersPlus.Counters 10 | { 11 | internal class PBCounter : Counter, IScoreEventHandler 12 | { 13 | private readonly Vector3 SCORE_COUNTER_OFFSET = new Vector3(0, -1.85f, 0); 14 | 15 | [Inject] private GameplayCoreSceneSetupData data; 16 | [Inject] private PlayerDataModel playerDataModel; 17 | [Inject] private ScoreConfigModel scoreConfig; 18 | [Inject] private RelativeScoreAndImmediateRankCounter relativeScoreAndImmediateRank; 19 | [Inject] private IReadonlyBeatmapData beatmapData; 20 | 21 | private TMP_Text counter; 22 | private PlayerLevelStatsData stats; 23 | 24 | private Color white = Color.white; // Caching it beforehand, since calling the constant makes a new struct 25 | 26 | private int maxPossibleScore = 0; 27 | private int highScore; 28 | private float pbRatio; 29 | 30 | public override void CounterInit() 31 | { 32 | BeatmapBasicData beatmap = data.beatmapBasicData; 33 | 34 | maxPossibleScore = ScoreModel.ComputeMaxMultipliedScoreForBeatmap(beatmapData); 35 | 36 | stats = playerDataModel.playerData.GetOrCreatePlayerLevelStatsData(data.beatmapKey); 37 | highScore = stats.highScore; 38 | 39 | if (scoreConfig.Enabled && Settings.UnderScore) 40 | { 41 | HUDCanvas scoreCanvas = CanvasUtility.GetCanvasSettingsFromID(scoreConfig.CanvasID); 42 | counter = CanvasUtility.CreateTextFromSettings(scoreConfig, SCORE_COUNTER_OFFSET * (3f / scoreCanvas.PositionScale)); 43 | } 44 | else 45 | { 46 | counter = CanvasUtility.CreateTextFromSettings(Settings); 47 | } 48 | counter.alignment = TextAlignmentOptions.Top; 49 | counter.fontSize = Settings.TextSize; 50 | 51 | pbRatio = (float)highScore / maxPossibleScore; 52 | 53 | SetPersonalBest(pbRatio); 54 | ScoreUpdated(0); 55 | } 56 | 57 | public void MaxScoreUpdated(int maxModifiedScore) { } 58 | 59 | public void ScoreUpdated(int modifiedScore) 60 | { 61 | if (maxPossibleScore != 0 && modifiedScore > highScore) 62 | { 63 | SetPersonalBest(modifiedScore / (float)maxPossibleScore); 64 | } 65 | 66 | counter.color = Settings.Mode switch 67 | { 68 | // Show default color when setting the first PB and setting is enabled 69 | _ when Settings.HideFirstScore && stats.highScore == 0 => Settings.DefaultColor, 70 | 71 | // Relative % is above PB % 72 | PBMode.Relative when relativeScoreAndImmediateRank.relativeScore >= pbRatio 73 | => Settings.BetterColor, 74 | 75 | // Relative % is approaching PB % 76 | PBMode.Relative when relativeScoreAndImmediateRank.relativeScore < pbRatio 77 | => Color.Lerp(white, Settings.DefaultColor, relativeScoreAndImmediateRank.relativeScore / pbRatio), 78 | 79 | // New high score, show PB color 80 | PBMode.Absolute when modifiedScore >= highScore 81 | => Settings.BetterColor, 82 | 83 | // Current score is approaching high school 84 | PBMode.Absolute when modifiedScore < highScore 85 | => Color.Lerp(white, Settings.DefaultColor, modifiedScore / (float)highScore), 86 | 87 | // The "C# stop yelling at me" case 88 | _ => Settings.DefaultColor, 89 | }; 90 | } 91 | 92 | private void SetPersonalBest(float pb) 93 | { 94 | if (Settings.HideFirstScore && stats.highScore == 0) counter.text = "PB: --"; 95 | else counter.text = $"PB: {(pb * 100).ToString($"F{Settings.DecimalPrecision}")}%"; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Counters+/Counters/ProgressBaseGameCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using UnityEngine; 3 | using Zenject; 4 | 5 | namespace CountersPlus.Counters 6 | { 7 | internal class ProgressBaseGameCounter : Counter // Yeah, yeah, I use a completely separate counter 8 | { 9 | private readonly Vector3 offset = new Vector3(0, -0.25f, 0); 10 | 11 | [Inject] private CoreGameHUDController coreGameHUDController; 12 | 13 | public override void CounterInit() 14 | { 15 | Canvas canvas = coreGameHUDController.GetComponentInChildren(true).GetComponent(); 16 | canvas.gameObject.SetActive(true); 17 | 18 | Canvas currentCanvas = CanvasUtility.GetCanvasFromID(Settings.CanvasID); 19 | HUDCanvas currentSettings = CanvasUtility.GetCanvasSettingsFromCanvas(currentCanvas); 20 | float positionScale = currentSettings?.PositionScale ?? 10; 21 | Vector2 anchoredPos = (CanvasUtility.GetAnchoredPositionFromConfig(Settings) + offset) * positionScale; 22 | 23 | canvas.transform.SetParent(currentCanvas.transform, true); 24 | 25 | RectTransform canvasRect = canvas.transform as RectTransform; 26 | canvasRect.anchoredPosition = anchoredPos; 27 | canvasRect.localPosition = new Vector3(canvasRect.localPosition.x, canvasRect.localPosition.y, 0); 28 | canvasRect.localEulerAngles = Vector3.zero; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Counters+/Counters/ProgressCounter.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage; 2 | using CountersPlus.ConfigModels; 3 | using HMUI; 4 | using System; 5 | using System.Linq; 6 | using TMPro; 7 | using UnityEngine; 8 | using UnityEngine.UI; 9 | using Zenject; 10 | using static CountersPlus.Utils.Accessors; 11 | 12 | namespace CountersPlus.Counters 13 | { 14 | internal class ProgressCounter : Counter, ITickable 15 | { 16 | private readonly Vector3 ringSize = Vector3.one * 1.175f; 17 | private readonly string multiplierImageSpriteName = "Circle"; 18 | 19 | [Inject] private AudioTimeSyncController atsc; 20 | [Inject] private CoreGameHUDController coreGameHUD; // For getting multiplier image 21 | [Inject] private GameplayCoreSceneSetupData gcssd; // I hope this works 22 | 23 | private TMP_Text timeText; 24 | private ImageView progressRing; 25 | private float length = 0; 26 | private float songBPM = 100; 27 | 28 | public override void CounterInit() 29 | { 30 | length = atsc.songLength; 31 | songBPM = gcssd.beatmapLevel.beatsPerMinute; 32 | 33 | timeText = CanvasUtility.CreateTextFromSettings(Settings); 34 | timeText.fontSize = 4; 35 | 36 | // Set default text, which is visible in Multiplayer 37 | // Missing cases are covered by the default "_" case 38 | timeText.text = Settings.Mode switch 39 | { 40 | ProgressMode.Percent when Settings.ProgressTimeLeft => "100%", 41 | ProgressMode.Percent when !Settings.ProgressTimeLeft => "0%", 42 | 43 | ProgressMode.TimeInBeats when Settings.ProgressTimeLeft 44 | => $"{Mathf.Round(songBPM / 60 * length / 0.25f) * 0.25f:F2}", 45 | ProgressMode.TimeInBeats when !Settings.ProgressTimeLeft => "0.00", 46 | 47 | _ when Settings.ProgressTimeLeft => $"{atsc.songLength:F2}", 48 | _ when !Settings.ProgressTimeLeft => "0:00" 49 | }; 50 | 51 | // I'm sorry, little one. 52 | GameObject baseGameProgress = SongProgressPanelGO(ref coreGameHUD); 53 | UnityEngine.Object.Destroy(baseGameProgress); 54 | 55 | if (Settings.Mode != ProgressMode.Percent) 56 | { 57 | var canvas = CanvasUtility.GetCanvasFromID(Settings.CanvasID); 58 | if (canvas != null) 59 | { 60 | ImageView backgroundImage = CreateRing(canvas); 61 | backgroundImage.rectTransform.anchoredPosition = timeText.rectTransform.anchoredPosition; 62 | backgroundImage.CrossFadeAlpha(0.05f, 1f, false); 63 | backgroundImage.transform.localScale = ringSize / 10; 64 | backgroundImage.type = Image.Type.Simple; 65 | 66 | progressRing = CreateRing(canvas); 67 | progressRing.rectTransform.anchoredPosition = timeText.rectTransform.anchoredPosition; 68 | progressRing.transform.localScale = ringSize / 10; 69 | 70 | // Start progress ring at 100% or 0%, depending on how the ring will behave 71 | progressRing.fillAmount = (Settings.ProgressTimeLeft && Settings.IncludeRing) ? 1 : 0; 72 | } 73 | } 74 | } 75 | 76 | public void Tick() 77 | { 78 | var time = atsc.songTime; 79 | if (Settings.ProgressTimeLeft) time = length - time; 80 | if (time <= 0f) return; 81 | 82 | switch (Settings.Mode) 83 | { 84 | case ProgressMode.TimeInBeats: 85 | float beats = Mathf.Round(songBPM / 60 * time / 0.25f) * 0.25f; 86 | timeText.text = beats.ToString("F2"); 87 | break; 88 | case ProgressMode.Original: 89 | timeText.text = $"{Math.Floor(time / 60):N0}:{Math.Floor(time % 60):00}"; 90 | break; 91 | default: 92 | timeText.text = $"{time / length * 100:00}%"; 93 | return; 94 | } 95 | 96 | progressRing.fillAmount = (Settings.IncludeRing ? time : atsc.songTime) / length; 97 | progressRing.SetVerticesDirty(); 98 | } 99 | 100 | private ImageView CreateRing(Canvas canvas) 101 | { 102 | // Unfortunately, there is no guarantee that I have the CoreGameHUDController, since No Text and Huds 103 | // completely disables it from spawning. So, to be safe, we recreate this all from scratch. 104 | GameObject imageGameObject = new GameObject("Ring Image", typeof(RectTransform)); 105 | imageGameObject.transform.SetParent(canvas.transform, false); 106 | ImageView newImage = imageGameObject.AddComponent(); 107 | newImage.enabled = false; 108 | newImage.material = Utilities.ImageResources.NoGlowMat; 109 | newImage.sprite = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == multiplierImageSpriteName); 110 | newImage.type = Image.Type.Filled; 111 | newImage.fillClockwise = true; 112 | newImage.fillOrigin = 2; 113 | newImage.fillAmount = 1; 114 | newImage.fillMethod = Image.FillMethod.Radial360; 115 | newImage.enabled = true; 116 | return newImage; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Counters+/Counters/ScoreCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using TMPro; 3 | using UnityEngine; 4 | using Zenject; 5 | using static CountersPlus.Utils.Accessors; 6 | 7 | namespace CountersPlus.Counters 8 | { 9 | // REVIEW: Perhaps look into harmony patches to take control of the base game score counter more... sanely? 10 | internal class ScoreCounter : Counter 11 | { 12 | private readonly Vector3 offset = new Vector3(0, 1.91666f, 0); 13 | 14 | [Inject] private CoreGameHUDController coreGameHUD; 15 | [Inject] private RelativeScoreAndImmediateRankCounter relativeScoreAndImmediateRank; 16 | [Inject] private MainConfigModel mainConfig; 17 | 18 | private RankModel.Rank prevImmediateRank = RankModel.Rank.SSS; 19 | private TextMeshProUGUI rankText; 20 | private TextMeshProUGUI relativeScoreText; 21 | 22 | public override void CounterInit() 23 | { 24 | // Yeah this is required. 25 | // If the Score Counter is all alone on its own Canvas with nothing to accompany them, 26 | // I need to give 'em a friend or else they get shy and hide away in the void. 27 | _ = CanvasUtility.CreateTextFromSettings(Settings, null); 28 | 29 | ScoreUIController scoreUIController = coreGameHUD.GetComponentInChildren(); 30 | TextMeshProUGUI old = ScoreUIText(ref scoreUIController); 31 | 32 | GameObject baseGameScore = RelativeScoreGO(ref coreGameHUD); 33 | baseGameScore.SetActive(true); 34 | relativeScoreText = baseGameScore.GetComponent(); 35 | relativeScoreText.enabled = true; 36 | relativeScoreText.color = Color.white; 37 | 38 | GameObject baseGameRank = ImmediateRankGO(ref coreGameHUD); 39 | baseGameRank.SetActive(true); 40 | rankText = baseGameRank.GetComponent(); 41 | rankText.enabled = true; 42 | rankText.color = Color.white; 43 | 44 | Canvas currentCanvas = CanvasUtility.GetCanvasFromID(Settings.CanvasID); 45 | 46 | old.rectTransform.SetParent(currentCanvas.transform, true); 47 | baseGameScore.transform.SetParent(old.transform, true); 48 | baseGameRank.transform.SetParent(old.transform, true); 49 | 50 | if (!mainConfig.ItalicText) 51 | { 52 | old.fontStyle = relativeScoreText.fontStyle = rankText.fontStyle = FontStyles.Normal; 53 | Vector3 localPosition = relativeScoreText.rectTransform.localPosition; 54 | relativeScoreText.rectTransform.localPosition = new Vector3(0, localPosition.y, localPosition.z); 55 | localPosition = rankText.rectTransform.localPosition; 56 | rankText.rectTransform.localPosition = new Vector3(0, localPosition.y, localPosition.z); 57 | } 58 | 59 | switch (Settings.Mode) 60 | { 61 | case ScoreMode.RankOnly: 62 | Object.Destroy(baseGameScore.gameObject); 63 | break; 64 | case ScoreMode.ScoreOnly: 65 | Object.Destroy(baseGameRank.gameObject); 66 | break; 67 | } 68 | 69 | RectTransform pointsTextTransform = old.rectTransform; 70 | 71 | HUDCanvas currentSettings = CanvasUtility.GetCanvasSettingsFromID(Settings.CanvasID); 72 | 73 | Vector2 anchoredPos = CanvasUtility.GetAnchoredPositionFromConfig(Settings) + (offset * (3f / currentSettings.PositionScale)); 74 | 75 | pointsTextTransform.localPosition = anchoredPos * currentSettings.PositionScale; 76 | pointsTextTransform.localPosition = new Vector3(pointsTextTransform.localPosition.x, pointsTextTransform.localPosition.y, 0); 77 | pointsTextTransform.localEulerAngles = Vector3.zero; 78 | 79 | Object.Destroy(coreGameHUD.GetComponentInChildren()); 80 | 81 | relativeScoreAndImmediateRank.relativeScoreOrImmediateRankDidChangeEvent += UpdateText; 82 | } 83 | 84 | private void UpdateText() 85 | { 86 | RankModel.Rank immediateRank = relativeScoreAndImmediateRank.immediateRank; 87 | if (immediateRank != prevImmediateRank) 88 | { 89 | rankText.text = RankModel.GetRankName(immediateRank); 90 | prevImmediateRank = immediateRank; 91 | 92 | rankText.color = Settings.CustomRankColors ? Settings.GetRankColorFromRank(immediateRank) : Color.white; 93 | } 94 | float relativeScore = relativeScoreAndImmediateRank.relativeScore * 100; 95 | relativeScoreText.text = $"{relativeScore.ToString($"F{Settings.DecimalPrecision}")}%"; 96 | } 97 | 98 | public override void CounterDestroy() 99 | { 100 | relativeScoreAndImmediateRank.relativeScoreOrImmediateRankDidChangeEvent -= UpdateText; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Counters+/Counters/SpeedCounter.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using TMPro; 6 | using UnityEngine; 7 | using Zenject; 8 | 9 | namespace CountersPlus.Counters 10 | { 11 | internal class SpeedCounter : Counter, ITickable 12 | { 13 | [Inject] private SaberManager saberManager; 14 | 15 | private Saber right; 16 | private Saber left; 17 | private List rSpeedList = new List(); 18 | private List lSpeedList = new List(); 19 | private List fastest = new List(); 20 | private TMP_Text averageCounter; 21 | private TMP_Text fastestCounter; 22 | 23 | private float t; 24 | 25 | public override void CounterInit() 26 | { 27 | right = saberManager.rightSaber; 28 | left = saberManager.leftSaber; 29 | 30 | switch (Settings.Mode) 31 | { 32 | case SpeedMode.Average: 33 | case SpeedMode.SplitAverage: 34 | GenerateBasicText("Average Speed", out averageCounter); 35 | break; 36 | case SpeedMode.Top5Sec: 37 | GenerateBasicText("Recent Top Speed", out fastestCounter); 38 | break; 39 | case SpeedMode.Both: 40 | case SpeedMode.SplitBoth: 41 | GenerateBasicText("Average Speed", out averageCounter); 42 | var label = CanvasUtility.CreateTextFromSettings(Settings, new Vector3(0, -1, 0)); 43 | label.fontSize = 3; 44 | label.text = "Recent Top Speed"; 45 | fastestCounter = CanvasUtility.CreateTextFromSettings(Settings, new Vector3(0, -1.4f, 0)); 46 | fastestCounter.text = "0"; 47 | break; 48 | } 49 | } 50 | 51 | public void Tick() 52 | { 53 | int precision = Settings.DecimalPrecision; 54 | // YES I AM USING GOTO TO LIMIT CODE DUPLICATION NOW STOP FLAMING ME 55 | switch (Settings.Mode) 56 | { 57 | case SpeedMode.Top5Sec: 58 | TickFastestSpeed(); 59 | break; 60 | 61 | case SpeedMode.Both: 62 | TickFastestSpeed(); 63 | goto case SpeedMode.Average; 64 | case SpeedMode.Average: 65 | rSpeedList.Add((right.bladeSpeed + left.bladeSpeed) / 2f); 66 | averageCounter.text = rSpeedList.Average().ToString($"F{precision}"); 67 | break; 68 | 69 | case SpeedMode.SplitBoth: 70 | TickFastestSpeed(); 71 | goto case SpeedMode.SplitAverage; 72 | case SpeedMode.SplitAverage: 73 | rSpeedList.Add(right.bladeSpeed); 74 | lSpeedList.Add(left.bladeSpeed); 75 | averageCounter.text = $"{lSpeedList.Average().ToString($"F{precision}")} | {rSpeedList.Average().ToString($"F{precision}")}"; 76 | break; 77 | } 78 | } 79 | 80 | // Ticked function instead of IEnumerator because its legit just better 81 | private void TickFastestSpeed() 82 | { 83 | fastest.Add((right.bladeSpeed + left.bladeSpeed) / 2f); 84 | t += Time.deltaTime; 85 | if (t >= 5) 86 | { 87 | t = 0; 88 | var top = fastest.Max(); 89 | fastest.Clear(); 90 | fastestCounter.text = top.ToString($"F{Settings.DecimalPrecision}"); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Counters+/Counters/Spinometer.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using TMPro; 6 | using UnityEngine; 7 | using Zenject; 8 | 9 | namespace CountersPlus.Counters 10 | { 11 | internal class Spinometer : Counter, ITickable 12 | { 13 | [Inject] private SaberManager saberManager; 14 | 15 | private Saber leftSaber = null; 16 | private Saber rightSaber = null; 17 | private List rightAngles = new List(); 18 | private List leftAngles = new List(); 19 | private List rightQuaternions = new List(); 20 | private List leftQuaternions = new List(); 21 | private float highestSpin; 22 | private TMP_Text spinometer; 23 | 24 | public override void CounterInit() 25 | { 26 | leftSaber = saberManager.leftSaber; 27 | rightSaber = saberManager.rightSaber; 28 | GenerateBasicText("Spinometer", out spinometer); 29 | Utils.SharedCoroutineStarter.instance.StartCoroutine(SecondTick()); 30 | } 31 | 32 | public void Tick() 33 | { 34 | if (leftSaber != saberManager.leftSaber) 35 | { 36 | leftSaber = saberManager.leftSaber; 37 | } 38 | if (rightSaber != saberManager.rightSaber) 39 | { 40 | rightSaber = saberManager.rightSaber; 41 | } 42 | leftQuaternions.Add(leftSaber.transform.rotation); 43 | rightQuaternions.Add(rightSaber.transform.rotation); 44 | if (leftQuaternions.Count >= 2 && rightQuaternions.Count >= 2) 45 | { 46 | leftAngles.Add(Quaternion.Angle(leftQuaternions.Last(), leftQuaternions[leftQuaternions.Count - 2])); 47 | rightAngles.Add(Quaternion.Angle(rightQuaternions.Last(), rightQuaternions[rightQuaternions.Count - 2])); 48 | } 49 | } 50 | 51 | private IEnumerator SecondTick() 52 | { 53 | while (true) 54 | { 55 | yield return new WaitForSecondsRealtime(1); 56 | leftQuaternions.Clear(); 57 | rightQuaternions.Clear(); 58 | float leftSpeed = leftAngles.Sum(); 59 | float rightSpeed = rightAngles.Sum(); 60 | leftAngles.Clear(); 61 | rightAngles.Clear(); 62 | float averageSpeed = (leftSpeed + rightSpeed) / 2; 63 | if (leftSpeed > highestSpin) highestSpin = leftSpeed; 64 | if (rightSpeed > highestSpin) highestSpin = rightSpeed; 65 | 66 | switch (Settings.Mode) 67 | { 68 | case SpinometerMode.Average: 69 | spinometer.text = $"{Mathf.RoundToInt(averageSpeed)}"; 70 | break; 71 | case SpinometerMode.Highest: 72 | spinometer.text = $"{Mathf.RoundToInt(highestSpin)}"; 73 | break; 74 | case SpinometerMode.SplitAverage: 75 | spinometer.text = $"{Mathf.RoundToInt(leftSpeed)} | {Mathf.RoundToInt(rightSpeed)}"; 76 | break; 77 | } 78 | } 79 | } 80 | 81 | private string DetermineColor(float speed) 82 | { 83 | ColorUtility.TryParseHtmlString("#FFA500", out Color orange); 84 | Color color = Color.Lerp(Color.white, orange, speed / 3600); 85 | if (speed >= 3600) color = Color.red; 86 | return ColorUtility.ToHtmlStringRGB(color); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Counters+/Custom/CustomConfigModel.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using IPA.Config.Stores.Attributes; 3 | using Newtonsoft.Json; 4 | 5 | namespace CountersPlus.Custom 6 | { 7 | public class CustomConfigModel : ConfigModel 8 | { 9 | [JsonProperty(nameof(Enabled), Required = Required.DisallowNull)] 10 | public override bool Enabled { get; set; } = true; 11 | [UseConverter] 12 | [JsonProperty(nameof(Position), Required = Required.DisallowNull)] 13 | public override CounterPositions Position { get; set; } = CounterPositions.BelowCombo; 14 | [JsonProperty(nameof(Distance), Required = Required.DisallowNull)] 15 | public override int Distance { get; set; } = 0; 16 | 17 | [Ignore] 18 | internal CustomCounter AttachedCustomCounter; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Counters+/Custom/CustomCounter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace CountersPlus.Custom 5 | { 6 | // Not to be confused with CountersPlus.Counters.CustomCounter 7 | internal class CustomCounter 8 | { 9 | [JsonProperty(Required = Required.Always)] 10 | public string Name; 11 | 12 | [JsonProperty(Required = Required.DisallowNull)] 13 | public string Description; 14 | 15 | [JsonProperty] 16 | public bool MultiplayerReady = false; 17 | 18 | [JsonProperty(Required = Required.DisallowNull)] 19 | public BSMLSettings BSML = null; 20 | 21 | [JsonProperty(Required = Required.Always)] 22 | internal string CounterLocation; 23 | 24 | [JsonProperty(Required = Required.DisallowNull)] 25 | internal CustomConfigModel ConfigDefaults = new CustomConfigModel(); 26 | 27 | public CustomConfigModel Config; 28 | 29 | public Type CounterType; 30 | 31 | public class BSMLSettings 32 | { 33 | [JsonProperty(nameof(Resource), Required = Required.AllowNull)] 34 | public string Resource; 35 | [JsonProperty(nameof(Host), Required = Required.AllowNull)] 36 | internal string Host; 37 | [JsonProperty(nameof(Icon), Required = Required.DisallowNull)] 38 | public string Icon; 39 | 40 | public Type HostType; 41 | 42 | public bool HasType => !string.IsNullOrEmpty(Host); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Counters+/Custom/CustomCounterFeature.cs: -------------------------------------------------------------------------------- 1 | using IPA.Loader; 2 | using IPA.Loader.Features; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | namespace CountersPlus.Custom 9 | { 10 | public class CustomCounterFeature : Feature 11 | { 12 | private Dictionary incompleteCustomCounters = new Dictionary(0); 13 | 14 | protected override bool Initialize(PluginMetadata meta, JObject featureData) 15 | { 16 | CustomCounter counter; 17 | try 18 | { 19 | counter = featureData.ToObject(); 20 | } 21 | catch (Exception e) 22 | { 23 | InvalidMessage = $"Invalid data: {e}"; 24 | return false; 25 | } 26 | 27 | incompleteCustomCounters.Add(meta, counter); 28 | return true; 29 | } 30 | 31 | public override void AfterInit(PluginMetadata meta) 32 | { 33 | if (incompleteCustomCounters.TryGetValue(meta, out CustomCounter counter)) 34 | { 35 | if (!TryLoadType(ref counter.CounterType, meta, counter.CounterLocation)) 36 | { 37 | Plugin.Logger.Error($"Failed to load a Type from the provided CounterLocation for {counter.Name}."); 38 | return; 39 | } 40 | if (counter.BSML != null && counter.BSML.HasType && !TryLoadType(ref counter.BSML.HostType, meta, counter.BSML.Host)) 41 | { 42 | Plugin.Logger.Error($"Failed to load a Type from the provided BSML Host for {counter.Name}."); 43 | return; 44 | } 45 | 46 | Plugin.LoadedCustomCounters.Add(counter); 47 | Plugin.Logger.Notice($"Loaded a Custom Counter ({counter.Name})."); 48 | } 49 | else 50 | { 51 | Plugin.Logger.Critical(@"A plugin has a defined Custom Counter, but Initialise was somehow not called. 52 | How the hell did we even get here?"); 53 | } 54 | } 55 | 56 | private bool TryLoadType(ref Type typeToLoad, PluginMetadata meta, string location) 57 | { 58 | // totally didn't yoink this from BSIPA's ConfigProviderFeature 59 | try 60 | { 61 | typeToLoad = meta.Assembly.GetType(location); 62 | } 63 | catch (ArgumentException) 64 | { 65 | InvalidMessage = $"Invalid type name {location}"; 66 | return false; 67 | } 68 | catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) 69 | { 70 | string filename; 71 | 72 | switch (e) 73 | { 74 | case FileNotFoundException fn: 75 | filename = fn.FileName; 76 | goto hasFilename; 77 | case FileLoadException fl: 78 | filename = fl.FileName; 79 | goto hasFilename; 80 | case BadImageFormatException bi: 81 | filename = bi.FileName; 82 | hasFilename: 83 | InvalidMessage = $"Could not find {filename} while loading type"; 84 | break; 85 | default: 86 | InvalidMessage = $"Error while loading type: {e}"; 87 | break; 88 | } 89 | 90 | return false; 91 | } 92 | catch (Exception e) // Is this unnecessary? Maybe. 93 | { 94 | InvalidMessage = $"An unknown error occured: {e}"; 95 | return false; 96 | } 97 | 98 | return true; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /Counters+/Harmony/CoreGameHUDControllerPatch.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using HarmonyLib; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Reflection.Emit; 8 | using UnityEngine; 9 | using HarmonyObj = HarmonyLib.Harmony; 10 | 11 | namespace CountersPlus.Harmony 12 | { 13 | /* 14 | * This patch ensures that only the HUD elements we want are spawned in, then the child objects 15 | * of CoreGameHUDController are hidden. 16 | */ 17 | [HarmonyPatch(typeof(CoreGameHUDController))] 18 | [HarmonyPatch("Initialize")] 19 | internal class CoreGameHUDControllerPatch 20 | { 21 | private static MethodInfo ProgressMethod = SymbolExtensions.GetMethodInfo(() => ShouldEnableProgressPanel(false)); 22 | private static MethodInfo ScoreMethod = SymbolExtensions.GetMethodInfo(() => ShouldEnableScorePanel(false)); 23 | private static MethodInfo SkipMethod = SymbolExtensions.GetMethodInfo(() => ShouldSkip(false)); 24 | 25 | private static bool isOverriding = false; 26 | 27 | private static IEnumerable Transpiler(IEnumerable original) 28 | { 29 | List codes = original.ToList(); 30 | int callvirtsFound = 0; 31 | bool insertedCheck = false; 32 | for (int i = 0; i < codes.Count; i++) 33 | { 34 | if (codes[i].opcode == OpCodes.Callvirt) 35 | { 36 | callvirtsFound++; 37 | switch (callvirtsFound) 38 | { 39 | case 1: 40 | if (!insertedCheck) 41 | { 42 | insertedCheck = true; 43 | codes.InsertRange(i - 4, new List() 44 | { 45 | new CodeInstruction(OpCodes.Dup), 46 | new CodeInstruction(OpCodes.Call, SkipMethod), 47 | new CodeInstruction(OpCodes.Not), 48 | new CodeInstruction(OpCodes.And) 49 | }); 50 | } 51 | i += 3; // Push it forward the same length as the amount of code we entered. 52 | break; 53 | case 2: 54 | codes.Insert(i, new CodeInstruction(OpCodes.Call, ProgressMethod)); 55 | break; 56 | case 3: 57 | case 4: 58 | codes.Insert(i, new CodeInstruction(OpCodes.Call, ScoreMethod)); 59 | break; 60 | } 61 | i++; // Increment i again so we do not enter an infinite loop. 62 | } 63 | } 64 | return codes; 65 | } 66 | 67 | public static bool ShouldSkip(bool original) 68 | { 69 | HUDConfigModel hudConfig = Plugin.MainConfig.HUDConfig; 70 | ProgressConfigModel progress = Plugin.MainConfig.ProgressConfig; 71 | ScoreConfigModel score = Plugin.MainConfig.ScoreConfig; 72 | isOverriding = original && ( 73 | (progress.Enabled && progress.Mode == ProgressMode.BaseGame && CheckIgnoreOption(hudConfig, progress)) || 74 | (score.Enabled && CheckIgnoreOption(hudConfig, score))); 75 | return isOverriding; 76 | } 77 | 78 | private static bool CheckIgnoreOption(HUDConfigModel hud, ConfigModel model) 79 | { 80 | if (model.CanvasID == -1) return hud.MainCanvasSettings.IgnoreNoTextAndHUDOption; 81 | return hud.OtherCanvasSettings[model.CanvasID].IgnoreNoTextAndHUDOption; 82 | } 83 | 84 | private static bool ShouldEnableProgressPanel(bool original) 85 | { 86 | if (!isOverriding) return original; 87 | ProgressConfigModel progressConfig = Plugin.MainConfig.ProgressConfig; 88 | return original || (progressConfig.Enabled && progressConfig.Mode == ProgressMode.BaseGame); 89 | } 90 | 91 | private static bool ShouldEnableScorePanel(bool original) 92 | { 93 | if (!isOverriding) return original; 94 | ScoreConfigModel scoreConfig = Plugin.MainConfig.ScoreConfig; 95 | return original || scoreConfig.Enabled; 96 | } 97 | 98 | private static void Postfix(CoreGameHUDController __instance) 99 | { 100 | if (isOverriding) 101 | Utils.SharedCoroutineStarter.instance.StartCoroutine(RemoveAfterOneFrame(__instance)); 102 | } 103 | 104 | private static IEnumerator RemoveAfterOneFrame(CoreGameHUDController coreGameHUD) 105 | { 106 | yield return new WaitForEndOfFrame(); 107 | foreach (Transform child in coreGameHUD.transform) 108 | { 109 | child.gameObject.SetActive(false); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Counters+/Installers/CoreInstaller.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.ConfigModels.SettableSettings; 3 | using CountersPlus.Custom; 4 | using CountersPlus.Utils; 5 | using IPA.Loader; 6 | using Zenject; 7 | 8 | namespace CountersPlus.Installers 9 | { 10 | public class CoreInstaller : Installer 11 | { 12 | public override void InstallBindings() 13 | { 14 | Container.Bind().AsSingle().NonLazy(); 15 | 16 | MainConfigModel mainConfig = Plugin.MainConfig; 17 | 18 | Container.Bind().FromInstance(mainConfig); 19 | mainConfig.HUDConfig.MainCanvasSettings.IsMainCanvas = true; 20 | Container.Bind().FromInstance(mainConfig.HUDConfig); 21 | 22 | BindConfig(mainConfig.MissedConfig); 23 | BindConfig(mainConfig.NoteConfig); 24 | BindConfig(mainConfig.ProgressConfig); 25 | BindConfig(mainConfig.ScoreConfig); 26 | BindConfig(mainConfig.SpeedConfig); 27 | BindConfig(mainConfig.SpinometerConfig); 28 | BindConfig(mainConfig.PBConfig); 29 | BindConfig(mainConfig.CutConfig); 30 | BindConfig(mainConfig.FailsConfig); 31 | BindConfig(mainConfig.NotesLeftConfig); 32 | BindConfig(mainConfig.MultiplayerRankConfig); 33 | 34 | foreach (CustomCounter customCounter in Plugin.LoadedCustomCounters) 35 | { 36 | if (!mainConfig.CustomCounters.TryGetValue(customCounter.Name, out CustomConfigModel config)) 37 | { 38 | config = customCounter.ConfigDefaults; 39 | mainConfig.CustomCounters.Add(customCounter.Name, config); 40 | } 41 | 42 | config.DisplayName = customCounter.Name; 43 | config.AttachedCustomCounter = customCounter; 44 | customCounter.Config = config; 45 | BindCustomCounter(customCounter, config); 46 | } 47 | 48 | if (PluginManager.GetPlugin("Heck") != null) 49 | { 50 | InstallHeckCompatibility(); 51 | } 52 | } 53 | 54 | // Helper function, allows easy modification to how configs are binded to zenject 55 | private void BindConfig(T settings) where T : ConfigModel 56 | { 57 | Container.BindInterfacesAndSelfTo().FromInstance(settings).AsCached(); 58 | Container.Bind().To().FromInstance(settings).AsCached(); 59 | } 60 | 61 | // Is this too much? Probably. 62 | private void BindCustomCounter(CustomCounter counter, CustomConfigModel settings) 63 | { 64 | Container.Bind().WithId(counter.Name).To().FromInstance(settings).AsCached(); 65 | Container.Bind().To().FromInstance(settings).AsCached(); 66 | Container.BindInterfacesAndSelfTo().FromInstance(settings).WhenInjectedInto(counter.CounterType); 67 | } 68 | 69 | private void InstallHeckCompatibility() 70 | { 71 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Counters+/Installers/CountersInstaller.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Counters; 3 | using CountersPlus.Counters.Event_Broadcasters; 4 | using CountersPlus.Counters.Interfaces; 5 | using CountersPlus.Counters.NoteCountProcessors; 6 | using CountersPlus.Custom; 7 | using CountersPlus.Utils; 8 | using System; 9 | using UnityEngine; 10 | using Zenject; 11 | 12 | namespace CountersPlus.Installers 13 | { 14 | class CountersInstaller : MonoInstaller 15 | { 16 | [Inject] 17 | private readonly HUDConfigModel hudConfig; 18 | 19 | [Inject] 20 | private readonly PlayerDataModel dataModel; 21 | 22 | public override void InstallBindings() 23 | { 24 | MainConfigModel mainConfig = Plugin.MainConfig; 25 | 26 | if (!mainConfig.Enabled) return; 27 | 28 | /// LOADING IMPORTANT SHIT LIKE CANVASES AND STUFF /// 29 | Container.Bind().AsSingle(); 30 | 31 | Container.Bind().To().AsSingle(); 32 | 33 | /// LOADING COUNTERS /// 34 | Plugin.Logger.Notice("Loading counters..."); 35 | 36 | AddCounter(); 37 | AddCounter(); 38 | 39 | if (mainConfig.ProgressConfig.Mode != ProgressMode.BaseGame) 40 | { 41 | AddCounter(); 42 | } 43 | else 44 | { 45 | AddCounter(); 46 | } 47 | 48 | AddCounter(); 49 | AddCounter(); 50 | AddCounter(); 51 | AddCounter(); 52 | AddCounter(); 53 | AddCounter(); 54 | 55 | AddCounter((settings) => { 56 | ScoreConfigModel scoreConfig = Container.Resolve(); 57 | HUDCanvas canvasSettings = GrabCanvasForCounter(scoreConfig); 58 | return scoreConfig.Enabled && settings.UnderScore && (dataModel.playerData.playerSpecificSettings.noTextsAndHuds ? canvasSettings.IgnoreNoTextAndHUDOption : true); 59 | }); 60 | 61 | InstallCustomCounters(); 62 | 63 | if (mainConfig.AprilFoolsTomfoolery && mainConfig.IsAprilFools) 64 | { 65 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 66 | } 67 | 68 | /// LOADING BROADCASTERS WITH BROADCAST IN-GAME EVENTS TO COUNTERS AND STUFF /// 69 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 70 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 71 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 72 | } 73 | 74 | protected virtual void InstallCustomCounters() 75 | { 76 | foreach (CustomCounter customCounter in Plugin.LoadedCustomCounters) 77 | { 78 | AddCustomCounter(customCounter, customCounter.CounterType); 79 | } 80 | } 81 | 82 | protected void AddCounter() where T : ConfigModel where R : ICounter 83 | { 84 | AddCounter(_ => false); 85 | } 86 | 87 | protected void AddCounter(Func additionalReasonToSpawn) where T : ConfigModel where R : ICounter 88 | { 89 | T settings = Container.Resolve(); 90 | 91 | HUDCanvas canvasSettings = GrabCanvasForCounter(settings); 92 | 93 | if (!settings.Enabled || (!canvasSettings.IgnoreNoTextAndHUDOption && dataModel.playerData.playerSpecificSettings.noTextsAndHuds 94 | && !additionalReasonToSpawn(settings))) return; 95 | 96 | Plugin.Logger.Debug($"Loading counter {settings.DisplayName}..."); 97 | 98 | if (typeof(MonoBehaviour).IsAssignableFrom(typeof(R))) 99 | { 100 | Container.BindInterfacesAndSelfTo().FromNewComponentOnRoot().AsSingle().NonLazy(); 101 | } 102 | else 103 | { 104 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 105 | } 106 | } 107 | 108 | protected void AddCustomCounter(CustomCounter customCounter, Type counterType) 109 | { 110 | ConfigModel settings; 111 | 112 | if ((settings = Container.TryResolveId(customCounter.Name)) != null) 113 | { 114 | HUDCanvas canvasSettings = GrabCanvasForCounter(settings); 115 | 116 | if (!settings.Enabled || (!canvasSettings.IgnoreNoTextAndHUDOption && dataModel.playerData.playerSpecificSettings.noTextsAndHuds)) return; 117 | 118 | Plugin.Logger.Debug($"Loading counter {customCounter.Name}..."); 119 | 120 | if (typeof(MonoBehaviour).IsAssignableFrom(counterType)) 121 | { 122 | Container.BindInterfacesAndSelfTo(counterType).FromNewComponentOnRoot().AsSingle().NonLazy(); 123 | } 124 | else 125 | { 126 | Container.BindInterfacesAndSelfTo(counterType).AsSingle().NonLazy(); 127 | } 128 | } 129 | } 130 | 131 | protected HUDCanvas GrabCanvasForCounter(ConfigModel settings) 132 | => settings.CanvasID == -1 || settings.CanvasID >= hudConfig.OtherCanvasSettings.Count 133 | ? hudConfig.MainCanvasSettings 134 | : hudConfig.OtherCanvasSettings[settings.CanvasID]; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Counters+/Installers/MenuUIInstaller.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Custom; 2 | using CountersPlus.UI; 3 | using CountersPlus.UI.FlowCoordinators; 4 | using CountersPlus.UI.SettingGroups; 5 | using CountersPlus.UI.ViewControllers; 6 | using CountersPlus.UI.ViewControllers.Editing; 7 | using CountersPlus.Utils; 8 | using HMUI; 9 | using System; 10 | using UnityEngine; 11 | using Zenject; 12 | 13 | namespace CountersPlus.Installers 14 | { 15 | public class MenuUIInstaller : Installer 16 | { 17 | // Using Zenject for UI lets goooooooooooo 18 | public override void InstallBindings() 19 | { 20 | // CanvasUtility for UI 21 | Container.Bind().AsSingle(); 22 | Container.Bind().AsSingle(); 23 | 24 | BindSettingsGroup(); 25 | BindSettingsGroup(); 26 | BindSettingsGroup(); 27 | 28 | foreach (CustomCounter customCounter in Plugin.LoadedCustomCounters) 29 | { 30 | if (customCounter.BSML != null && customCounter.BSML.HasType) 31 | { 32 | Type hostType = customCounter.BSML.HostType; 33 | if (typeof(MonoBehaviour).IsAssignableFrom(hostType)) 34 | { 35 | Container.Bind(hostType).WithId(customCounter.Name).FromNewComponentOnNewGameObject().AsCached(); 36 | } 37 | else 38 | { 39 | Container.Bind(hostType).WithId(customCounter.Name).AsCached(); 40 | } 41 | } 42 | } 43 | 44 | BindViewController(); 45 | BindViewController(); 46 | BindViewController(); 47 | BindViewController(); 48 | BindViewController(); 49 | BindViewController(); 50 | BindViewController(); 51 | BindViewController(); 52 | 53 | Container.Bind().FromNewComponentOnNewGameObject().AsSingle(); 54 | Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); 55 | } 56 | 57 | private void BindViewController() where T : ViewController 58 | { 59 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 60 | } 61 | 62 | private void BindSettingsGroup() where T : SettingsGroup 63 | { 64 | Container.Bind().To().AsCached().NonLazy(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Counters+/Installers/MultiplayerCountersInstaller.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Counters; 3 | using CountersPlus.Custom; 4 | using CountersPlus.Multiplayer; 5 | using UnityEngine; 6 | 7 | namespace CountersPlus.Installers 8 | { 9 | internal class MultiplayerCountersInstaller : CountersInstaller 10 | { 11 | public override void InstallBindings() 12 | { 13 | // haha i made goobie beg me for this one line of code 14 | if (!Plugin.MainConfig.Enabled) return; 15 | 16 | // If CoreGameHUD isn't bound already (by MultEx or others), we need to bind it ourselves 17 | var coreGameHUD = Container.TryResolve(); 18 | if (coreGameHUD == null) 19 | { 20 | // Thankfully the CoreGameHUD is just a child of the installer GameObject 21 | coreGameHUD = GetComponentInChildren(); 22 | Container.Bind().FromInstance(coreGameHUD).AsSingle(); 23 | 24 | // Change base game HUD elements to standard position 25 | var energyUIPanel = coreGameHUD.GetComponentInChildren(); 26 | energyUIPanel.transform.position = new Vector3(0, -0.64f, 7.75f); 27 | energyUIPanel.transform.rotation = Quaternion.Euler(0, 0, 0); 28 | 29 | var comboUI = coreGameHUD.GetComponentInChildren(); 30 | comboUI.transform.position = new Vector3(-3.2f, 1.83f, 7f); 31 | comboUI.transform.rotation = Quaternion.Euler(0, 0, 0); 32 | 33 | var multiplierUI = coreGameHUD.GetComponentInChildren(); 34 | multiplierUI.transform.position = new Vector3(3.2f, 1.7f, 7f); 35 | multiplierUI.transform.rotation = Quaternion.Euler(0, 0, 0); 36 | } 37 | 38 | // For the multiplayer rank counter, we also need to bind the Multiplayer Position 39 | Container.Bind() 40 | .FromInstance(GetComponentInChildren()) 41 | .AsSingle(); 42 | 43 | Container.BindInterfacesAndSelfTo().AsSingle(); 44 | 45 | // Install counters like normal 46 | base.InstallBindings(); 47 | 48 | // Then add our multiplayer rank counter 49 | AddCounter(); 50 | } 51 | 52 | protected override void InstallCustomCounters() 53 | { 54 | // Only load multiplayer-ready custom counters 55 | foreach (CustomCounter customCounter in Plugin.LoadedCustomCounters.FindAll(x => x.MultiplayerReady)) 56 | { 57 | AddCustomCounter(customCounter, customCounter.CounterType); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Counters+/Multiplayer/CanvasIntroFadeController.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.ConfigModels; 2 | using CountersPlus.Utils; 3 | using System; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | using Zenject; 7 | 8 | namespace CountersPlus.Multiplayer 9 | { 10 | internal class CanvasIntroFadeController : IInitializable, ITickable, IDisposable 11 | { 12 | [Inject] private CoreGameHUDController coreGameHUDController; 13 | [Inject] private HUDConfigModel hudConfig; 14 | [Inject] private CanvasUtility canvasUtility; 15 | 16 | [Inject] private MultiplayerController multiplayerController; 17 | 18 | private CanvasGroup coreGameHUDCanvasGroup; 19 | private List countersPlusCanvasGroups = new List(); 20 | 21 | private bool tick = false; 22 | 23 | public void Initialize() 24 | { 25 | coreGameHUDCanvasGroup = coreGameHUDController.GetComponent(); 26 | 27 | for (int i = -1; i < hudConfig.OtherCanvasSettings.Count; i++) 28 | { 29 | var canvas = canvasUtility.GetCanvasFromID(i).gameObject; 30 | 31 | var canvasGroup = canvas.AddComponent(); 32 | canvasGroup.alpha = 0; 33 | 34 | countersPlusCanvasGroups.Add(canvasGroup); 35 | } 36 | 37 | multiplayerController.stateChangedEvent += MultiplayerController_stateChangedEvent; 38 | } 39 | 40 | // Short circuit which stops Tick() from executing when in gameplay 41 | private void MultiplayerController_stateChangedEvent(MultiplayerController.State state) 42 | => tick = state != MultiplayerController.State.Gameplay; 43 | 44 | public void Tick() 45 | { 46 | if (!tick) return; 47 | 48 | var alpha = coreGameHUDCanvasGroup.alpha; 49 | 50 | countersPlusCanvasGroups.ForEach(canvas => canvas.alpha = alpha); 51 | } 52 | 53 | public void Dispose() 54 | => multiplayerController.stateChangedEvent -= MultiplayerController_stateChangedEvent; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Counters+/Plugin.cs: -------------------------------------------------------------------------------- 1 | using CountersPlus.Custom; 2 | using CountersPlus.ConfigModels; 3 | using CountersPlus.Installers; 4 | using IPA; 5 | using IPA.Config; 6 | using IPA.Config.Stores; 7 | using SiraUtil.Zenject; 8 | using System.Collections.Generic; 9 | using System.Reflection; 10 | using IPALogger = IPA.Logging.Logger; 11 | using HarmonyObj = HarmonyLib.Harmony; 12 | 13 | namespace CountersPlus 14 | { 15 | [Plugin(RuntimeOptions.DynamicInit)] 16 | public class Plugin 17 | { 18 | internal static Plugin Instance { get; private set; } 19 | internal static IPALogger Logger { get; private set; } 20 | internal static MainConfigModel MainConfig { get; private set; } 21 | internal static List LoadedCustomCounters { get; private set; } = new List(); 22 | 23 | private const string HARMONY_ID = "com.caeden117.countersplus"; 24 | private HarmonyObj harmony; 25 | 26 | [Init] 27 | public Plugin(IPALogger logger, 28 | [Config.Name("CountersPlus")] Config conf, 29 | Zenjector zenjector) 30 | { 31 | Instance = this; 32 | Logger = logger; 33 | MainConfig = conf.Generated(); 34 | harmony = new HarmonyObj(HARMONY_ID); 35 | 36 | zenjector.Expose("Environment"); 37 | 38 | zenjector.Install(Location.App); 39 | zenjector.Install(Location.Menu); 40 | 41 | zenjector.Install(Location.StandardPlayer | Location.CampaignPlayer); 42 | zenjector.Install(); 43 | } 44 | 45 | [OnEnable] 46 | public void OnEnable() => harmony.PatchAll(Assembly.GetExecutingAssembly()); 47 | 48 | [OnDisable] 49 | public void OnDisable() => harmony.UnpatchSelf(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Counters+/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("Counters+")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Counters+")] 13 | [assembly: AssemblyCopyright("Copyright © Caeden117 2018-2024")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | [assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedStore.AssemblyVisibilityTarget)] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("6240d1b1-390e-4808-adf5-2f1c9e18317f")] 25 | 26 | // Version information for an assembly consists of the following four values: 27 | // 28 | // Major Version 29 | // Minor Version 30 | // Build Number 31 | // Revision 32 | // 33 | // You can specify all the values or you can default the Build and Revision Numbers 34 | // by using the '*' as shown below: 35 | // [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: AssemblyVersion("2.3.11")] 37 | [assembly: AssemblyFileVersion("2.3.11")] 38 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/BlankScreen.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Cut.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Error.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Fail.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Missed.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Multiplayer Rank.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Notes Left.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Notes.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Personal Best.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Progress.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Score.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Speed.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Config/Spinometer.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Counters+/UI/BSML/Credits.bsml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |