├── .gitignore ├── Images └── screenshot.jpg ├── SliceDetails ├── Resources │ ├── bloq.png │ ├── dot.png │ ├── arrow.png │ ├── cut_arrow.png │ └── bloq_gradient.png ├── Directory.Build.props ├── Installers │ ├── SDAppInstaller.cs │ ├── SDGameInstaller.cs │ └── SDMenuInstaller.cs ├── manifest.json ├── AffinityPatches │ ├── ResultsViewControllerPatch.cs │ ├── MenuTransitionsHelperPatch.cs │ ├── SoloFreePlayFlowCoordinatorPatch.cs │ ├── PartyFreePlayFlowCoordinatorPatch.cs │ └── PauseMenuManagerPatches.cs ├── UI │ ├── HoverHintControllerGrabber.cs │ ├── HoverHintControllerHandler.cs │ ├── AssetLoader.cs │ ├── Views │ │ ├── settingsView.bsml │ │ └── gridView.bsml │ ├── SettingsViewController.cs │ ├── PauseUIController.cs │ ├── SelectedTileIndicator.cs │ ├── UICreator.cs │ ├── NoteUI.cs │ └── GridViewController.cs ├── Utils │ └── ColorSchemeManager.cs ├── Data │ ├── NoteInfo.cs │ ├── Score.cs │ └── Tile.cs ├── Plugin.cs ├── Settings │ └── SettingsStore.cs ├── SliceProcessor.cs ├── SliceRecorder.cs └── SliceDetails.csproj ├── README.md ├── LICENSE └── SliceDetails.sln /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | */bin 3 | */obj 4 | *.csproj.user -------------------------------------------------------------------------------- /Images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckosmic/SliceDetails/HEAD/Images/screenshot.jpg -------------------------------------------------------------------------------- /SliceDetails/Resources/bloq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckosmic/SliceDetails/HEAD/SliceDetails/Resources/bloq.png -------------------------------------------------------------------------------- /SliceDetails/Resources/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckosmic/SliceDetails/HEAD/SliceDetails/Resources/dot.png -------------------------------------------------------------------------------- /SliceDetails/Resources/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckosmic/SliceDetails/HEAD/SliceDetails/Resources/arrow.png -------------------------------------------------------------------------------- /SliceDetails/Resources/cut_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckosmic/SliceDetails/HEAD/SliceDetails/Resources/cut_arrow.png -------------------------------------------------------------------------------- /SliceDetails/Resources/bloq_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckosmic/SliceDetails/HEAD/SliceDetails/Resources/bloq_gradient.png -------------------------------------------------------------------------------- /SliceDetails/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | True 6 | BSIPA 7 | 8 | -------------------------------------------------------------------------------- /SliceDetails/Installers/SDAppInstaller.cs: -------------------------------------------------------------------------------- 1 | using SliceDetails.UI; 2 | using Zenject; 3 | 4 | namespace SliceDetails.Installers 5 | { 6 | public class SDAppInstaller : Installer 7 | { 8 | public override void InstallBindings() { 9 | Container.Bind().AsSingle().Lazy(); 10 | Container.Bind().AsSingle(); 11 | Container.Bind().AsSingle(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SliceDetails/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json", 3 | "id": "SliceDetails", 4 | "name": "SliceDetails", 5 | "author": "ckosmic", 6 | "version": "1.3.1", 7 | "description": "View your average cuts per-angle per-grid position in the pause menu and completion screen.", 8 | "gameVersion": "1.20.0", 9 | "dependsOn": { 10 | "BSIPA": "^4.2.1", 11 | "BeatSaberMarkupLanguage": "^1.6.0", 12 | "SiraUtil": "^3.0.0" 13 | }, 14 | "links": { 15 | "project-source": "https://github.com/ckosmic/SliceDetails" 16 | } 17 | } -------------------------------------------------------------------------------- /SliceDetails/AffinityPatches/ResultsViewControllerPatch.cs: -------------------------------------------------------------------------------- 1 | using SiraUtil.Affinity; 2 | using SliceDetails.UI; 3 | 4 | namespace SliceDetails.AffinityPatches 5 | { 6 | internal class ResultsViewControllerPatch : IAffinity 7 | { 8 | private readonly UICreator _uiCreator; 9 | 10 | public ResultsViewControllerPatch(UICreator uiCreator) { 11 | _uiCreator = uiCreator; 12 | } 13 | 14 | [AffinityPostfix] 15 | [AffinityPatch(typeof(ResultsViewController), nameof(ResultsViewController.DisableResultEnvironmentController))] 16 | internal void Postfix(ref ResultsViewController __instance) { 17 | if(Plugin.Settings.ShowInCompletionScreen) 18 | _uiCreator.RemoveFloatingScreen(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SliceDetails/AffinityPatches/MenuTransitionsHelperPatch.cs: -------------------------------------------------------------------------------- 1 | using SiraUtil.Affinity; 2 | using SliceDetails.UI; 3 | 4 | namespace SliceDetails.AffinityPatches 5 | { 6 | internal class MenuTransitionsHelperPatch : IAffinity 7 | { 8 | private readonly PauseUIController _pauseUIController; 9 | 10 | public MenuTransitionsHelperPatch(PauseUIController pauseUIController) { 11 | _pauseUIController = pauseUIController; 12 | } 13 | 14 | [AffinityPostfix] 15 | [AffinityPatch(typeof(MenuTransitionsHelper), nameof(MenuTransitionsHelper.HandleMainGameSceneDidFinish))] 16 | internal void Postfix(ref MenuTransitionsHelper __instance) { 17 | if (Plugin.Settings.ShowInPauseMenu) 18 | _pauseUIController.CleanUp(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SliceDetails/Installers/SDGameInstaller.cs: -------------------------------------------------------------------------------- 1 | using SliceDetails.AffinityPatches; 2 | using SliceDetails.UI; 3 | using Zenject; 4 | 5 | namespace SliceDetails.Installers 6 | { 7 | internal class SDGameInstaller : Installer 8 | { 9 | public override void InstallBindings() { 10 | Container.BindInterfacesAndSelfTo().AsSingle(); 11 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 12 | Container.Bind().AsSingle(); 13 | Container.BindInterfacesAndSelfTo().AsSingle(); 14 | 15 | Container.BindInterfacesTo().AsSingle(); 16 | Container.BindInterfacesTo().AsSingle(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SliceDetails/UI/HoverHintControllerGrabber.cs: -------------------------------------------------------------------------------- 1 | using HMUI; 2 | using Zenject; 3 | 4 | namespace SliceDetails.UI 5 | { 6 | internal class HoverHintControllerGrabber : IInitializable 7 | { 8 | private readonly HoverHintControllerHandler _hoverHintControllerHandler; 9 | 10 | private HoverHintController _hoverHintController; 11 | 12 | public HoverHintControllerGrabber(HoverHintControllerHandler hoverHintControllerHandler, HoverHintController hoverHintController) { 13 | _hoverHintControllerHandler = hoverHintControllerHandler; 14 | _hoverHintController = hoverHintController; 15 | } 16 | 17 | public void Initialize() { 18 | _hoverHintControllerHandler.SetOriginalHoverHintController(_hoverHintController); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SliceDetails/Installers/SDMenuInstaller.cs: -------------------------------------------------------------------------------- 1 | using SliceDetails.AffinityPatches; 2 | using SliceDetails.UI; 3 | using Zenject; 4 | 5 | namespace SliceDetails.Installers 6 | { 7 | internal class SDMenuInstaller : Installer 8 | { 9 | public override void InstallBindings() { 10 | Container.BindInterfacesAndSelfTo().AsSingle(); 11 | Container.Bind().FromNewComponentAsViewController().AsSingle(); 12 | Container.Bind().AsSingle(); 13 | 14 | Container.BindInterfacesTo().AsSingle(); 15 | Container.BindInterfacesTo().AsSingle(); 16 | Container.BindInterfacesTo().AsSingle(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SliceDetails/Utils/ColorSchemeManager.cs: -------------------------------------------------------------------------------- 1 | using IPA.Utilities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using UnityEngine; 8 | using static PlayerSaveData; 9 | 10 | namespace SliceDetails.Utils 11 | { 12 | internal class ColorSchemeManager 13 | { 14 | private static ColorScheme _colorScheme; 15 | 16 | public static ColorScheme GetMainColorScheme() { 17 | if (_colorScheme == null) { 18 | ColorSchemesSettings colorSchemesSettings = ReflectionUtil.GetField(Resources.FindObjectsOfTypeAll().FirstOrDefault(), "_playerData").colorSchemesSettings; 19 | _colorScheme = colorSchemesSettings.GetSelectedColorScheme(); 20 | } 21 | return _colorScheme; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SliceDetails 2 | 3 | A Beat Saber mod that lets you view your average cuts per-note angle per-grid position in the pause menu and level completion screen. 4 | 5 | ![Screenshot 1](Images/screenshot.jpg) 6 | 7 | ## Installation 8 | 9 | - Install `BeatSaberMarkupLanguage` and `SiraUtil` from ModAssistant or manually 10 | - Download the [latest release](https://github.com/ckosmic/SliceDetails/releases/latest) and extract it into your Beat Saber directory 11 | 12 | ## Configuration 13 | 14 | Configuration is handled in UserData/SliceDetails.json: 15 | - `ShowHandle` (bool, default: false): Enables grabbable handles below the floating screens to allow you to position them 16 | - `TrueCutOffsets` (bool, default: true): If false, magnifies higher score offsets to help give a better look at which side of the block you need to improve on. 17 | 18 | ## License 19 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /SliceDetails/Data/NoteInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UnityEngine; 7 | 8 | namespace SliceDetails.Data 9 | { 10 | internal class NoteInfo 11 | { 12 | public NoteData noteData; 13 | public NoteCutInfo cutInfo; 14 | public float cutAngle; 15 | public float cutOffset; 16 | public Score score; 17 | public Vector2 noteGridPosition; 18 | public int noteIndex; 19 | 20 | public NoteInfo() { 21 | 22 | } 23 | 24 | public NoteInfo(NoteData noteData, NoteCutInfo cutInfo, float cutAngle, float cutOffset, Vector2 noteGridPosition, int noteIndex) { 25 | this.noteData = noteData; 26 | this.cutInfo = cutInfo; 27 | this.cutAngle = cutAngle; 28 | this.cutOffset = cutOffset; 29 | this.noteGridPosition = noteGridPosition; 30 | this.noteIndex = noteIndex; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SliceDetails/AffinityPatches/SoloFreePlayFlowCoordinatorPatch.cs: -------------------------------------------------------------------------------- 1 | using SiraUtil.Affinity; 2 | using SliceDetails.UI; 3 | using UnityEngine; 4 | 5 | namespace SliceDetails.AffinityPatches 6 | { 7 | internal class SoloFreePlayFlowCoordinatorPatch : IAffinity 8 | { 9 | private readonly UICreator _uiCreator; 10 | 11 | public SoloFreePlayFlowCoordinatorPatch(UICreator uiCreator) { 12 | _uiCreator = uiCreator; 13 | } 14 | 15 | [AffinityPostfix] 16 | [AffinityPatch(typeof(SoloFreePlayFlowCoordinator), "ProcessLevelCompletionResultsAfterLevelDidFinish")] 17 | internal void Postfix(ref SoloFreePlayFlowCoordinator __instance, LevelCompletionResults levelCompletionResults) { 18 | if (levelCompletionResults.levelEndAction == LevelCompletionResults.LevelEndAction.None && Plugin.Settings.ShowInCompletionScreen) { 19 | _uiCreator.CreateFloatingScreen(Plugin.Settings.ResultsUIPosition, Quaternion.Euler(Plugin.Settings.ResultsUIRotation)); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SliceDetails/UI/HoverHintControllerHandler.cs: -------------------------------------------------------------------------------- 1 | using HMUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SliceDetails.UI 9 | { 10 | internal class HoverHintControllerHandler 11 | { 12 | public HoverHintController hoverHintController { 13 | get { 14 | return (_hoverHintControllerCopy == null) ? _hoverHintControllerOriginal : _hoverHintControllerCopy; 15 | } 16 | } 17 | 18 | private HoverHintController _hoverHintControllerOriginal; 19 | private HoverHintController _hoverHintControllerCopy; 20 | 21 | internal void SetOriginalHoverHintController(HoverHintController original) { 22 | _hoverHintControllerOriginal = original; 23 | } 24 | 25 | internal void CloneHoverHintController() { 26 | _hoverHintControllerCopy = UnityEngine.Object.Instantiate(_hoverHintControllerOriginal); 27 | _hoverHintControllerCopy.transform.SetParent(null); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SliceDetails/AffinityPatches/PartyFreePlayFlowCoordinatorPatch.cs: -------------------------------------------------------------------------------- 1 | using SiraUtil.Affinity; 2 | using SliceDetails.UI; 3 | using UnityEngine; 4 | 5 | namespace SliceDetails.AffinityPatches 6 | { 7 | internal class PartyFreePlayFlowCoordinatorPatch : IAffinity 8 | { 9 | private readonly UICreator _uiCreator; 10 | 11 | public PartyFreePlayFlowCoordinatorPatch(UICreator uiCreator) 12 | { 13 | _uiCreator = uiCreator; 14 | } 15 | 16 | [AffinityPostfix] 17 | [AffinityPatch(typeof(PartyFreePlayFlowCoordinator), "ProcessLevelCompletionResultsAfterLevelDidFinish")] 18 | internal void Postfix(ref PartyFreePlayFlowCoordinator __instance, LevelCompletionResults levelCompletionResults) 19 | { 20 | if (levelCompletionResults.levelEndAction == LevelCompletionResults.LevelEndAction.None && Plugin.Settings.ShowInCompletionScreen) 21 | { 22 | _uiCreator.CreateFloatingScreen(Plugin.Settings.ResultsUIPosition, Quaternion.Euler(Plugin.Settings.ResultsUIRotation)); 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /SliceDetails/Plugin.cs: -------------------------------------------------------------------------------- 1 | using IPA; 2 | using IPA.Config; 3 | using IPA.Config.Stores; 4 | using SiraUtil.Zenject; 5 | using SliceDetails.Installers; 6 | using SliceDetails.Settings; 7 | using BeatSaberMarkupLanguage.Settings; 8 | using SliceDetails.UI; 9 | 10 | namespace SliceDetails 11 | { 12 | 13 | [Plugin(RuntimeOptions.DynamicInit), NoEnableDisable] 14 | public class Plugin 15 | { 16 | internal static SettingsStore Settings { get; private set; } 17 | 18 | [Init] 19 | public void Init(IPA.Logging.Logger logger, Config config, Zenjector zenject) { 20 | Settings = config.Generated(); 21 | 22 | BSMLSettings.instance.AddSettingsMenu("SliceDetails", $"SliceDetails.UI.Views.settingsView.bsml", SettingsViewController.instance); 23 | 24 | zenject.UseLogger(logger); 25 | 26 | zenject.Install(Location.App); 27 | zenject.Install(Location.Menu); 28 | zenject.Install(Location.StandardPlayer); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SliceDetails/Settings/SettingsStore.cs: -------------------------------------------------------------------------------- 1 | using IPA.Config.Stores; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using UnityEngine; 9 | 10 | [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)] 11 | namespace SliceDetails.Settings 12 | { 13 | internal class SettingsStore 14 | { 15 | public Vector3 ResultsUIPosition = new Vector3(-3.25f, 3.25f, 1.75f); 16 | public Vector3 ResultsUIRotation = new Vector3(340.0f, 292.0f, 0.0f); 17 | public Vector3 PauseUIPosition = new Vector3(-3.0f, 1.5f, 0.0f); 18 | public Vector3 PauseUIRotation = new Vector3(0.0f, 270.0f, 0.0f); 19 | public bool ShowInPauseMenu = true; 20 | public bool ShowInCompletionScreen = true; 21 | public bool ShowHandle = false; 22 | public bool TrueCutOffsets = true; 23 | public bool CountArcs = true; 24 | public bool CountChains = true; 25 | public bool ShowSliceCounts = true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 ckosmic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /SliceDetails/AffinityPatches/PauseMenuManagerPatches.cs: -------------------------------------------------------------------------------- 1 | using SiraUtil.Affinity; 2 | using SliceDetails.UI; 3 | 4 | namespace SliceDetails.AffinityPatches 5 | { 6 | internal class PauseMenuManagerPatches : IAffinity 7 | { 8 | private readonly PauseUIController _pauseUIController; 9 | 10 | public PauseMenuManagerPatches(PauseUIController pauseUIController) { 11 | _pauseUIController = pauseUIController; 12 | } 13 | 14 | [AffinityPostfix] 15 | [AffinityPatch(typeof(PauseMenuManager), nameof(PauseMenuManager.ShowMenu))] 16 | internal void ShowMenuPostfix(ref PauseMenuManager __instance) { 17 | if (Plugin.Settings.ShowInPauseMenu && _pauseUIController != null) 18 | _pauseUIController.PauseMenuOpened(__instance); 19 | } 20 | 21 | [AffinityPostfix] 22 | [AffinityPatch(typeof(PauseMenuManager), nameof(PauseMenuManager.StartResumeAnimation))] 23 | internal void StartResumeAnimationPostfix(ref PauseMenuManager __instance) { 24 | if (Plugin.Settings.ShowInPauseMenu && _pauseUIController != null) 25 | _pauseUIController.PauseMenuClosed(__instance); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SliceDetails/UI/AssetLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using UnityEngine; 4 | 5 | namespace SliceDetails.UI 6 | { 7 | internal class AssetLoader 8 | { 9 | public Sprite spr_arrow { get; } 10 | public Sprite spr_dot { get; } 11 | public Sprite spr_roundrect { get; } 12 | 13 | public AssetLoader() { 14 | spr_arrow = LoadSpriteFromResource("SliceDetails.Resources.arrow.png"); 15 | spr_dot = LoadSpriteFromResource("SliceDetails.Resources.dot.png"); 16 | spr_roundrect = LoadSpriteFromResource("SliceDetails.Resources.bloq.png"); 17 | } 18 | 19 | public static Sprite LoadSpriteFromResource(string path) { 20 | Assembly assembly = Assembly.GetCallingAssembly(); 21 | using (Stream stream = assembly.GetManifestResourceStream(path)) { 22 | if (stream != null) { 23 | byte[] data = new byte[stream.Length]; 24 | stream.Read(data, 0, (int)stream.Length); 25 | Texture2D tex = new Texture2D(2, 2); 26 | if (tex.LoadImage(data)) { 27 | Sprite sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0, 0), 100); 28 | return sprite; 29 | } 30 | } 31 | } 32 | return null; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SliceDetails.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30413.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SliceDetails", "SliceDetails\SliceDetails.csproj", "{15CDF006-CF49-4774-8C0E-8B5DA75D0194}" 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 | {15CDF006-CF49-4774-8C0E-8B5DA75D0194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {15CDF006-CF49-4774-8C0E-8B5DA75D0194}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {15CDF006-CF49-4774-8C0E-8B5DA75D0194}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {15CDF006-CF49-4774-8C0E-8B5DA75D0194}.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 = {E1EABC81-D6F4-4474-9977-439F9381E319} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /SliceDetails/UI/Views/settingsView.bsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SliceDetails/Data/Score.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SliceDetails.Data 8 | { 9 | internal class Score 10 | { 11 | public float PreSwing { get; set; } 12 | public float PostSwing { get; set; } 13 | public float Offset { get; set; } 14 | public float TotalScore { 15 | get 16 | { 17 | if (PreSwing < 0) 18 | return PostSwing + Offset; 19 | if (PostSwing < 0) 20 | return PreSwing + Offset; 21 | return PreSwing + PostSwing + Offset; 22 | } 23 | } 24 | 25 | public bool CountPreSwing { get; set; } = true; 26 | public bool CountPostSwing { get; set; } = true; 27 | 28 | public Score(float? preSwing, float? postSwing, float offset) { 29 | if (preSwing != null) 30 | PreSwing = (float)preSwing; 31 | else 32 | CountPreSwing = false; 33 | if (postSwing != null) 34 | PostSwing = (float)postSwing; 35 | else 36 | CountPostSwing = false; 37 | Offset = offset; 38 | } 39 | 40 | public static Score operator +(Score a, Score b) => new Score(a.PreSwing + b.PreSwing, a.PostSwing + b.PostSwing, a.Offset + b.Offset); 41 | public static Score operator /(Score a, float b) => new Score(a.PreSwing / b, a.PostSwing / b, a.Offset / b); 42 | public static Score operator /(Score a, Score b) => new Score(a.PreSwing / b.PreSwing, a.PostSwing / b.PostSwing, a.Offset / b.Offset); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SliceDetails/UI/SettingsViewController.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | 3 | namespace SliceDetails.UI 4 | { 5 | internal class SettingsViewController : PersistentSingleton 6 | { 7 | [UIValue("show-pause")] 8 | public bool ShowInPauseMenu { 9 | get { return Plugin.Settings.ShowInPauseMenu; } 10 | set { Plugin.Settings.ShowInPauseMenu = value; } 11 | } 12 | 13 | [UIValue("show-completion")] 14 | public bool ShowInCompletionScreen 15 | { 16 | get { return Plugin.Settings.ShowInCompletionScreen; } 17 | set { Plugin.Settings.ShowInCompletionScreen = value; } 18 | } 19 | 20 | [UIValue("show-handles")] 21 | public bool ShowHandle 22 | { 23 | get { return Plugin.Settings.ShowHandle; } 24 | set { Plugin.Settings.ShowHandle = value; } 25 | } 26 | 27 | [UIValue("show-counts")] 28 | public bool ShowSliceCounts 29 | { 30 | get { return Plugin.Settings.ShowSliceCounts; } 31 | set { Plugin.Settings.ShowSliceCounts = value; } 32 | } 33 | 34 | [UIValue("true-offsets")] 35 | public bool TrueCutOffsets 36 | { 37 | get { return Plugin.Settings.TrueCutOffsets; } 38 | set { Plugin.Settings.TrueCutOffsets = value; } 39 | } 40 | 41 | [UIValue("count-arcs")] 42 | public bool CountArcs 43 | { 44 | get { return Plugin.Settings.CountArcs; } 45 | set { Plugin.Settings.CountArcs = value; } 46 | } 47 | 48 | [UIValue("count-chains")] 49 | public bool CountChains 50 | { 51 | get { return Plugin.Settings.CountChains; } 52 | set { Plugin.Settings.CountChains = value; } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SliceDetails/SliceProcessor.cs: -------------------------------------------------------------------------------- 1 | using SliceDetails.UI; 2 | using SliceDetails.Data; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace SliceDetails 7 | { 8 | internal class SliceProcessor 9 | { 10 | public Tile[] tiles = new Tile[12]; 11 | public bool ready { get; private set; } = false; 12 | 13 | public void ResetProcessor() { 14 | ready = false; 15 | 16 | tiles = new Tile[12]; 17 | 18 | // Create "tiles", basically allocate information about each position in the 4x3 note grid. 19 | for (int i = 0; i < 12; i++) { 20 | tiles[i] = new Tile(); 21 | for (int j = 0; j < 18; j++) { 22 | tiles[i].tileNoteInfos[j] = new List(); 23 | } 24 | } 25 | } 26 | 27 | public void ProcessSlices(List noteInfos) { 28 | ResetProcessor(); 29 | 30 | // Populate the tiles' note infos. Each List in tileNoteInfos cooresponds to each direction/color combination (i.e. DownLeft/ColorA) 31 | // where elements 0-8 are ColorA notes and elements 9-17 are ColorB notes numbering from NoteCutDirection.Up (0) to NoteCutDirection.Any (8) 32 | foreach (NoteInfo ni in noteInfos) { 33 | int noteDirection = (int)Enum.Parse(typeof(OrderedNoteCutDirection), ni.noteData.cutDirection.ToString()); 34 | int noteColor = (int)ni.noteData.colorType; 35 | int tileNoteDataIndex = noteColor * 9 + noteDirection; 36 | 37 | tiles[ni.noteIndex].tileNoteInfos[tileNoteDataIndex].Add(ni); 38 | } 39 | 40 | // Calculate average angles and offsets 41 | for (int i = 0; i < 12; i++) { 42 | tiles[i].CalculateAverages(); 43 | } 44 | 45 | ready = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SliceDetails/UI/PauseUIController.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using Zenject; 3 | 4 | namespace SliceDetails.UI 5 | { 6 | internal class PauseUIController : IInitializable 7 | { 8 | private readonly SliceRecorder _sliceRecorder; 9 | private readonly UICreator _uiCreator; 10 | private readonly HoverHintControllerHandler _hoverHintControllerHandler; 11 | 12 | private GridViewController _gridViewController; 13 | private GameObject _gridViewControllerParent; 14 | 15 | public PauseUIController(SliceRecorder sliceRecorder, UICreator uiCreator, GridViewController gridViewController, HoverHintControllerHandler hoverHintControllerHandler) { 16 | _sliceRecorder = sliceRecorder; 17 | _uiCreator = uiCreator; 18 | _gridViewController = gridViewController; 19 | _hoverHintControllerHandler = hoverHintControllerHandler; 20 | } 21 | 22 | public void Initialize() { 23 | if (Plugin.Settings.ShowInPauseMenu) { 24 | _hoverHintControllerHandler.CloneHoverHintController(); 25 | _uiCreator.CreateFloatingScreen(Plugin.Settings.PauseUIPosition, Quaternion.Euler(Plugin.Settings.PauseUIRotation)); 26 | _gridViewControllerParent = _gridViewController.transform.parent.gameObject; 27 | _gridViewControllerParent?.SetActive(false); 28 | } 29 | } 30 | 31 | public void PauseMenuOpened(PauseMenuManager pauseMenuManager) { 32 | _gridViewControllerParent?.SetActive(true); 33 | _uiCreator.ParentFloatingScreen(pauseMenuManager.transform); 34 | _sliceRecorder.ProcessSlices(); 35 | _gridViewController.SetTileScores(); 36 | } 37 | 38 | public void PauseMenuClosed(PauseMenuManager pauseMenuManager) { 39 | _gridViewController.CloseModal(false); 40 | _gridViewControllerParent?.SetActive(false); 41 | } 42 | 43 | public void CleanUp() { 44 | _gridViewController.CloseModal(false); 45 | _uiCreator.RemoveFloatingScreen(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SliceDetails/Data/Tile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UnityEngine; 7 | 8 | namespace SliceDetails.Data 9 | { 10 | internal class Tile 11 | { 12 | public List[] tileNoteInfos = new List[18]; 13 | public float[] angleAverages = new float[18]; 14 | public float[] offsetAverages = new float[18]; 15 | public Score[] scoreAverages = new Score[18]; 16 | public int[] noteCounts = new int[18]; 17 | public float scoreAverage = 0.0f; 18 | public bool atLeastOneNote = false; 19 | public int noteCount = 0; 20 | 21 | public void CalculateAverages() { 22 | angleAverages = new float[18]; 23 | offsetAverages = new float[18]; 24 | scoreAverages = new Score[18]; 25 | noteCounts = new int[18]; 26 | scoreAverage = 0.0f; 27 | atLeastOneNote = false; 28 | 29 | for (int i = 0; i < 18; i++) { 30 | scoreAverages[i] = new Score(0.0f, 0.0f, 0.0f); 31 | } 32 | 33 | noteCount = 0; 34 | for (int i = 0; i < tileNoteInfos.Length; i++) { 35 | if (tileNoteInfos[i].Count > 0) { 36 | int preSwingCount = 0; 37 | int postSwingCount = 0; 38 | Vector2 angleXYAverages = Vector2.zero; 39 | foreach (NoteInfo noteInfo in tileNoteInfos[i]) { 40 | atLeastOneNote = true; 41 | angleXYAverages.x += Mathf.Cos(noteInfo.cutAngle * Mathf.PI / 180f); 42 | angleXYAverages.y += Mathf.Sin(noteInfo.cutAngle * Mathf.PI / 180f); 43 | offsetAverages[i] += noteInfo.cutOffset; 44 | scoreAverages[i] += noteInfo.score; 45 | noteCounts[i]++; 46 | scoreAverage += noteInfo.score.TotalScore; 47 | preSwingCount += scoreAverages[i].CountPreSwing ? 1 : 0; 48 | postSwingCount += scoreAverages[i].CountPostSwing ? 1 : 0; 49 | noteCount++; 50 | } 51 | angleXYAverages.x /= tileNoteInfos[i].Count; 52 | angleXYAverages.y /= tileNoteInfos[i].Count; 53 | angleAverages[i] = Mathf.Atan2(angleXYAverages.y, angleXYAverages.x) * 180f / Mathf.PI; 54 | offsetAverages[i] /= tileNoteInfos[i].Count; 55 | scoreAverages[i].PreSwing /= preSwingCount; 56 | scoreAverages[i].PostSwing /= postSwingCount; 57 | scoreAverages[i].Offset /= tileNoteInfos[i].Count; 58 | } 59 | } 60 | scoreAverage /= noteCount; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SliceDetails/UI/SelectedTileIndicator.cs: -------------------------------------------------------------------------------- 1 | using HMUI; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.UI; 5 | using BeatSaberMarkupLanguage; 6 | 7 | namespace SliceDetails.UI 8 | { 9 | internal class SelectedTileIndicator : MonoBehaviour 10 | { 11 | private static Sprite newRoundRect; 12 | private List _tileDots = new List(); 13 | 14 | public void Initialize(AssetLoader assetLoader) { 15 | // Create a new sliced sprite using the existing bloq texture so we don't have to make another resource 16 | if (newRoundRect == null) { 17 | Vector4 spriteBorder = new Vector4(54, 54, 54, 54); 18 | Texture2D spriteTexture = assetLoader.spr_roundrect.texture; 19 | Rect rect = new Rect(0, 0, spriteTexture.width, spriteTexture.height); 20 | newRoundRect = Sprite.Create(spriteTexture, rect, new Vector2(0.5f, 0.5f), 200, 1, SpriteMeshType.FullRect, spriteBorder); 21 | } 22 | 23 | ImageView background = new GameObject("Background").AddComponent(); 24 | background.transform.SetParent(transform, false); 25 | background.rectTransform.localScale = Vector3.one; 26 | background.rectTransform.localPosition = Vector3.zero; 27 | background.rectTransform.sizeDelta = new Vector2(15.0f, 12.0f); 28 | background.sprite = newRoundRect; 29 | background.type = Image.Type.Sliced; 30 | background.color = new Color(0.125f, 0.125f, 0.125f, 0.75f); 31 | background.material = Utilities.ImageResources.NoGlowMat; 32 | 33 | for (int i = 0; i < 12; i++) { 34 | ImageView tileDot = new GameObject("TileDot").AddComponent(); 35 | tileDot.transform.SetParent(background.transform, false); 36 | tileDot.rectTransform.localScale = Vector3.one; 37 | tileDot.rectTransform.localPosition = new Vector3((i % 4) * 3.0f - 4.5f, (i / 4) * 3.0f - 3.0f, 0.0f); 38 | tileDot.rectTransform.sizeDelta = new Vector2(6.0f, 6.0f); 39 | tileDot.sprite = assetLoader.spr_dot; 40 | tileDot.type = Image.Type.Simple; 41 | tileDot.color = Color.white; 42 | tileDot.material = Utilities.ImageResources.NoGlowMat; 43 | _tileDots.Add(tileDot); 44 | } 45 | } 46 | 47 | public void SetSelectedTile(int tileIndex) { 48 | for (int i = 0; i < _tileDots.Count; i++) { 49 | _tileDots[i].color = (tileIndex == i ? Color.white : new Color(0.5f, 0.5f, 0.5f, 0.7f)); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SliceDetails/UI/Views/gridView.bsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /SliceDetails/UI/UICreator.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.FloatingScreen; 2 | using HMUI; 3 | using SiraUtil.Logging; 4 | using UnityEngine; 5 | using UnityEngine.SceneManagement; 6 | 7 | namespace SliceDetails.UI 8 | { 9 | internal class UICreator 10 | { 11 | private readonly GridViewController _gridViewController; 12 | private readonly SliceProcessor _sliceProcessor; 13 | private readonly SiraLog _siraLog; 14 | 15 | private FloatingScreen _floatingScreen; 16 | 17 | public HoverHintController hoverHintController; 18 | 19 | public UICreator(SiraLog siraLog, GridViewController gridViewController, SliceProcessor sliceProcesssor) { 20 | _siraLog = siraLog; 21 | _gridViewController = gridViewController; 22 | _sliceProcessor = sliceProcesssor; 23 | } 24 | 25 | public void CreateFloatingScreen(Vector3 position, Quaternion rotation) { 26 | _siraLog.Info("Creating floating screen: " + (_gridViewController == null)); 27 | _gridViewController.UpdateUINotesHoverHintController(); 28 | 29 | 30 | _floatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(150f, 120f), true, position, rotation); 31 | _floatingScreen.SetRootViewController(_gridViewController, ViewController.AnimationType.None); 32 | _floatingScreen.ShowHandle = Plugin.Settings.ShowHandle; 33 | _floatingScreen.HandleSide = FloatingScreen.Side.Bottom; 34 | _floatingScreen.HighlightHandle = true; 35 | _floatingScreen.handle.transform.localScale = Vector3.one * 5.0f; 36 | _floatingScreen.handle.transform.localPosition = new Vector3(0.0f, -25.0f, 0.0f); 37 | _floatingScreen.HandleReleased += OnHandleReleased; 38 | _floatingScreen.gameObject.name = "SliceDetailsScreen"; 39 | _floatingScreen.transform.localScale = Vector3.one * 0.03f; 40 | 41 | _gridViewController.SetTileScores(); 42 | _gridViewController.transform.localScale = Vector3.one; 43 | _gridViewController.transform.localEulerAngles = Vector3.zero; 44 | _gridViewController.gameObject.SetActive(true); 45 | } 46 | 47 | public void RemoveFloatingScreen() { 48 | // Destroying the hover hint panel breaks everything so move it out of the screen before destroying 49 | if (_floatingScreen != null) { 50 | if (_floatingScreen.transform.GetComponentInChildren(true)) { 51 | Transform hoverHintPanel = _floatingScreen.transform.GetComponentInChildren(true).transform; 52 | hoverHintPanel.SetParent(null); 53 | } 54 | _gridViewController.transform.SetParent(null); 55 | _gridViewController.gameObject.SetActive(false); 56 | UnityEngine.Object.Destroy(_floatingScreen.gameObject); 57 | } 58 | _sliceProcessor.ResetProcessor(); 59 | } 60 | 61 | public void ParentFloatingScreen(Transform parent) { 62 | _floatingScreen.transform.SetParent(parent); 63 | } 64 | 65 | private void OnHandleReleased(object sender, FloatingScreenHandleEventArgs args) { 66 | if (SceneManager.GetActiveScene().name == "MainMenu") { 67 | Plugin.Settings.ResultsUIPosition = _floatingScreen.transform.position; 68 | Plugin.Settings.ResultsUIRotation = _floatingScreen.transform.eulerAngles; 69 | } else if (SceneManager.GetActiveScene().name == "GameCore") { 70 | Plugin.Settings.PauseUIPosition = _floatingScreen.transform.position; 71 | Plugin.Settings.PauseUIRotation = _floatingScreen.transform.eulerAngles; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SliceDetails/SliceRecorder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Zenject; 5 | using SliceDetails.Data; 6 | using SiraUtil.Logging; 7 | 8 | namespace SliceDetails 9 | { 10 | internal class SliceRecorder : UnityEngine.Object, IInitializable, IDisposable 11 | { 12 | private readonly BeatmapObjectManager _beatmapObjectManager; 13 | private readonly SliceProcessor _sliceProcessor; 14 | private readonly ScoreController _scoreController; 15 | 16 | [Inject] private SiraLog _siraLog; 17 | 18 | private Dictionary _noteSwingInfos = new Dictionary(); 19 | private List _noteInfos = new List(); 20 | 21 | public SliceRecorder(BeatmapObjectManager beatmapObjectManager, ScoreController scoreController, SliceProcessor sliceProcessor) { 22 | _beatmapObjectManager = beatmapObjectManager; 23 | _scoreController = scoreController; 24 | _sliceProcessor = sliceProcessor; 25 | } 26 | 27 | public void Initialize() { 28 | _beatmapObjectManager.noteWasCutEvent += OnNoteWasCut; 29 | _scoreController.scoringForNoteFinishedEvent += ScoringForNoteFinishedHandler; 30 | _sliceProcessor.ResetProcessor(); 31 | } 32 | 33 | public void Dispose() { 34 | _beatmapObjectManager.noteWasCutEvent -= OnNoteWasCut; 35 | _scoreController.scoringForNoteFinishedEvent -= ScoringForNoteFinishedHandler; 36 | // Process slices once the map ends 37 | ProcessSlices(); 38 | } 39 | 40 | public void ClearSlices() { 41 | _noteInfos.Clear(); 42 | ProcessSlices(); 43 | } 44 | 45 | public void ProcessSlices() { 46 | _sliceProcessor.ProcessSlices(_noteInfos); 47 | } 48 | 49 | private void OnNoteWasCut(NoteController noteController, in NoteCutInfo noteCutInfo) { 50 | if (noteController.noteData.colorType == ColorType.None || !noteCutInfo.allIsOK) return; 51 | ProcessNote(noteController, noteCutInfo); 52 | } 53 | 54 | private void ProcessNote(NoteController noteController, NoteCutInfo noteCutInfo) { 55 | if (noteController == null) return; 56 | 57 | Vector2 noteGridPosition; 58 | noteGridPosition.y = (int)noteController.noteData.noteLineLayer; 59 | noteGridPosition.x = noteController.noteData.lineIndex; 60 | int noteIndex = (int)(noteGridPosition.y * 4 + noteGridPosition.x); 61 | 62 | // No ME notes allowed >:( 63 | if (noteGridPosition.x >= 4 || noteGridPosition.y >= 3 || noteGridPosition.x < 0 || noteGridPosition.y < 0) return; 64 | 65 | Vector2 cutDirection = new Vector3(-noteCutInfo.cutNormal.y, noteCutInfo.cutNormal.x); 66 | float cutAngle = Mathf.Atan2(cutDirection.y, cutDirection.x) * Mathf.Rad2Deg + 180f; 67 | 68 | float cutOffset = noteCutInfo.cutDistanceToCenter; 69 | Vector3 noteCenter = noteController.noteTransform.position; 70 | if (Vector3.Dot(noteCutInfo.cutNormal, noteCutInfo.cutPoint - noteCenter) > 0f) 71 | { 72 | cutOffset = -cutOffset; 73 | } 74 | 75 | NoteInfo noteInfo = new NoteInfo(noteController.noteData, noteCutInfo, cutAngle, cutOffset, noteGridPosition, noteIndex); 76 | 77 | if (!_noteSwingInfos.ContainsKey(noteController.noteData)) 78 | { 79 | _noteSwingInfos.Add(noteController.noteData, noteInfo); 80 | } 81 | } 82 | 83 | public void ScoringForNoteFinishedHandler(ScoringElement scoringElement) { 84 | NoteInfo noteSwingInfo; 85 | if (_noteSwingInfos.TryGetValue(scoringElement.noteData, out noteSwingInfo)) 86 | { 87 | GoodCutScoringElement goodScoringElement = (GoodCutScoringElement)scoringElement; 88 | 89 | IReadonlyCutScoreBuffer cutScoreBuffer = goodScoringElement.cutScoreBuffer; 90 | 91 | int preSwing = cutScoreBuffer.beforeCutScore; 92 | int postSwing = cutScoreBuffer.afterCutScore; 93 | int offset = cutScoreBuffer.centerDistanceCutScore; 94 | 95 | switch (goodScoringElement.noteData.scoringType) 96 | { 97 | case NoteData.ScoringType.Normal: 98 | noteSwingInfo.score = new Score(preSwing, postSwing, offset); 99 | _noteInfos.Add(noteSwingInfo); 100 | break; 101 | case NoteData.ScoringType.SliderHead: 102 | if (!Plugin.Settings.CountArcs) break; 103 | noteSwingInfo.score = new Score(preSwing, null, offset); 104 | _noteInfos.Add(noteSwingInfo); 105 | break; 106 | case NoteData.ScoringType.SliderTail: 107 | if (!Plugin.Settings.CountArcs) break; 108 | noteSwingInfo.score = new Score(null, postSwing, offset); 109 | _noteInfos.Add(noteSwingInfo); 110 | break; 111 | case NoteData.ScoringType.BurstSliderHead: 112 | if (!Plugin.Settings.CountChains) break; 113 | noteSwingInfo.score = new Score(preSwing, null, offset); 114 | _noteInfos.Add(noteSwingInfo); 115 | break; 116 | } 117 | 118 | _noteSwingInfos.Remove(goodScoringElement.noteData); 119 | } 120 | else { 121 | // Bad cut, do nothing 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /SliceDetails/SliceDetails.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | Library 6 | 9 7 | disable 8 | false 9 | ..\Refs 10 | $(LocalRefsDir) 11 | $(MSBuildProjectDirectory)\ 12 | portable 13 | 14 | 15 | 16 | 17 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll 18 | 19 | 20 | $(BeatSaberDir)\Plugins\BSML.dll 21 | 22 | 23 | $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll 24 | 25 | 26 | $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll 27 | 28 | 29 | $(BeatSaberDir)\Plugins\SiraUtil.dll 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll 39 | False 40 | 41 | 42 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll 43 | False 44 | 45 | 46 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll 47 | False 48 | 49 | 50 | $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll 51 | False 52 | 53 | 54 | $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll 55 | False 56 | 57 | 58 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll 59 | False 60 | 61 | 62 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.AnimationModule.dll 63 | 64 | 65 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.AudioModule.dll 66 | 67 | 68 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll 69 | False 70 | 71 | 72 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll 73 | 74 | 75 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.InputLegacyModule.dll 76 | 77 | 78 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.PhysicsModule.dll 79 | 80 | 81 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll 82 | False 83 | 84 | 85 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll 86 | False 87 | 88 | 89 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll 90 | False 91 | 92 | 93 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll 94 | False 95 | 96 | 97 | $(BeatSaberDir)\Beat Saber_Data\Managed\VRUI.dll 98 | 99 | 100 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll 101 | 102 | 103 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | build; native; contentfiles; analyzers; buildtransitive 121 | all 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /SliceDetails/UI/NoteUI.cs: -------------------------------------------------------------------------------- 1 | using HMUI; 2 | using IPA.Utilities; 3 | using SliceDetails.Utils; 4 | using SliceDetails.Data; 5 | using System; 6 | using UnityEngine; 7 | using TMPro; 8 | using UnityEngine.UI; 9 | 10 | namespace SliceDetails.UI 11 | { 12 | 13 | internal class NoteUI : MonoBehaviour 14 | { 15 | private ImageView _directionArrowImage; 16 | private Transform _cutGroup; 17 | private ImageView _cutArrowImage; 18 | private ImageView _cutDistanceImage; 19 | private ImageView _backgroundImage; 20 | 21 | private Color _noteColor; 22 | private HoverHint _noteHoverHint; 23 | private HoverHintController _hoverHintController; 24 | private TextMeshProUGUI _hoverPanelTmpro; 25 | 26 | private float _noteRotation; 27 | 28 | public void Initialize(OrderedNoteCutDirection cutDirection, ColorType colorType, AssetLoader assetLoader) { 29 | transform.localScale = Vector3.one * 0.9f; 30 | 31 | _backgroundImage = GetComponent(); 32 | _directionArrowImage = transform.Find("NoteDirArrow").GetComponent(); 33 | _cutArrowImage = transform.Find("NoteCutArrow").GetComponent(); 34 | _cutDistanceImage = transform.Find("NoteCutDistance").GetComponent(); 35 | 36 | _cutGroup = new GameObject("NoteCutGroup").transform; 37 | _cutGroup.SetParent(_backgroundImage.transform); 38 | _cutGroup.localPosition = Vector3.zero; 39 | _cutGroup.localScale = Vector3.one; 40 | _cutGroup.localRotation = Quaternion.identity; 41 | _cutArrowImage.transform.SetParent(_cutGroup); 42 | _cutDistanceImage.transform.SetParent(_cutGroup); 43 | 44 | if (colorType == ColorType.ColorA) 45 | _noteColor = ColorSchemeManager.GetMainColorScheme().saberAColor; 46 | else if (colorType == ColorType.ColorB) 47 | _noteColor = ColorSchemeManager.GetMainColorScheme().saberBColor; 48 | 49 | _backgroundImage.color = _noteColor; 50 | _cutDistanceImage.color = new Color(0.0f, 1.0f, 0.0f, 0.75f); 51 | 52 | Texture2D square = new Texture2D(2, 2); 53 | square.filterMode = FilterMode.Point; 54 | square.Apply(); 55 | _cutDistanceImage.sprite = Sprite.Create(square, new Rect(0, 0, square.width, square.height), new Vector2(0, 0), 100); 56 | 57 | _noteHoverHint = _backgroundImage.gameObject.AddComponent(); 58 | _noteHoverHint.text = ""; 59 | 60 | 61 | switch (cutDirection) { 62 | case OrderedNoteCutDirection.Down: 63 | _noteRotation = 0.0f; 64 | break; 65 | case OrderedNoteCutDirection.Up: 66 | _noteRotation = 180.0f; 67 | break; 68 | case OrderedNoteCutDirection.Left: 69 | _noteRotation = 270.0f; 70 | break; 71 | case OrderedNoteCutDirection.Right: 72 | _noteRotation = 90.0f; 73 | break; 74 | case OrderedNoteCutDirection.DownLeft: 75 | _noteRotation = 315.0f; 76 | break; 77 | case OrderedNoteCutDirection.DownRight: 78 | _noteRotation = 45.0f; 79 | break; 80 | case OrderedNoteCutDirection.UpLeft: 81 | _noteRotation = 225.0f; 82 | break; 83 | case OrderedNoteCutDirection.UpRight: 84 | _noteRotation = 135.0f; 85 | break; 86 | case OrderedNoteCutDirection.Any: 87 | _noteRotation = 0.0f; 88 | _directionArrowImage.sprite = assetLoader.spr_dot; 89 | break; 90 | } 91 | 92 | transform.localRotation = Quaternion.Euler(new Vector3(0f, 0f, _noteRotation)); 93 | } 94 | 95 | public void SetHoverHintController(HoverHintController hoverHintController) { 96 | _hoverHintController = hoverHintController; 97 | HoverHintPanel hhp = _hoverHintController.GetField("_hoverHintPanel"); 98 | // Skew cringe skew cringe 99 | hhp.GetComponent().SetField("_skew", 0.0f); 100 | _hoverPanelTmpro = hhp.GetComponentInChildren(); 101 | _hoverPanelTmpro.fontStyle = FontStyles.Normal; 102 | _hoverPanelTmpro.alignment = TextAlignmentOptions.Left; 103 | _hoverPanelTmpro.overflowMode = TextOverflowModes.Overflow; 104 | _hoverPanelTmpro.enableWordWrapping = false; 105 | ContentSizeFitter csf = _hoverPanelTmpro.gameObject.AddComponent(); 106 | csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; 107 | } 108 | 109 | public void SetNoteData(float angle, float offset, Score score, int count) { 110 | _noteHoverHint.SetField("_hoverHintController", _hoverHintController); 111 | 112 | if (angle == 0f && offset == 0f) { 113 | _backgroundImage.color = Color.gray; 114 | _cutArrowImage.gameObject.SetActive(false); 115 | _cutDistanceImage.gameObject.SetActive(false); 116 | _directionArrowImage.color = new Color(0.8f, 0.8f, 0.8f); 117 | _noteHoverHint.text = ""; 118 | } else { 119 | _backgroundImage.color = _noteColor; 120 | _cutArrowImage.gameObject.SetActive(true); 121 | _cutDistanceImage.gameObject.SetActive(true); 122 | _cutGroup.transform.localRotation = Quaternion.Euler(new Vector3(0f, 0f, angle - _noteRotation - 90f)); 123 | if (Plugin.Settings.TrueCutOffsets) { 124 | _cutArrowImage.transform.localPosition = new Vector3(offset * 20.0f, 0f, 0f); 125 | _cutDistanceImage.transform.localScale = new Vector2(-offset * 1.33f, 1.0f); 126 | } else { 127 | _cutArrowImage.transform.localPosition = new Vector3(offset * (30.0f + score.Offset), 0f, 0f); 128 | _cutDistanceImage.transform.localScale = new Vector2(-offset * (1.995f + score.Offset*0.0665f), 1.0f); 129 | } 130 | _directionArrowImage.color = Color.white; 131 | string noteNotes = count == 1 ? "note" : "notes"; 132 | _noteHoverHint.text = "Average score (" + count + " " + noteNotes + ")\n" + String.Format("{0:0.00}", score.TotalScore) + "\nPre-swing - " + String.Format("{0:0.00}", score.PreSwing) + "\nPost-swing - " + String.Format("{0:0.00}", score.PostSwing) + "\nAccuracy - " + String.Format("{0:0.00}", score.Offset) + ""; 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SliceDetails/UI/GridViewController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using BeatSaberMarkupLanguage.Attributes; 5 | using BeatSaberMarkupLanguage.Components; 6 | using BeatSaberMarkupLanguage.ViewControllers; 7 | using HMUI; 8 | using IPA.Utilities; 9 | using SiraUtil.Logging; 10 | using TMPro; 11 | using UnityEngine; 12 | using UnityEngine.EventSystems; 13 | using Zenject; 14 | using SliceDetails.Data; 15 | using UnityEngine.SceneManagement; 16 | 17 | namespace SliceDetails.UI 18 | { 19 | public enum OrderedNoteCutDirection 20 | { 21 | UpLeft = 0, 22 | Up = 1, 23 | UpRight = 2, 24 | Left = 3, 25 | Any = 4, 26 | Right = 5, 27 | DownLeft = 6, 28 | Down = 7, 29 | DownRight = 8, 30 | None = 9 31 | } 32 | 33 | [HotReload(RelativePathToLayout = @"Views\gridView.bsml")] 34 | [ViewDefinition("SliceDetails.UI.Views.gridView.bsml")] 35 | internal class GridViewController : BSMLAutomaticViewController { 36 | 37 | private SiraLog _siraLog; 38 | private AssetLoader _assetLoader; 39 | private HoverHintControllerHandler _hoverHintControllerHandler; 40 | private SliceProcessor _sliceProcessor; 41 | private DiContainer _diContainer; 42 | 43 | [UIObject("tile-grid")] 44 | private readonly GameObject _tileGrid; 45 | [UIObject("tile-row")] 46 | private readonly GameObject _tileRow; 47 | [UIComponent("tile")] 48 | private readonly ClickableImage _tile; 49 | 50 | [UIObject("note-modal")] 51 | private readonly GameObject _noteModal; 52 | [UIObject("note-horizontal")] 53 | private readonly GameObject _noteHorizontal; 54 | [UIObject("note-grid")] 55 | private GameObject _noteGrid; 56 | [UIObject("note-row")] 57 | private GameObject _noteRow; 58 | 59 | [UIComponent("note")] 60 | private readonly ImageView _note; 61 | [UIComponent("note-dir-arrow")] 62 | private readonly ImageView _noteDirArrow; 63 | [UIComponent("note-cut-arrow")] 64 | private readonly ImageView _noteCutArrow; 65 | [UIComponent("note-cut-distance")] 66 | private readonly ImageView _noteCutDistance; 67 | [UIComponent("sd-version")] 68 | private readonly TextMeshProUGUI _sdVersionText; 69 | [UIComponent("reset-button")] 70 | private readonly RectTransform _resetButtonTransform; 71 | 72 | private List _tiles = new List(); 73 | private List _notes = new List(); 74 | private SelectedTileIndicator _selectedTileIndicator; 75 | private BasicUIAudioManager _basicUIAudioManager; 76 | 77 | 78 | [Inject] 79 | internal void Construct(SiraLog siraLog, AssetLoader assetLoader, HoverHintControllerHandler hoverHintControllerHandler, SliceProcessor sliceProcessor, DiContainer diContainer) { 80 | _siraLog = siraLog; 81 | _assetLoader = assetLoader; 82 | _hoverHintControllerHandler = hoverHintControllerHandler; 83 | _sliceProcessor = sliceProcessor; 84 | _diContainer = diContainer; 85 | _siraLog.Debug("GridViewController Constructed"); 86 | } 87 | 88 | [UIAction("#post-parse")] 89 | public void PostParse() { 90 | _noteDirArrow.gameObject.name = "NoteDirArrow"; 91 | _noteCutArrow.gameObject.name = "NoteCutArrow"; 92 | _noteCutDistance.gameObject.name = "NoteCutDistance"; 93 | 94 | _sdVersionText.text = $"SliceDetails v{ System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(3) }"; 95 | ReflectionUtil.InvokeMethod(_sdVersionText, "Awake"); // For some reason this is necessary 96 | _sdVersionText.rectTransform.sizeDelta = new Vector2(40.0f, 10.0f); 97 | _sdVersionText.transform.localPosition = new Vector3(0.0f, -17.0f, 0.0f); 98 | 99 | if (SceneManager.GetActiveScene().name == "MainMenu") { 100 | Destroy(_resetButtonTransform.gameObject); 101 | } else { 102 | _resetButtonTransform.sizeDelta = new Vector2(8.0f, 4.0f); 103 | _resetButtonTransform.localPosition = new Vector3(-15.0f, -17.0f, 0.0f); 104 | _resetButtonTransform.GetComponentInChildren().fontStyle = FontStyles.Normal; 105 | foreach (ImageView iv in _resetButtonTransform.GetComponentsInChildren()) { 106 | iv.SetField("_skew", 0.0f); 107 | iv.transform.localPosition = Vector3.zero; 108 | } 109 | } 110 | 111 | _tiles = new List(); 112 | // Create first row of tiles 113 | for (int i = 0; i < 4; i++) { 114 | ClickableImage tileInstance = Instantiate(_tile.gameObject, _tileRow.transform).GetComponent(); 115 | _tiles.Add(tileInstance); 116 | } 117 | 118 | // Create other 2 rows of tiles 119 | for (int i = 0; i < 2; i++) { 120 | GameObject tileRowInstance = Instantiate(_tileRow, _tileGrid.transform); 121 | tileRowInstance.transform.SetAsFirstSibling(); 122 | _tiles.AddRange(tileRowInstance.GetComponentsInChildren()); 123 | } 124 | 125 | // Set tile click events and data 126 | for (int i = 0; i < _tiles.Count; i++) { 127 | _tiles[i].OnClickEvent = _tile.OnClickEvent; 128 | _tiles[i].OnClickEvent += SetNotesData; 129 | _tiles[i].DefaultColor = _tile.DefaultColor; 130 | _tiles[i].HighlightColor = _tile.HighlightColor; 131 | } 132 | 133 | Transform noteParent = _noteRow.transform; 134 | Transform rowParent = _noteGrid.transform; 135 | _notes = new List(); 136 | HoverHintController currentHoverHintController = _hoverHintControllerHandler.hoverHintController; 137 | for (int i = 0; i < 18; i++) { 138 | if (i % 9 == 0) { 139 | rowParent = Instantiate(_noteGrid, _noteHorizontal.transform).transform; 140 | } 141 | if (i % 3 == 0) { 142 | noteParent = Instantiate(_noteRow, rowParent).transform; 143 | } 144 | 145 | ColorType colorType = (ColorType)(i >= 9 ? 1 : 0); 146 | OrderedNoteCutDirection cutDirection = (OrderedNoteCutDirection)(i % 9); 147 | NoteUI uiNote = Instantiate(_note.gameObject, noteParent).AddComponent(); 148 | uiNote.Initialize(cutDirection, colorType, _assetLoader); 149 | uiNote.SetHoverHintController(currentHoverHintController); 150 | 151 | _notes.Add(uiNote); 152 | } 153 | 154 | _selectedTileIndicator = new GameObject("SelectedTileIndicator").AddComponent(); 155 | _selectedTileIndicator.Initialize(_assetLoader); 156 | _selectedTileIndicator.transform.SetParent(_noteModal.transform, false); 157 | _selectedTileIndicator.transform.localPosition = new Vector3(0f, 30f, 0f); 158 | 159 | _basicUIAudioManager = Resources.FindObjectsOfTypeAll().First(x => x.GetComponent().enabled && x.isActiveAndEnabled); 160 | 161 | DestroyImmediate(_note.gameObject); 162 | DestroyImmediate(_noteRow); 163 | DestroyImmediate(_noteGrid); 164 | DestroyImmediate(_tile.gameObject); 165 | } 166 | 167 | public void SetTileScores() { 168 | for (int i = 0; i < _tiles.Count; i++) { 169 | FormattableText[] texts = _tiles[i].transform.GetComponentsInChildren(true); 170 | 171 | if(Plugin.Settings.ShowSliceCounts) { 172 | texts[0].transform.localPosition = new Vector3(0.0f, 0.75f, 0.0f); 173 | texts[1].transform.localPosition = new Vector3(0.0f, -1.5f, 0.0f); 174 | texts[1].gameObject.SetActive(true); 175 | } else { 176 | texts[1].gameObject.SetActive(false); 177 | } 178 | 179 | if (_sliceProcessor.tiles[i].atLeastOneNote) { 180 | texts[0].text = String.Format("{0:0.00}", _sliceProcessor.tiles[i].scoreAverage); 181 | texts[1].text = _sliceProcessor.tiles[i].noteCount.ToString(); 182 | } else { 183 | texts[0].text = ""; 184 | texts[1].text = ""; 185 | } 186 | } 187 | } 188 | 189 | private void SetNotesData(PointerEventData eventData) { 190 | int tileIndex = _tiles.IndexOf(eventData.pointerPress.GetComponent()); 191 | _selectedTileIndicator.SetSelectedTile(tileIndex); 192 | Tile tile = _sliceProcessor.tiles[tileIndex]; 193 | for (int i = 0; i < _notes.Count; i++) { 194 | float angle = tile.angleAverages[i]; 195 | float offset = tile.offsetAverages[i]; 196 | Score score = tile.scoreAverages[i]; 197 | int count = tile.noteCounts[i]; 198 | 199 | _notes[i].SetNoteData(angle, offset, score, count); 200 | } 201 | } 202 | 203 | [UIAction("#presentNotesModal")] 204 | public void PresentModal() { 205 | if (_basicUIAudioManager != null) 206 | _basicUIAudioManager.HandleButtonClickEvent(); 207 | } 208 | 209 | public void CloseModal(bool animated) { 210 | _noteModal.GetComponent().Hide(animated); 211 | } 212 | 213 | public void UpdateUINotesHoverHintController() { 214 | HoverHintController currentHoverHintController = _hoverHintControllerHandler.hoverHintController; 215 | for (int i = 0; i < _notes.Count; i++) { 216 | _notes[i].SetHoverHintController(currentHoverHintController); 217 | } 218 | } 219 | 220 | [UIAction("resetRecorder")] 221 | public void ResetRecorder() { 222 | SliceRecorder sliceRecorder = _diContainer.TryResolve(); 223 | sliceRecorder?.ClearSlices(); 224 | SetTileScores(); 225 | } 226 | } 227 | } 228 | --------------------------------------------------------------------------------