├── .gitignore ├── Images ├── ss.jpg └── icon.png ├── Counter ├── BSML │ ├── SettingsHandler.cs │ └── Settings.bsml └── OverswingCounter.cs ├── Models ├── RollingAverage.cs ├── CutInfo.cs └── DataProcessor.cs ├── Configuration └── Config.cs ├── LICENSE ├── manifest.json ├── OverswingCounter.sln ├── Harmony Patches └── CutHandler.cs ├── Plugin.cs ├── README.md ├── .editorconfig ├── OverswingCounter.csproj └── Directory.Build.targets /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | bin/* 3 | obj/* 4 | *.user 5 | *.xcf 6 | Screenshots/SocialPreview.jpg 7 | .vs/* -------------------------------------------------------------------------------- /Images/ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinsi55/BeatSaber_OverswingCounter/HEAD/Images/ss.jpg -------------------------------------------------------------------------------- /Images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinsi55/BeatSaber_OverswingCounter/HEAD/Images/icon.png -------------------------------------------------------------------------------- /Counter/BSML/SettingsHandler.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using OverswingCounter.Configuration; 3 | 4 | namespace OverswingCounter.Counter.BSML { 5 | internal class SettingsHandler { 6 | [UIValue("Config")] 7 | public Config Config => Config.Instance; 8 | } 9 | } -------------------------------------------------------------------------------- /Models/RollingAverage.cs: -------------------------------------------------------------------------------- 1 | namespace OverswingCounter.Models { 2 | public class RollingAverage { 3 | private int _size = 0; 4 | private int _usedSize = 0; 5 | private int _currentIndex = 0; 6 | private float[] _container; 7 | 8 | public double Average { get; private set; } 9 | public double Sum { get; private set; } 10 | 11 | public RollingAverage(int size) { 12 | _size = size; 13 | _container = new float[size]; 14 | } 15 | 16 | public void Add(float value) { 17 | if(++_currentIndex >= _size) 18 | _currentIndex = 0; 19 | 20 | Sum -= _container[_currentIndex]; 21 | _container[_currentIndex] = value; 22 | Sum += value; 23 | 24 | if(_usedSize < _size) 25 | _usedSize++; 26 | 27 | Average = Sum / _usedSize; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Configuration/Config.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using IPA.Config.Stores; 3 | 4 | [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)] 5 | 6 | namespace OverswingCounter.Configuration { 7 | internal class Config { 8 | public static Config Instance; 9 | 10 | public virtual int averageCount { get; set; } = 12; 11 | public virtual int decimalPlaces { get; set; } = 0; 12 | public virtual bool underswingByPoints { get; set; } = true; 13 | 14 | public virtual float ignoreCutsWithNoPrecedingWithin { get; set; } = 0.6f; 15 | 16 | public virtual float targetExtraAngle { get; set; } = 13f; 17 | public virtual float lowerWarning { get; set; } = 8f; 18 | public virtual float upperWarning { get; set; } = 13f; 19 | 20 | public virtual bool ignoreArcsAndChains { get; set; } = true; 21 | } 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kinsi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json", 3 | "id": "OverswingCounter", 4 | "name": "OverswingCounter", 5 | "author": "Kinsi55, OrdinaryTable", 6 | "version": "0.2.1", 7 | "description": "Counter for Counters+ that shows you how much swing angle it is that you have too much, or too little, compared to a 'perfect' swing", 8 | "gameVersion": "1.39.1", 9 | "dependsOn": { 10 | "BSIPA": "^4.2.0", 11 | "BeatSaberMarkupLanguage": "^1.6.0", 12 | "Counters+": "^2.2.7" 13 | }, 14 | "features": { 15 | "CountersPlus.CustomCounter": { 16 | "Name": "Overswing Counter", 17 | "Description": "Point out the overswing of your cuts", 18 | "CounterLocation": "OverswingCounter.Counter.OverswingCounter", 19 | "ConfigDefaults": { 20 | "Enabled": true, 21 | "Position": "AboveCombo", 22 | "Distance": 0 23 | }, 24 | "BSML": { 25 | "Resource": "OverswingCounter.Counter.BSML.Settings.bsml", 26 | "Host": "OverswingCounter.Counter.BSML.SettingsHandler", 27 | "Icon": "OverswingCounter.Images.Icon.png" 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /OverswingCounter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31025.194 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OverswingCounter", "OverswingCounter.csproj", "{EE0630A6-8988-480C-8D24-C4F532DB9063}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {EE0630A6-8988-480C-8D24-C4F532DB9063}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {EE0630A6-8988-480C-8D24-C4F532DB9063}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {EE0630A6-8988-480C-8D24-C4F532DB9063}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {EE0630A6-8988-480C-8D24-C4F532DB9063}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {6A13FCD3-FD4D-4083-BB6B-F87FD84F920C} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Harmony Patches/CutHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using HarmonyLib; 4 | using OverswingCounter.Configuration; 5 | using OverswingCounter.Models; 6 | 7 | namespace OverswingCounter.Harmony_Patches { 8 | [HarmonyPatch(typeof(GoodCutScoringElement), nameof(GoodCutScoringElement.Init))] 9 | internal static class CutHandler { 10 | internal static Dictionary CurrentPrimaryCut; 11 | internal static Dictionary LastFinishedCut; 12 | internal static Action NewCutCompleted; 13 | 14 | internal static void Clear() { 15 | CurrentPrimaryCut = new Dictionary(2) 16 | { 17 | { SaberType.SaberA, null }, 18 | { SaberType.SaberB, null } 19 | }; 20 | LastFinishedCut = new Dictionary(2) 21 | { 22 | { SaberType.SaberA, null }, 23 | { SaberType.SaberB, null } 24 | }; 25 | } 26 | 27 | [HarmonyPrefix] 28 | internal static void Prefix(ref NoteCutInfo noteCutInfo) { 29 | if(Config.Instance.ignoreArcsAndChains && 30 | noteCutInfo.noteData.scoringType is NoteData.ScoringType.SliderHead 31 | or NoteData.ScoringType.SliderTail 32 | or NoteData.ScoringType.BurstSliderHead 33 | or NoteData.ScoringType.BurstSliderElement) 34 | return; 35 | 36 | new CutInfo(noteCutInfo); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Plugin.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using HarmonyLib; 4 | using IPA; 5 | using IPA.Config.Stores; 6 | using OverswingCounter.Configuration; 7 | using OverswingCounter.Harmony_Patches; 8 | using UnityEngine; 9 | using UnityEngine.SceneManagement; 10 | using IPALogger = IPA.Logging.Logger; 11 | 12 | namespace OverswingCounter { 13 | [Plugin(RuntimeOptions.SingleStartInit)] 14 | public class Plugin { 15 | internal static Plugin Instance { get; private set; } 16 | internal static IPALogger Log { get; private set; } 17 | internal static Harmony Harmony; 18 | internal static SaberManager SaberManager; 19 | 20 | [Init] 21 | public void Init(IPALogger logger, IPA.Config.Config config) { 22 | Instance = this; 23 | Log = logger; 24 | Config.Instance = config.Generated(); 25 | Harmony = new Harmony("Kinsi55.BeatSaber.OverswingCounter"); 26 | } 27 | 28 | [OnEnable] 29 | public void OnEnable() { 30 | SceneManager.activeSceneChanged += SceneManager_activeSceneChanged; 31 | 32 | Harmony.PatchAll(Assembly.GetExecutingAssembly()); 33 | } 34 | 35 | private void SceneManager_activeSceneChanged(Scene oldScene, Scene newScene) { 36 | if(oldScene.name != "GameCore" && newScene.name != "GameCore") 37 | return; 38 | 39 | SaberManager = Resources.FindObjectsOfTypeAll().First(); 40 | CutHandler.Clear(); 41 | } 42 | 43 | [OnDisable] 44 | public void OnDisable() { 45 | SceneManager.activeSceneChanged -= SceneManager_activeSceneChanged; 46 | 47 | Harmony.UnpatchSelf(); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Counter/BSML/Settings.bsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 15 | 18 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Models/CutInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OverswingCounter.Harmony_Patches; 3 | using UnityEngine; 4 | 5 | namespace OverswingCounter.Models { 6 | public class CutInfo { 7 | public NoteCutInfo NoteCutInfo; 8 | public Saber Saber; 9 | public SaberType SaberType; 10 | public SaberMovementData SaberMovementData => Saber.movementDataForLogic; 11 | public DataProcessor DataProcessor; 12 | public float CutTime; 13 | 14 | public float BeforeRating; 15 | public float AfterRating; 16 | 17 | public float Angle; 18 | public Vector3 StartPos; 19 | public Vector3 EndPos; 20 | public bool IsDownswing => Angle is <= 10f or >= 170f; 21 | 22 | public bool IsPrimary { get; private set; } = false; 23 | public CutInfo LastFinishedCutToCompareAgainst { get; private set; } 24 | 25 | public CutInfo(NoteCutInfo noteCutInfo) { 26 | NoteCutInfo = noteCutInfo; 27 | Saber = Plugin.SaberManager.SaberForType(NoteCutInfo.saberType); 28 | SaberType = Saber.saberType; 29 | DataProcessor = new DataProcessor(this, noteCutInfo.notePosition, noteCutInfo.noteRotation); 30 | CutTime = Time.realtimeSinceStartup; 31 | 32 | LastFinishedCutToCompareAgainst = CutHandler.LastFinishedCut[SaberType]; 33 | IsPrimary = CutHandler.CurrentPrimaryCut[SaberType] == null; 34 | if(IsPrimary) 35 | CutHandler.CurrentPrimaryCut[SaberType] = this; 36 | } 37 | 38 | public void Finish() { 39 | SaberMovementData.RemoveDataProcessor(DataProcessor); 40 | 41 | #if DEBUG 42 | Plugin.Log.Info($"Finished: {BeforeRating:P2}pre {AfterRating:P2}post {Angle:F1}deg"); 43 | #endif 44 | 45 | if(AfterRating < 1f) { 46 | var corrected = (float)Math.Round(AfterRating * SaberSwingRating.kAfterCutAngleFor1Rating) / SaberSwingRating.kAfterCutAngleFor1Rating; 47 | if(corrected > AfterRating) 48 | AfterRating = corrected; 49 | } 50 | 51 | if(BeforeRating < 1f) { 52 | var corrected = (float)Math.Round(BeforeRating * SaberSwingRating.kBeforeCutAngleFor1Rating) / SaberSwingRating.kBeforeCutAngleFor1Rating; 53 | if(corrected > BeforeRating) 54 | BeforeRating = corrected; 55 | } 56 | 57 | CutHandler.LastFinishedCut[SaberType] = this; 58 | if(CutHandler.CurrentPrimaryCut[SaberType] == this) 59 | CutHandler.CurrentPrimaryCut[SaberType] = null; 60 | 61 | if(CutHandler.NewCutCompleted != null) 62 | CutHandler.NewCutCompleted(this); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OverswingCounter 2 | 3 | The Game version(s) specific releases are compatible with are mentioned in the Release title (Its obviously possible latest is not supported assuming its been released recently). If you need the plugin for an older version - Grab an older release that fits 🤯 4 | 5 | ### Install 6 | 7 | Click on [releases](https://github.com/kinsi55/BeatSaber_OverswingCounter/releases), download the dll from the latest release and place it in your plugins folder. 8 | 9 | --- 10 | 11 | ### Functional principle (What do the numbers mean??) 12 | 13 | ![SS](Images/ss.jpg) 14 | 15 | ***This counter primarily focuses on the pre-swing angle, but its not just that and once you realize how it works you know why:*** 16 | 17 | - Cuts which had no other cut with the same saber within a given timespan are ignored (By default 0.6 seconds, configureable / disableable) 18 | - For multiple cuts in the same Swing only the first cut is taken into account. When you have situations like sliders or crosses you obviously do have overswing on the followthrough cuts, but thats not something you can change 19 | - If you have a bottom row down Swing followed by a top row up Swing (Works for any angle, the thing that matters is that the second Swing roughly started where the first one ended), because of the first post-swing, your next pre-swing will obviously probably be too big. In this situation the amount you overswung will be determined by the previous post-swing (If that yields a lower number, which it almost always will). 20 | - Double directionals are a special case. As previously mentioned, the counter is split in Down and Up Swings. Where you have double downs, as soon as you cut the second block, the counter will take the previous cuts post-overswing and add that into the bottom value. 21 | 22 | ***You can interpret the numbers in two different ways:*** 23 | 24 | Either the lower values are how much you overswing on your pre-swing *coming from the bottom*, or it is how much you overswing on your post-swing *going to the bottom* - Vice versa. The latter is more consistent when accounting for the case of double downs. Each of the values has its own history / averaging timespan 25 | 26 | ***Somewhat accurate TL;DR: If the lower number is big you are overswinging on your down post-swings, vice versa.*** 27 | 28 | 29 | Caveats: 30 | 31 | - **It does not work in (New) replays**, works in old ones (As much as possible anyways). I'm not sure if I'll ever be able to change that, so you probably want to record yourself playing if you want to analyze it after the fact. 32 | - The splitting of the values into Up and Down happens based off your swing angle, so for 90 degree's its kinda "random" 33 | -------------------------------------------------------------------------------- /Models/DataProcessor.cs: -------------------------------------------------------------------------------- 1 | using IPA.Utilities; 2 | using UnityEngine; 3 | 4 | namespace OverswingCounter.Models { 5 | public class DataProcessor : ISaberMovementDataProcessor { 6 | private static FieldAccessor.Accessor SaberMovementData_validCount = FieldAccessor.GetAccessor("_validCount"); 7 | private static FieldAccessor.Accessor SaberMovementData_data = FieldAccessor.GetAccessor("_data"); 8 | private static FieldAccessor.Accessor SaberMovementData_nextAddIndex = FieldAccessor.GetAccessor("_nextAddIndex"); 9 | 10 | private float _cutTime; 11 | private bool _notePlaneWasCut; 12 | 13 | private Vector3 _cutPlaneNormal; 14 | private Vector3 _notePlaneCenter; 15 | private Vector3 _noteForward; 16 | private Plane _notePlane; 17 | 18 | public CutInfo CutInfo; 19 | 20 | public DataProcessor(CutInfo cutInfo, Vector3 notePosition, Quaternion noteRotation) { 21 | CutInfo = cutInfo; 22 | 23 | var lastAddedData = cutInfo.SaberMovementData.lastAddedData; 24 | _cutTime = lastAddedData.time; 25 | _cutPlaneNormal = lastAddedData.segmentNormal; 26 | _notePlaneCenter = notePosition; 27 | _notePlane = new Plane(noteRotation * Vector3.up, _notePlaneCenter); 28 | _noteForward = noteRotation * Vector3.forward; 29 | 30 | CutInfo.BeforeRating = ComputeSwingRating(); 31 | 32 | CutInfo.SaberMovementData.AddDataProcessor(this); 33 | CutInfo.SaberMovementData.RequestLastDataProcessing(this); 34 | } 35 | 36 | // Stripped down, unclamped version of SaberSwingRatingCounter::ProcessNewData 37 | public void ProcessNewData(BladeMovementDataElement newData, BladeMovementDataElement prevData, bool prevDataAreValid) { 38 | if(newData.time - _cutTime > 0.4f) { 39 | CutInfo.Finish(); 40 | return; 41 | } 42 | 43 | if(!prevDataAreValid) 44 | return; 45 | 46 | if(!_notePlaneWasCut) 47 | _notePlane = new Plane(Vector3.Cross(_cutPlaneNormal, _noteForward), _notePlaneCenter); 48 | 49 | if(!_notePlaneWasCut && !_notePlane.SameSide(newData.topPos, prevData.topPos)) { 50 | var beforeCutTopPos = prevData.topPos; 51 | var beforeCutBottomPos = prevData.bottomPos; 52 | var afterCutTopPos = newData.topPos; 53 | var afterCutBottomPos = newData.bottomPos; 54 | 55 | // Raycasts are somewhat expensive - Might make sense to reuse the SSRC's values with a transpiler 56 | var ray = new Ray(beforeCutTopPos, afterCutTopPos - beforeCutTopPos); 57 | _notePlane.Raycast(ray, out var distance); 58 | 59 | var diff = ray.GetPoint(distance) - (beforeCutBottomPos + afterCutBottomPos) * 0.5f; 60 | var overrideSegmentAngle = Vector3.Angle(diff, beforeCutTopPos - beforeCutBottomPos); 61 | var angleDiff = Vector3.Angle(diff, afterCutTopPos - afterCutBottomPos); 62 | _cutTime = newData.time; 63 | _notePlaneWasCut = true; 64 | 65 | CutInfo.BeforeRating = ComputeSwingRating(true, overrideSegmentAngle); 66 | CutInfo.AfterRating = SaberSwingRating.AfterCutStepRating(angleDiff, 0f); 67 | } else { 68 | var angle = Vector3.Angle(newData.segmentNormal, _cutPlaneNormal); 69 | if(angle > 90f) { 70 | CutInfo.Finish(); 71 | return; 72 | } 73 | 74 | CutInfo.AfterRating += SaberSwingRating.AfterCutStepRating(newData.segmentAngle, angle); 75 | } 76 | } 77 | 78 | // Unclamped version of SaberMovementData::ComputeSwingRating(bool, float) 79 | public float ComputeSwingRating(bool overrideSegmentAngle = false, float overrideValue = 0f) { 80 | var instance = CutInfo.SaberMovementData; 81 | var _validCount = SaberMovementData_validCount(ref instance); 82 | var _data = SaberMovementData_data(ref instance); 83 | 84 | if(_validCount < 2) 85 | return 0f; 86 | 87 | var len = _data.Length; 88 | var idx = SaberMovementData_nextAddIndex(ref instance) - 1; 89 | if(idx < 0) 90 | idx += len; 91 | 92 | var startElement = _data[idx]; 93 | var time = startElement.time; 94 | var earliestProcessedMovementData = time; 95 | var rating = 0f; 96 | var segmentNormal = startElement.segmentNormal; 97 | var angleDiff = overrideSegmentAngle ? overrideValue : startElement.segmentAngle; 98 | var minRequiredMovementData = 2; 99 | var startPos = Vector3.zero; 100 | var endPos = startElement.topPos; 101 | 102 | rating += SaberSwingRating.BeforeCutStepRating(angleDiff, 0f); 103 | while(time - earliestProcessedMovementData < 0.4f && minRequiredMovementData < _validCount) { 104 | if(--idx < 0) 105 | idx += len; 106 | var elem = _data[idx]; 107 | 108 | var angle = Vector3.Angle(elem.segmentNormal, segmentNormal); 109 | if(angle > 90f) 110 | break; 111 | startPos = elem.topPos; 112 | 113 | rating += SaberSwingRating.BeforeCutStepRating(elem.segmentAngle, angle); 114 | earliestProcessedMovementData = elem.time; 115 | minRequiredMovementData++; 116 | } 117 | 118 | CutInfo.StartPos = startPos; 119 | CutInfo.EndPos = endPos; 120 | CutInfo.Angle = Mathf.Rad2Deg * Mathf.Atan2(endPos.y - startPos.y, endPos.x - startPos.x); 121 | 122 | return rating; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Counter/OverswingCounter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using CountersPlus.Counters.Custom; 4 | using OverswingCounter.Configuration; 5 | using OverswingCounter.Harmony_Patches; 6 | using OverswingCounter.Models; 7 | using TMPro; 8 | using UnityEngine; 9 | 10 | namespace OverswingCounter.Counter { 11 | public class OverswingCounter : BasicCustomCounter { 12 | private TMP_Text _counterLeftPreswingUp; 13 | private TMP_Text _counterRightPreswingUp; 14 | 15 | private TMP_Text _counterLeftPreswingDown; 16 | private TMP_Text _counterRightPreswingDown; 17 | 18 | private RollingAverage[] _leftValues; 19 | private RollingAverage[] _rightValues; 20 | 21 | public override void CounterInit() { 22 | var label = CanvasUtility.CreateTextFromSettings(Settings); 23 | label.text = "Overswing"; 24 | label.fontSize = 3; 25 | 26 | TMP_Text CreateLabel(TextAlignmentOptions align, Vector3 offset) { 27 | var x = CanvasUtility.CreateTextFromSettings(Settings, offset); 28 | x.text = FormatDecimals(0f); 29 | x.alignment = align; 30 | 31 | return x; 32 | } 33 | 34 | _counterRightPreswingUp = CreateLabel(TextAlignmentOptions.TopLeft, new Vector3(0.25f, -0.6f, 0)); 35 | _counterLeftPreswingUp = CreateLabel(TextAlignmentOptions.TopRight, new Vector3(-0.25f, -0.6f, 0)); 36 | 37 | _counterRightPreswingDown = CreateLabel(TextAlignmentOptions.TopLeft, new Vector3(0.25f, -0.2f, 0)); 38 | _counterLeftPreswingDown = CreateLabel(TextAlignmentOptions.TopRight, new Vector3(-0.25f, -0.2f, 0)); 39 | 40 | _leftValues = new[] { 41 | new RollingAverage(Config.Instance.averageCount), 42 | new RollingAverage(Config.Instance.averageCount) 43 | }; 44 | 45 | _rightValues = new[] { 46 | new RollingAverage(Config.Instance.averageCount), 47 | new RollingAverage(Config.Instance.averageCount) 48 | }; 49 | 50 | CutHandler.NewCutCompleted = ProcessCompletedCut; 51 | } 52 | 53 | public override void CounterDestroy() { 54 | 55 | } 56 | 57 | private void ProcessCompletedCut(CutInfo cut) { 58 | if(!cut.IsPrimary || cut.LastFinishedCutToCompareAgainst == null) 59 | return; 60 | 61 | var previousCutWasWithinTimeframe = 62 | Config.Instance.ignoreCutsWithNoPrecedingWithin == 0f || 63 | cut.CutTime - cut.LastFinishedCutToCompareAgainst.CutTime < Config.Instance.ignoreCutsWithNoPrecedingWithin; 64 | 65 | if(!previousCutWasWithinTimeframe) 66 | return; 67 | 68 | var adjustedBeforeCut = cut.BeforeRating; 69 | var isLeftSaber = cut.SaberType == SaberType.SaberA; 70 | 71 | 72 | 73 | var isRelatedToPreviousCut = previousCutWasWithinTimeframe && Vector2.Distance(cut.LastFinishedCutToCompareAgainst.EndPos, cut.StartPos) <= 0.4; 74 | 75 | if(isRelatedToPreviousCut) { 76 | var prevPostswingExtraAngle = (cut.LastFinishedCutToCompareAgainst.AfterRating - 1) * SaberSwingRating.kAfterCutAngleFor1Rating; 77 | 78 | /* 79 | * Based on the extra postswing angle, calculate how much % of a PRESWING that is, to compare it to 80 | * this cuts preswing and see which one should be used to decide the overswing 81 | */ 82 | var extraPreswingAsPostswingFrac = 1f + (prevPostswingExtraAngle / SaberSwingRating.kBeforeCutAngleFor1Rating); 83 | 84 | #if DEBUG 85 | Console.WriteLine("Previous cut post-swing, converted to pre swing: {0:P2}", extraPreswingAsPostswingFrac); 86 | #endif 87 | // We wanna use whatever is lower, either the previous postswing, or our pre swing 88 | if(extraPreswingAsPostswingFrac < adjustedBeforeCut) { 89 | #if DEBUG 90 | Console.WriteLine("Previous had a smaller effective preswing. Using that instead."); 91 | #endif 92 | adjustedBeforeCut = extraPreswingAsPostswingFrac; 93 | } 94 | } else if(previousCutWasWithinTimeframe) { 95 | #if DEBUG 96 | Console.WriteLine("Previous cut with this saber was unrelated - Accounting for its postswing"); 97 | #endif 98 | /* 99 | * If the previous cut is NOT related to our current cut we need to factor in its postswing 100 | * as a theoretical preswing. This should probably primarily only happen with DD's 101 | */ 102 | GetCounterAndLabel( 103 | isLeftSaber, 104 | cut.LastFinishedCutToCompareAgainst.IsDownswing ? 1 : 0, 105 | out var targetAvgPrev, out var labelPrev 106 | ); 107 | 108 | targetAvgPrev.Add((cut.LastFinishedCutToCompareAgainst.AfterRating - 1) * SaberSwingRating.kAfterCutAngleFor1Rating); 109 | SetLabelValue(targetAvgPrev, labelPrev); 110 | } 111 | 112 | 113 | 114 | GetCounterAndLabel( 115 | isLeftSaber, 116 | cut.IsDownswing ? 0 : 1, 117 | out var targetAvg, out var label 118 | ); 119 | 120 | targetAvg.Add((adjustedBeforeCut - 1f) * SaberSwingRating.kBeforeCutAngleFor1Rating); 121 | SetLabelValue(targetAvg, label); 122 | } 123 | 124 | private void GetCounterAndLabel(bool leftSaber, int index, out RollingAverage avg, out TMP_Text label) { 125 | avg = leftSaber ? _leftValues[index] : _rightValues[index]; 126 | label = leftSaber ? (index == 0 ? _counterLeftPreswingDown : _counterLeftPreswingUp) : (index == 0 ? _counterRightPreswingDown : _counterRightPreswingUp); 127 | } 128 | 129 | private void SetLabelValue(RollingAverage v, TMP_Text label) { 130 | label.text = FormatDecimals((float)v.Average) + "°"; 131 | 132 | var ptsDeviation = v.Average - Config.Instance.targetExtraAngle; 133 | 134 | var colorValue = ptsDeviation; 135 | var outColor = Color.red; 136 | 137 | if(colorValue >= 0) { 138 | colorValue /= Config.Instance.upperWarning; 139 | outColor = Color.yellow; 140 | } else { 141 | colorValue /= -Config.Instance.lowerWarning; 142 | } 143 | 144 | label.color = Color.Lerp(Color.white, outColor, (float)Math.Pow(colorValue, 3)); 145 | } 146 | 147 | private string FormatDecimals(float f) => f.ToString($"F{Config.Instance.decimalPlaces}", CultureInfo.InvariantCulture); 148 | } 149 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Entfernen Sie die folgende Zeile, wenn Sie EDITORCONFIG-Einstellungen von höheren Verzeichnissen vererben möchten. 2 | root = true 3 | 4 | # C#-Dateien 5 | [*.cs] 6 | 7 | #### Wichtige EditorConfig-Optionen #### 8 | 9 | # Einzüge und Abstände 10 | indent_size = 4 11 | indent_style = tab 12 | tab_width = 4 13 | 14 | # Einstellungen für neue Zeilen 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET-Codierungskonventionen #### 19 | 20 | # Using-Direktiven organisieren 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = true 23 | 24 | # this.- und Me.-Einstellungen 25 | dotnet_style_qualification_for_event = false 26 | dotnet_style_qualification_for_field = false 27 | dotnet_style_qualification_for_method = false 28 | dotnet_style_qualification_for_property = false 29 | 30 | # Einstellungen für Klammern 31 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion 32 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 33 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 34 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 35 | 36 | # Einstellungen für Ausdrucksebene 37 | dotnet_style_coalesce_expression = true 38 | dotnet_style_collection_initializer = true 39 | dotnet_style_explicit_tuple_names = true 40 | dotnet_style_null_propagation = true 41 | dotnet_style_object_initializer = true:silent 42 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 43 | dotnet_style_prefer_auto_properties = true 44 | dotnet_style_prefer_compound_assignment = true 45 | dotnet_style_prefer_conditional_expression_over_assignment = true 46 | dotnet_style_prefer_conditional_expression_over_return = true 47 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 48 | dotnet_style_prefer_inferred_tuple_names = true 49 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 50 | dotnet_style_prefer_simplified_boolean_expressions = true 51 | dotnet_style_prefer_simplified_interpolation = true 52 | 53 | # Einstellungen für Felder 54 | dotnet_style_readonly_field = true 55 | 56 | # Einstellungen für Parameter 57 | dotnet_code_quality_unused_parameters = all 58 | 59 | # Unterdrückungseinstellungen 60 | dotnet_remove_unnecessary_suppression_exclusions = none 61 | 62 | #### C#-Codierungskonventionen #### 63 | 64 | # Var-Einstellungen 65 | csharp_style_var_elsewhere = true:suggestion 66 | csharp_style_var_for_built_in_types = true:suggestion 67 | csharp_style_var_when_type_is_apparent = true:suggestion 68 | 69 | # Ausdruckskörpermember 70 | csharp_style_expression_bodied_accessors = true 71 | csharp_style_expression_bodied_constructors = false 72 | csharp_style_expression_bodied_indexers = true 73 | csharp_style_expression_bodied_lambdas = true 74 | csharp_style_expression_bodied_local_functions = true 75 | csharp_style_expression_bodied_methods = true 76 | csharp_style_expression_bodied_operators = false 77 | csharp_style_expression_bodied_properties = true 78 | 79 | # Einstellungen für den Musterabgleich 80 | csharp_style_pattern_matching_over_as_with_null_check = true 81 | csharp_style_pattern_matching_over_is_with_cast_check = true 82 | csharp_style_prefer_not_pattern = true 83 | csharp_style_prefer_pattern_matching = false 84 | csharp_style_prefer_switch_expression = true 85 | 86 | # Einstellungen für NULL-Überprüfung 87 | csharp_style_conditional_delegate_call = true 88 | 89 | # Einstellungen für Modifizierer 90 | csharp_prefer_static_local_function = true 91 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 92 | 93 | # Einstellungen für Codeblöcke 94 | csharp_prefer_braces = false:suggestion 95 | csharp_prefer_simple_using_statement = false 96 | 97 | # Einstellungen für Ausdrucksebene 98 | csharp_style_deconstructed_variable_declaration = false:silent 99 | csharp_style_implicit_object_creation_when_type_is_apparent = false 100 | csharp_style_inlined_variable_declaration = true 101 | csharp_style_throw_expression = false 102 | 103 | #### C#-Formatierungsregeln #### 104 | 105 | # Einstellungen für neue Zeilen 106 | csharp_new_line_before_catch = false 107 | csharp_new_line_before_else = false 108 | csharp_new_line_before_finally = false 109 | csharp_new_line_before_members_in_anonymous_types = true 110 | csharp_new_line_before_members_in_object_initializers = true 111 | csharp_new_line_before_open_brace = none 112 | csharp_new_line_between_query_expression_clauses = true 113 | 114 | # Einstellungen für Einrückung 115 | csharp_indent_block_contents = true 116 | csharp_indent_braces = false 117 | csharp_indent_case_contents = true 118 | csharp_indent_case_contents_when_block = false 119 | csharp_indent_labels = no_change 120 | csharp_indent_switch_labels = true 121 | 122 | # Einstellungen für Abstände 123 | csharp_space_after_cast = false 124 | csharp_space_after_colon_in_inheritance_clause = true 125 | csharp_space_after_comma = true 126 | csharp_space_after_dot = false 127 | csharp_space_after_keywords_in_control_flow_statements = false 128 | csharp_space_after_semicolon_in_for_statement = true 129 | csharp_space_around_binary_operators = before_and_after 130 | csharp_space_around_declaration_statements = false 131 | csharp_space_before_colon_in_inheritance_clause = true 132 | csharp_space_before_comma = false 133 | csharp_space_before_dot = false 134 | csharp_space_before_open_square_brackets = false 135 | csharp_space_before_semicolon_in_for_statement = false 136 | csharp_space_between_empty_square_brackets = false 137 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 138 | csharp_space_between_method_call_name_and_opening_parenthesis = false 139 | csharp_space_between_method_call_parameter_list_parentheses = false 140 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 141 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 142 | csharp_space_between_method_declaration_parameter_list_parentheses = false 143 | csharp_space_between_parentheses = false 144 | csharp_space_between_square_brackets = false 145 | 146 | # Umbrucheinstellungen 147 | csharp_preserve_single_line_blocks = true 148 | csharp_preserve_single_line_statements = false -------------------------------------------------------------------------------- /OverswingCounter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8.0.30703 4 | {EE0630A6-8988-480C-8D24-C4F532DB9063} 5 | OverswingCounter 6 | OverswingCounter 7 | net48 8 | true 9 | ..\Refs 10 | $(LocalRefsDir) 11 | $(MSBuildProjectDirectory)\ 12 | OverswingCounter 13 | OverswingCounter 14 | Kinsi55 15 | 9 16 | disable 17 | 18 | 19 | True 20 | 21 | 22 | True 23 | True 24 | 25 | 26 | 27 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll 28 | false 29 | 30 | 31 | $(BeatSaberDir)\Plugins\Counters+.dll 32 | false 33 | 34 | 35 | $(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll 36 | 37 | 38 | $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll 39 | false 40 | 41 | 42 | 43 | False 44 | $(BeatSaberDir)\Libs\0Harmony.dll 45 | False 46 | 47 | 48 | $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll 49 | False 50 | 51 | 52 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll 53 | False 54 | 55 | 56 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll 57 | False 58 | 59 | 60 | $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll 61 | False 62 | 63 | 64 | $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll 65 | False 66 | 67 | 68 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll 69 | False 70 | 71 | 72 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll 73 | False 74 | 75 | 76 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll 77 | False 78 | 79 | 80 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll 81 | False 82 | 83 | 84 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll 85 | False 86 | 87 | 88 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll 89 | False 90 | 91 | 92 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll 93 | False 94 | 95 | 96 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll 97 | False 98 | 99 | 100 | $(BeatSaberDir)\Plugins\BSML.dll 101 | False 102 | 103 | 104 | 105 | 106 | 1.4.2 107 | runtime; build; native; contentfiles; analyzers; buildtransitive 108 | all 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 2.0 7 | 8 | false 9 | 10 | $(OutputPath)$(AssemblyName) 11 | 12 | $(OutputPath)Final 13 | True 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | $(BasePluginVersion) 30 | $(BasePluginVersion) 31 | $(BasePluginVersion) 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | $(AssemblyName) 41 | $(ArtifactName)-$(PluginVersion) 42 | $(ArtifactName)-bs$(GameVersion) 43 | $(ArtifactName)-$(CommitHash) 44 | 45 | 46 | 47 | 48 | 49 | 50 | $(AssemblyName) 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | $(AssemblyName) 65 | $(OutDir)zip\ 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | $(BeatSaberDir)\Plugins 79 | True 80 | Unable to copy assembly to game folder, did you set 'BeatSaberDir' correctly in your 'csproj.user' file? Plugins folder doesn't exist: '$(PluginDir)'. 81 | 82 | Unable to copy to Plugins folder, '$(BeatSaberDir)' does not appear to be a Beat Saber game install. 83 | 84 | Unable to copy to Plugins folder, 'BeatSaberDir' has not been set in your 'csproj.user' file. 85 | False 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | $(BeatSaberDir)\IPA\Pending\Plugins 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | --------------------------------------------------------------------------------