├── .gitignore ├── Assets ├── Arrow.png ├── RRect.png ├── White.png ├── Circle.png ├── RRectMask.png ├── svg2png.bat ├── Arrow.svg ├── Circle.svg ├── rrect.svg └── RRectMask.svg ├── Models ├── ScoreScalingMode.cs └── ScoreScalingModeExt.cs ├── SliceVisualizer.csproj.user.example ├── Views └── Main.bsml ├── manifest.json ├── Configuration ├── FloatConverter.cs ├── ColorConverter.cs └── PluginConfig.cs ├── Installers ├── NsvGameInstaller.cs └── NsvAppInstaller.cs ├── Factories └── NsvBlockFactory.cs ├── Directory.Build.props ├── ViewControllers └── MainViewController.cs ├── UI ├── SettingsUI.cs └── SliceVisualizerFlowCoordinator.cs ├── LICENSE ├── SliceVisualizer.sln ├── Plugin.cs ├── NsvAssetLoader.cs ├── Core ├── NsvController.cs └── NsvSlicedBlock.cs ├── README.md ├── SliceVisualizer.csproj ├── .editorconfig └── Directory.Build.targets /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | *.user 5 | 6 | # JetBrains Rider 7 | .idea/ 8 | *.sln.iml -------------------------------------------------------------------------------- /Assets/Arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1el/BeatSaber-SliceVisualizer/HEAD/Assets/Arrow.png -------------------------------------------------------------------------------- /Assets/RRect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1el/BeatSaber-SliceVisualizer/HEAD/Assets/RRect.png -------------------------------------------------------------------------------- /Assets/White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1el/BeatSaber-SliceVisualizer/HEAD/Assets/White.png -------------------------------------------------------------------------------- /Assets/Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1el/BeatSaber-SliceVisualizer/HEAD/Assets/Circle.png -------------------------------------------------------------------------------- /Assets/RRectMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1el/BeatSaber-SliceVisualizer/HEAD/Assets/RRectMask.png -------------------------------------------------------------------------------- /Assets/svg2png.bat: -------------------------------------------------------------------------------- 1 | imagick -background none RRect.svg RRect.png 2 | imagick -background none Arrow.svg Arrow.png 3 | imagick -background none Circle.svg Circle.png 4 | -------------------------------------------------------------------------------- /Models/ScoreScalingMode.cs: -------------------------------------------------------------------------------- 1 | namespace SliceVisualizer.Models 2 | { 3 | public enum ScoreScalingMode 4 | { 5 | Linear, 6 | Sqrt, 7 | Log 8 | } 9 | } -------------------------------------------------------------------------------- /Assets/Arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Assets/Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Assets/rrect.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /SliceVisualizer.csproj.user.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | C:\Program Files (x86)\Steam\steamapps\common\Beat Saber 6 | 7 | 8 | -------------------------------------------------------------------------------- /Assets/RRectMask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /Views/Main.bsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/beat-saber-modding-group/BSIPA-MetadataFileSchema/master/Schema.json", 3 | "id": "SliceVisualizer", 4 | "name": "SliceVisualizer", 5 | "author": "Igor null ", 6 | "version": "0.1.3", 7 | "description": "Shows your cut data to train your accuracy", 8 | "gameVersion": "1.13.4", 9 | "dependsOn": { 10 | "BeatSaberMarkupLanguage": "^1.5.2", 11 | "BSIPA": "^4.1.6", 12 | "SiraUtil": "^2.5.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Configuration/FloatConverter.cs: -------------------------------------------------------------------------------- 1 | using IPA.Config.Data; 2 | 3 | namespace SliceVisualizer.Configuration 4 | { 5 | internal static class FloatConverter 6 | { 7 | public static float ValueToFloat(Value val) 8 | { 9 | return val switch 10 | { 11 | FloatingPoint point => (float) point.Value, 12 | Integer integer => integer.Value, 13 | _ => throw new System.ArgumentException("List element was not a number"), 14 | }; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Installers/NsvGameInstaller.cs: -------------------------------------------------------------------------------- 1 | using SliceVisualizer.Configuration; 2 | using SliceVisualizer.Core; 3 | using SliceVisualizer.Factories; 4 | using Zenject; 5 | 6 | namespace SliceVisualizer.Installers 7 | { 8 | internal class NsvGameInstaller : Installer 9 | { 10 | public NsvGameInstaller() 11 | { 12 | } 13 | 14 | public override void InstallBindings() 15 | { 16 | Container.BindFactory().AsSingle(); 17 | Container.BindInterfacesAndSelfTo().AsSingle(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Factories/NsvBlockFactory.cs: -------------------------------------------------------------------------------- 1 | using SliceVisualizer.Core; 2 | using Zenject; 3 | 4 | namespace SliceVisualizer.Factories 5 | { 6 | internal class NsvBlockFactory : PlaceholderFactory 7 | { 8 | private readonly DiContainer _container; 9 | 10 | public NsvBlockFactory(DiContainer container) 11 | { 12 | _container = container; 13 | } 14 | 15 | public override NsvSlicedBlock Create() 16 | { 17 | var slicedBlock = _container.InstantiateComponentOnNewGameObject(); 18 | slicedBlock.gameObject.name = $"{nameof(NsvSlicedBlock)} (Factorized)"; 19 | 20 | return slicedBlock; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | true 9 | true 10 | true 11 | 12 | 13 | false 14 | true 15 | true 16 | 17 | -------------------------------------------------------------------------------- /ViewControllers/MainViewController.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage.Attributes; 2 | using BeatSaberMarkupLanguage.ViewControllers; 3 | using SliceVisualizer.Configuration; 4 | 5 | namespace SliceVisualizer.ViewControllers 6 | { 7 | internal class MainViewController : BSMLResourceViewController 8 | { 9 | public void Activated() 10 | { 11 | NotifyPropertyChanged("EnabledValue"); 12 | } 13 | [UIValue("Enabled-value")] 14 | public bool EnabledValue 15 | { 16 | get => PluginConfig.Instance.Enabled; 17 | set 18 | { 19 | PluginConfig.Instance.Enabled = value; 20 | } 21 | } 22 | 23 | public override string ResourceName => "SliceVisualizer.Views.Main.bsml"; 24 | } 25 | } -------------------------------------------------------------------------------- /Installers/NsvAppInstaller.cs: -------------------------------------------------------------------------------- 1 | using IPA.Logging; 2 | using SiraUtil; 3 | using SliceVisualizer.Configuration; 4 | using SliceVisualizer.UI; 5 | using Zenject; 6 | 7 | namespace SliceVisualizer.Installers 8 | { 9 | internal class NsvAppInstaller : Installer 10 | { 11 | private readonly Logger _logger; 12 | private readonly PluginConfig _config; 13 | 14 | public NsvAppInstaller(Logger logger, PluginConfig config) 15 | { 16 | _logger = logger; 17 | _config = config; 18 | PluginConfig.Instance = config; 19 | SettingsUI.CreateMenu(); 20 | } 21 | 22 | public override void InstallBindings() 23 | { 24 | Container.BindLoggerAsSiraLogger(_logger); 25 | 26 | Container.BindInstance(_config).AsSingle(); 27 | 28 | Container.BindInterfacesAndSelfTo().AsSingle().Lazy(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Models/ScoreScalingModeExt.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | 4 | namespace SliceVisualizer.Models 5 | { 6 | public static class ScoreScalingModeExt 7 | { 8 | public static float ApplyScaling(this ScoreScalingMode mode, float offset, float min, float max) 9 | { 10 | Func transform = mode switch 11 | { 12 | ScoreScalingMode.Linear => (x => x), 13 | ScoreScalingMode.Log => Mathf.Log, 14 | ScoreScalingMode.Sqrt => Mathf.Sqrt, 15 | _ => (x => x), 16 | }; 17 | 18 | var sign = Mathf.Sign(offset); 19 | offset = Mathf.Abs(offset); 20 | 21 | if (offset < min) 22 | { 23 | return 0.0f; 24 | } 25 | 26 | var tMin = transform(min); 27 | var tMax = transform(max); 28 | offset = (transform(offset) - tMin) / (tMax - tMin); 29 | 30 | return Mathf.Clamp(sign * offset, -0.5f, 0.5f); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /UI/SettingsUI.cs: -------------------------------------------------------------------------------- 1 | using BeatSaberMarkupLanguage; 2 | using BeatSaberMarkupLanguage.MenuButtons; 3 | 4 | namespace SliceVisualizer.UI 5 | { 6 | internal class SettingsUI 7 | { 8 | public static SliceVisualizerFlowCoordinator? SliceVisualizerFlowCoordinator; 9 | public static bool Created; 10 | 11 | public static void CreateMenu() 12 | { 13 | if (!Created) 14 | { 15 | var menuButton = new MenuButton("SliceVisualizer", "Chase the perfect slice", ShowFlow); 16 | MenuButtons.instance.RegisterButton(menuButton); 17 | Created = true; 18 | } 19 | } 20 | 21 | public static void ShowFlow() 22 | { 23 | if (SliceVisualizerFlowCoordinator == null) 24 | { 25 | SliceVisualizerFlowCoordinator = BeatSaberUI.CreateFlowCoordinator(); 26 | } 27 | BeatSaberUI.MainFlowCoordinator.PresentFlowCoordinator(SliceVisualizerFlowCoordinator); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Igor null 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SliceVisualizer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SliceVisualizer", "SliceVisualizer.csproj", "{16235F7D-5042-4185-8EC7-E555DA68A2F8}" 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 | {16235F7D-5042-4185-8EC7-E555DA68A2F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {16235F7D-5042-4185-8EC7-E555DA68A2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {16235F7D-5042-4185-8EC7-E555DA68A2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {16235F7D-5042-4185-8EC7-E555DA68A2F8}.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 = {E889DCF9-0744-4AB0-9C59-2A866F1E774B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Plugin.cs: -------------------------------------------------------------------------------- 1 | using IPA; 2 | using IPA.Config; 3 | using IPA.Config.Stores; 4 | using IPA.Logging; 5 | using SiraUtil.Zenject; 6 | using SliceVisualizer.Configuration; 7 | using SliceVisualizer.Installers; 8 | 9 | 10 | namespace SliceVisualizer 11 | { 12 | [Plugin(RuntimeOptions.DynamicInit)] 13 | public class Plugin 14 | { 15 | internal static Logger Log = null!; 16 | /// 17 | /// Called when the plugin is first loaded by IPA (either when the game starts or when the plugin is enabled if it starts disabled). 18 | /// [Init] methods that use a Constructor or called before regular methods like InitWithConfig. 19 | /// Only use [Init] with one Constructor. 20 | /// 21 | [Init] 22 | public void Init(Logger logger, Config conf, Zenjector zenject) 23 | { 24 | Log = logger; 25 | zenject.OnApp().WithParameters(logger, conf.Generated()); 26 | zenject.OnGame(false).ShortCircuitForTutorial(); 27 | } 28 | 29 | [OnEnable, OnDisable] 30 | public void OnStateChanged() 31 | { 32 | // Zenject is poggers 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Configuration/ColorConverter.cs: -------------------------------------------------------------------------------- 1 | using IPA.Config.Data; 2 | using IPA.Config.Stores; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace SliceVisualizer.Configuration 7 | { 8 | internal class ColorConverter : ValueConverter 9 | { 10 | public override Color FromValue(Value value, object parent) 11 | { 12 | switch (value) 13 | { 14 | case List list: 15 | { 16 | var array = list.Select(FloatConverter.ValueToFloat).ToArray(); 17 | return new Color(array[0], array[1], array[2], array[3]); 18 | } 19 | 20 | case Text text: 21 | { 22 | return ColorUtility.TryParseHtmlString(text.Value, out var color) ? color : Color.white; 23 | } 24 | 25 | default: 26 | throw new System.ArgumentException("Color deserializer expects either string or array"); 27 | } 28 | } 29 | 30 | public override Value ToValue(Color obj, object parent) 31 | { 32 | var array = new[] { obj.r, obj.g, obj.b, obj.a }; 33 | return Value.From(array.Select(x => Value.Float((decimal)x))); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UI/SliceVisualizerFlowCoordinator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BeatSaberMarkupLanguage; 3 | using HMUI; 4 | using SliceVisualizer.ViewControllers; 5 | 6 | namespace SliceVisualizer.UI 7 | { 8 | internal class SliceVisualizerFlowCoordinator : FlowCoordinator 9 | { 10 | private MainViewController? _mainViewController; 11 | // private BindingsViewController _bindingsViewController; 12 | // private MiscViewController _miscViewController; 13 | // private ThresholdViewController _thresholdViewController; 14 | 15 | public void Awake() 16 | { 17 | if (!_mainViewController) 18 | { 19 | _mainViewController = BeatSaberUI.CreateViewController(); 20 | } 21 | } 22 | 23 | protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) 24 | { 25 | try 26 | { 27 | if (firstActivation) 28 | { 29 | SetTitle("SliceVisualizer settings"); 30 | showBackButton = true; 31 | ProvideInitialViewControllers(_mainViewController); 32 | } 33 | _mainViewController?.Activated(); 34 | } 35 | catch (Exception e) 36 | { 37 | Plugin.Log.Error(e); 38 | } 39 | } 40 | 41 | protected override void BackButtonWasPressed(ViewController topViewController) 42 | { 43 | BeatSaberUI.MainFlowCoordinator.DismissFlowCoordinator(this); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Configuration/PluginConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using IPA.Config.Stores; 3 | using IPA.Config.Stores.Attributes; 4 | using IPA.Config.Stores.Converters; 5 | using SiraUtil.Converters; 6 | using UnityEngine; 7 | using SliceVisualizer.Models; 8 | 9 | [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)] 10 | namespace SliceVisualizer.Configuration 11 | { 12 | internal class PluginConfig 13 | { 14 | internal static PluginConfig Instance = null!; 15 | public virtual bool Enabled { get; set; } = true; 16 | public virtual float SliceWidth { get; set; } = 0.05f; 17 | 18 | [UseConverter(typeof(EnumConverter))] 19 | public virtual ScoreScalingMode ScoreScaling { get; set; } = ScoreScalingMode.Linear; 20 | public virtual float ScoreScaleMin { get; set; } = 0.05f; 21 | public virtual float ScoreScaleMax { get; set; } = 0.5f; 22 | public virtual float ScoreScale { get; set; } = 1.0f; 23 | [UseConverter(typeof(ColorConverter))] 24 | public virtual Color MissedAreaColor { get; set; } = new Color(0f, 0f, 0f, 0.5f); 25 | [UseConverter(typeof(ColorConverter))] 26 | public virtual Color SliceColor { get; set; } = new Color(1f, 1f, 1f, 1f); 27 | [UseConverter(typeof(ColorConverter))] 28 | public virtual Color ArrowColor { get; set; } = new Color(1f, 1f, 1f, 1f); 29 | [UseConverter(typeof(ColorConverter))] 30 | public virtual Color BadDirectionColor { get; set; } = new Color(0f, 0f, 0f, 1f); 31 | [UseConverter(typeof(ColorConverter))] 32 | public virtual Color CenterColor { get; set; } = new Color(1f, 1f, 1f, 1f); 33 | public virtual bool UseCustomColors { get; set; } = false; 34 | [UseConverter(typeof(ColorConverter))] 35 | public virtual Color LeftColor { get; set; } = new Color(0.659f, 0.125f, 0.125f, 1f); 36 | [UseConverter(typeof(ColorConverter))] 37 | public virtual Color RightColor { get; set; } = new Color(0.125f, 0.392f, 0.659f, 1f); 38 | public virtual float CubeLifetime { get; set; } = 1f; 39 | public virtual float PopEnd { get; set; } = 0.1f; 40 | public virtual float FadeStart { get; set; } = 0.5f; 41 | public virtual float PopDistance { get; set; } = 0.5f; 42 | public virtual bool PositionFromCubeTransform { get; set; } = true; 43 | public virtual bool RotationFromCubeTransform { get; set; } = true; 44 | public virtual float CubeScale { get; set; } = 0.9f; 45 | public virtual float CenterScale { get; set; } = 0.2f; 46 | public virtual float ArrowScale { get; set; } = 0.6f; 47 | public virtual float UIOpacity { get; set; } = 1.0f; 48 | [UseConverter(typeof(Vector3Converter))] 49 | public virtual Vector3 CanvasOffset { get; set; } = new Vector3(0f, 0f, 16f); 50 | public virtual float CanvasScale { get; set; } = 1f; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NsvAssetLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using SiraUtil.Tools; 5 | using UnityEngine; 6 | using Zenject; 7 | 8 | namespace SliceVisualizer 9 | { 10 | internal class NsvAssetLoader : IInitializable, IDisposable 11 | { 12 | private readonly SiraLog _siraLog; 13 | 14 | private Material? _uiNoGlowMaterial; 15 | private bool _loggedNoGlowMaterial = false; 16 | 17 | public Material? UINoGlowMaterial { 18 | get { 19 | if (!(_uiNoGlowMaterial is null)) 20 | { 21 | return _uiNoGlowMaterial; 22 | } 23 | var sprite = Resources.FindObjectsOfTypeAll().FirstOrDefault(m => m.name == "GameUISprite"); 24 | if (sprite is null) 25 | { 26 | if (!_loggedNoGlowMaterial) 27 | { 28 | _loggedNoGlowMaterial = true; 29 | _siraLog.Error("Trying to get GameUISprite before it was loaded. This should not happen."); 30 | } 31 | return null; 32 | } 33 | 34 | _uiNoGlowMaterial = new Material(sprite); 35 | return _uiNoGlowMaterial; 36 | } 37 | } 38 | 39 | public Sprite? RRect { get; private set; } 40 | public Sprite? Circle { get; private set; } 41 | public Sprite? Arrow { get; private set; } 42 | public Sprite? White { get; private set; } 43 | 44 | public NsvAssetLoader(SiraLog siraLog) 45 | { 46 | _siraLog = siraLog; 47 | } 48 | 49 | public void Initialize() 50 | { 51 | var assembly = Assembly.GetExecutingAssembly(); 52 | RRect = LoadSpriteFromResources(assembly, "SliceVisualizer.Assets.RRect.png"); 53 | Circle = LoadSpriteFromResources(assembly, "SliceVisualizer.Assets.Circle.png"); 54 | Arrow = LoadSpriteFromResources(assembly, "SliceVisualizer.Assets.Arrow.png"); 55 | White = LoadSpriteFromResources(assembly, "SliceVisualizer.Assets.White.png", 1f); 56 | } 57 | 58 | public void Dispose() 59 | { 60 | _uiNoGlowMaterial = null; 61 | 62 | RRect = null; 63 | Circle = null; 64 | Arrow = null; 65 | White = null; 66 | } 67 | 68 | private Sprite? LoadSpriteFromResources(Assembly assembly, string resourcePath, float pixelsPerUnit = 256.0f) 69 | { 70 | using var stream = assembly.GetManifestResourceStream(resourcePath); 71 | if (stream == null) 72 | { 73 | _siraLog.Warning($"Couldn't find embedded resource {resourcePath}"); 74 | return null; 75 | } 76 | 77 | byte[] imageData = new byte[stream.Length]; 78 | stream.Read(imageData, 0, (int) stream.Length); 79 | if (imageData.Length == 0) 80 | { 81 | return null; 82 | } 83 | 84 | var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false, false); 85 | texture.LoadImage(imageData); 86 | 87 | var rect = new Rect(0, 0, texture.width, texture.height); 88 | var sprite = Sprite.Create(texture, rect, Vector2.zero, pixelsPerUnit); 89 | 90 | _siraLog.Info($"Successfully loaded sprite {resourcePath}, w={texture.width}, h={texture.height}"); 91 | 92 | return sprite; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Core/NsvController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SliceVisualizer.Configuration; 3 | using UnityEngine; 4 | using UnityEngine.SceneManagement; 5 | using UnityEngine.UI; 6 | using Zenject; 7 | using HMUI; 8 | using BeatSaberMarkupLanguage.Tags; 9 | using BeatSaberMarkupLanguage.Components.Settings; 10 | using SiraUtil.Tools; 11 | 12 | namespace SliceVisualizer.Core 13 | { 14 | internal class NsvController : IInitializable, ITickable, IDisposable 15 | { 16 | private readonly PluginConfig _config; 17 | private readonly SiraLog _logger; 18 | private readonly BeatmapObjectManager _beatmapObjectManager; 19 | private readonly NsvSlicedBlock[] _slicedBlockPool; 20 | private readonly Factories.NsvBlockFactory _blockFactory; 21 | 22 | private GameObject _canvasGO = null!; 23 | 24 | private static readonly int MaxItems = 12; 25 | 26 | public NsvController(BeatmapObjectManager beatmapObjectManager, Factories.NsvBlockFactory blockFactory, SiraLog logger) 27 | { 28 | _config = PluginConfig.Instance; 29 | _logger = logger; 30 | _beatmapObjectManager = beatmapObjectManager; 31 | _slicedBlockPool = new NsvSlicedBlock[MaxItems]; 32 | _blockFactory = blockFactory; 33 | } 34 | 35 | public void Initialize() 36 | { 37 | _canvasGO = new GameObject("SliceVisualizerCanvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); 38 | var canvas = _canvasGO.GetComponent(); 39 | canvas.renderMode = RenderMode.WorldSpace; 40 | _canvasGO.transform.localScale = Vector3.one * _config.CanvasScale; 41 | _canvasGO.transform.localPosition = _config.CanvasOffset; 42 | _beatmapObjectManager.noteWasCutEvent += OnNoteCut; 43 | for (var i = 0; i < MaxItems; i++) 44 | { 45 | _slicedBlockPool[i] = _blockFactory.Create(); 46 | _slicedBlockPool[i].Init(_canvasGO.transform); 47 | } 48 | 49 | try 50 | { 51 | CreateCheckbox(); 52 | } 53 | catch (Exception err) 54 | { 55 | _logger.Info(string.Format("Cannot create checkbox: {0}", err)); 56 | } 57 | } 58 | 59 | public void Tick() 60 | { 61 | var delta = Time.deltaTime; 62 | foreach (var slicedBlock in _slicedBlockPool) 63 | { 64 | if (!(slicedBlock is null) && slicedBlock.isActive) 65 | { 66 | slicedBlock.ExternalUpdate(delta); 67 | } 68 | } 69 | } 70 | 71 | public void Dispose() 72 | { 73 | _beatmapObjectManager.noteWasCutEvent -= OnNoteCut; 74 | 75 | foreach (var slicedBlock in _slicedBlockPool) 76 | { 77 | if (slicedBlock is null) { continue; } 78 | slicedBlock.Dispose(); 79 | } 80 | } 81 | 82 | private void OnNoteCut(NoteController noteController, in NoteCutInfo noteCutInfo) 83 | { 84 | if (!_config.Enabled) 85 | { 86 | return; 87 | } 88 | 89 | // Re-use cubes at the same column & layer to avoid UI cluttering 90 | var noteData = noteController.noteData; 91 | // doing the modulus twice is required for negative indices 92 | var lineLayer = (((int) noteController.noteData.noteLineLayer % 3) + 3) % 3; 93 | var lineIndex = ((noteController.noteData.lineIndex % 4) + 4) % 4; 94 | var blockIndex = lineIndex + 4 * lineLayer; 95 | var slicedBlock = _slicedBlockPool[blockIndex]; 96 | if (slicedBlock is null) { return; } 97 | slicedBlock.SetData(noteController, noteCutInfo, noteData); 98 | } 99 | 100 | public void CreateCheckbox() 101 | { 102 | var canvas = GameObject.Find("Wrapper/StandardGameplay/PauseMenu/Wrapper/MenuWrapper/Canvas").GetComponent(); 103 | if (!canvas) 104 | { 105 | return; 106 | } 107 | 108 | var toggleObject = new ToggleSettingTag().CreateObject(canvas.transform); 109 | toggleObject.GetComponentInChildren().text = "SliceVisualizer"; 110 | 111 | if (!(toggleObject.transform is RectTransform toggleObjectTransform)) 112 | { 113 | return; 114 | } 115 | 116 | toggleObjectTransform.anchoredPosition = new Vector2(27, -7); 117 | toggleObjectTransform.sizeDelta = new Vector2(-130, 7); 118 | 119 | var toggleSetting = toggleObject.GetComponent(); 120 | toggleSetting.Value = _config.Enabled; 121 | toggleSetting.toggle.onValueChanged.AddListener(enabled => _config.Enabled = enabled); 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Beat Saber Slice Visualizer mod 2 | 3 | This plugin shows how your slice is offset from center. 4 | Using that information, you can train your accuracy. 5 | 6 | Demo: https://www.youtube.com/watch?v=mrq-uZ2JnXg 7 | 8 | ## Installation 9 | 10 | - Install BSIPA and SiraUtil, recommended way is to use [Beat Saber Mod Assistant][mod_assistant] 11 | - Copy SliceVisualizer.dll to `Beat Saber\Plugins` directory. 12 | 13 | ## Configuration 14 | 15 | Edit `Beat Saber\UserData\SliceVizualizer.json`. 16 | This file will be created automatically when you first launch the game with the plugin installed. 17 | 18 | ### Configuration options: 19 | - `SliceWidth` (default: 0.05) -- Width of the slice line as a proportion of note size 20 | - `ScoreScaling` (default: `"Linear"`) -- Specify scaling function for cut offset. Possible values: `"Linear", "Log", "Sqrt"`. 21 | This option, when set to `Log` or `Sqrt` allows you to exaggerate small offsets, and group large offsets together. 22 | - `ScoreScaleMin` (default: 0.05) -- Minumum value for the scaling. When the offset is smaller than this value, it is rendered as zero. 23 | - `ScoreScaleMax` (default: 0.5) -- Maximum value for scaling. When the cut offset is 1, the final render will equal this value. 24 | - `ScoreScale` (default: 1.0) -- Multiplier for cut offset. Set to bigget value to exaggerate offset. 25 | - `MissedAreaColor` (default: `[0.0, 0.0, 0.0, 0.5]`) -- RGBA color for the area between note center and cut line. 26 | - `SliceColor` (default: `[1.0, 1.0, 1.0, 1.0]`) -- RGBA color for the cut line. 27 | - `ArrowColor` (default: `[1.0, 1.0, 1.0, 1.0]`) -- RGBA color for the arrow image. 28 | - `BadDirectionColor` (default: `[0.0, 0.0, 0.0, 1.0]`) -- RGBA color for the arrow, in case you cut the note from the wrong side. 29 | - `CenterColor` (default: `[1.0, 1.0, 1.0, 1.0]`) -- RGBA color for the circle in the center of note. 30 | - `UseCustomColors` (default: `false`) -- override the in-game colors for notes with the following: 31 | - `LeftColor` (default: `[0.659, 0.125, 0.125, 1]`) -- Color for left notes. 32 | - `RightColor` (default: `[0.125, 0.392, 0.659, 1]`) -- Color for right notes. 33 | - `CubeLifetime` (default: 1.0) -- total lifetime of the visualization in seconds. 34 | - `PopEnd` (default: 0.1) -- highlight time for the visualization in seconds. set to 0.0 to disable highlighting. 35 | - `FadeStart` (default: 0.5) -- the time at which the visualization starts to fade out in seconds. 36 | - `PopDistance` (default: 0.5) -- the visualization starts at a position close to the player, and moves away from the player for this distance. 37 | Set to 0.0 to disable this effect. 38 | - `PositionFromCubeTransform` (default: true) -- use render position of the note to calculate position of the visualiztion. 39 | - `RotationFromCubeTransform` (default: true) -- use render rotation of the note to calculate note direction for the visualiztion. 40 | - `CubeScale` (default: 0.9) -- note size for the visualization. 41 | - `CenterScale` (default: 0.2) -- scale of the circle in the center, relative to note scale. 42 | - `ArrowScale` (default: 0.6) -- scale of the note arrow, relative to note scale. 43 | - `UIOpacity` (default: 1.0) -- Overall opacity for the UI. Multiplicative with opacity/alpha of other colors. 44 | - `CanvasOffset` (default: `[0.0, 0.0, 16.0]`) -- control `[x, y, z]` position of bottom center point of the visualization canvas. 45 | `x` is left to right, `y` is bottom to top, `z` is back to front. 46 | - `CanvasScale` (default: 1.0) -- Control visualization canvas scaling. This affects note scaling as well. 47 | 48 | ## Setting up Development 49 | 50 | Please read the [BSMT Wiki][bsmt_wiki] for more information on plugin development for BeatSaber. 51 | 52 | In case you have troubles with setting up paths or references, please read: 53 | [BSMT Wiki - Resolving References][bsmt_wiki_ref] 54 | 55 | To develop this plugin, you'll need: 56 | 57 | - Visual Studio 2019 58 | - Beat Saber installed 59 | - BSIPA and SiraUtil installed in Beat Saber directory, recommended way is to use [Beat Saber Mod Assistant][mod_assistant] 60 | - Recommended to install Visual Studio extension `BeatSaberModdingTools.vsix` from [here][bsmg_tools_vsix] 61 | 62 | Next: 63 | 64 | - Clone this repo `git clone git@github.com:m1el/BeatSaber-SliceVisualizer.git` 65 | - Copy `SliceVisualizer.csproj.user.example` to `SliceVisualizer.csproj.user` 66 | - Edit `SliceVisualizer.csproj.user` to set your Beat Saber installation directory 67 | - Build Solution 68 | 69 | ## LICENSE 70 | 71 | The MIT License, copyright Igor null \ 72 | 73 | [bsmt_wiki]: https://github.com/Zingabopp/BeatSaberModdingTools/wiki "Beat Saber Modding Tools Wiki" 74 | [bsmt_wiki_ref]: https://github.com/Zingabopp/BeatSaberModdingTools/wiki/Resolving-References "Beat Saber Modding Tools Wiki - Resolving References" 75 | [mod_assistant]: https://github.com/Assistant/ModAssistant/releases/latest "Beat Saber Mod Assistant latest release" 76 | [bsmg_tools_vsix]: https://github.com/Zingabopp/BeatSaberModdingTools/releases/latest "Beat Saber Modding tools Visual Studio extension" 77 | -------------------------------------------------------------------------------- /SliceVisualizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net472 4 | Library 5 | 8 6 | enable 7 | false 8 | SliceVisualizer 9 | ..\Refs 10 | $(LocalRefsDir) 11 | $(MSBuildProjectDirectory)\ 12 | 13 | 14 | 15 | bin\Debug\ 16 | full 17 | 18 | 19 | 20 | bin\Release\ 21 | pdbonly 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | True 30 | True 31 | 32 | 33 | 34 | 35 | $(BeatSaberDir)\Plugins\BSML.dll 36 | 37 | 38 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll 39 | 40 | 41 | $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll 42 | 43 | 44 | $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll 45 | 46 | 47 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll 48 | False 49 | 50 | 51 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll 52 | False 53 | 54 | 55 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll 56 | False 57 | 58 | 59 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll 60 | False 61 | 62 | 63 | $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll 64 | False 65 | 66 | 67 | $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll 68 | False 69 | 70 | 71 | $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll 72 | False 73 | 74 | 75 | $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll 76 | False 77 | 78 | 79 | $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll 80 | False 81 | 82 | 83 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll 84 | false 85 | 86 | 87 | $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll 88 | false 89 | 90 | 91 | $(BeatSaberDir)\Plugins\SiraUtil.dll 92 | false 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | all 110 | runtime; build; native; contentfiles; analyzers; buildtransitive 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Program 119 | $(BeatSaberDir)\Beat Saber.exe 120 | $(BeatSaberDir) 121 | 122 | 123 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = tab 7 | tab_width = 4 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | charset = utf-8 11 | max_line_length = 200 12 | insert_final_newline = false 13 | 14 | # ReSharper properties 15 | resharper_csharp_indent_style = space 16 | resharper_csharp_max_line_length = 200 17 | resharper_html_indent_style = space 18 | resharper_html_max_line_length = 200 19 | resharper_resx_indent_style = space 20 | resharper_resx_max_line_length = 200 21 | resharper_use_indent_from_vs = false 22 | resharper_vb_indent_style = space 23 | resharper_vb_max_line_length = 200 24 | resharper_xmldoc_indent_style = space 25 | resharper_xmldoc_max_line_length = 200 26 | resharper_xml_indent_style = space 27 | resharper_xml_max_line_length = 200 28 | 29 | # ReSharper inspection severities 30 | resharper_web_config_module_not_resolved_highlighting = warning 31 | resharper_web_config_type_not_resolved_highlighting = warning 32 | resharper_web_config_wrong_module_highlighting = warning 33 | 34 | # Organize usings 35 | dotnet_sort_system_directives_first = true 36 | dotnet_separate_import_directive_groups = false 37 | 38 | # this. preferences 39 | dotnet_style_qualification_for_field = false:warning 40 | dotnet_style_qualification_for_property = false:warning 41 | dotnet_style_qualification_for_method = false:warning 42 | dotnet_style_qualification_for_event = false:warning 43 | 44 | # Language keywords vs BCL types preferences 45 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 46 | dotnet_style_predefined_type_for_member_access = true:warning 47 | 48 | # Parentheses preferences 49 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent 50 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent 51 | dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:silent 52 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 53 | 54 | # Modifier preferences 55 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning 56 | dotnet_style_readonly_field = true:suggestion 57 | 58 | # Expression-level preferences 59 | dotnet_style_object_initializer = true:suggestion 60 | dotnet_style_collection_initializer = true:suggestion 61 | dotnet_style_explicit_tuple_names = true:suggestion 62 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 63 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 64 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 65 | dotnet_style_prefer_auto_properties = true:suggestion 66 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 67 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 68 | dotnet_style_prefer_compound_assignment = true:suggestion 69 | 70 | # Null-checking preferences 71 | dotnet_style_coalesce_expression = true:suggestion 72 | dotnet_style_null_propagation = true:suggestion 73 | 74 | # Parameter preferences 75 | dotnet_code_quality_unused_parameters = non_public:suggestion 76 | 77 | ############################### 78 | # Naming Conventions # 79 | ############################### 80 | 81 | # Style Definitions 82 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 83 | dotnet_naming_style.uppercase_style.capitalization = all_upper 84 | 85 | # Use upper case for constant fields 86 | dotnet_naming_rule.constant_fields_should_be_upper_case.severity = warning 87 | dotnet_naming_rule.constant_fields_should_be_upper_case.symbols = constant_fields 88 | dotnet_naming_rule.constant_fields_should_be_upper_case.style = uppercase_style 89 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 90 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 91 | dotnet_naming_symbols.constant_fields.required_modifiers = const 92 | 93 | # Use upper case for constant fields 94 | dotnet_naming_rule.static_readonly_fields_should_be_upper_case.severity = warning 95 | dotnet_naming_rule.static_readonly_fields_should_be_upper_case.symbols = static_readonly_fields 96 | dotnet_naming_rule.static_readonly_fields_should_be_upper_case.style = uppercase_style 97 | dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field 98 | dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = public 99 | dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly 100 | 101 | ############################### 102 | # C# Code Style Rules # 103 | ############################### 104 | 105 | # var preferences 106 | csharp_style_var_for_built_in_types = true:warning 107 | csharp_style_var_when_type_is_apparent = true:warning 108 | csharp_style_var_elsewhere = true:suggestion 109 | 110 | # Expression-bodied members 111 | csharp_style_expression_bodied_methods = false:suggestion 112 | csharp_style_expression_bodied_constructors = false:suggestion 113 | csharp_style_expression_bodied_operators = false:suggestion 114 | csharp_style_expression_bodied_properties = true:suggestion 115 | csharp_style_expression_bodied_indexers = true:suggestion 116 | csharp_style_expression_bodied_accessors = true:suggestion 117 | csharp_style_expression_bodied_lambdas = true:suggestion 118 | csharp_style_expression_bodied_local_functions = false:warning 119 | 120 | # Pattern-matching preferences 121 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 122 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 123 | 124 | # Null-checking preferences 125 | csharp_style_throw_expression = true:suggestion 126 | csharp_style_conditional_delegate_call = true:suggestion 127 | 128 | # Modifier preferences 129 | csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion 130 | 131 | # Expression-level preferences 132 | csharp_prefer_braces = true:warning 133 | csharp_style_deconstructed_variable_declaration = true:suggestion 134 | csharp_prefer_simple_default_expression = true:warning 135 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 136 | csharp_style_inlined_variable_declaration = true:warning 137 | 138 | ############################### 139 | # C# Formatting Rules # 140 | ############################### 141 | 142 | # New line preferences 143 | csharp_new_line_before_open_brace = all 144 | csharp_new_line_before_else = true 145 | csharp_new_line_before_catch = true 146 | csharp_new_line_before_finally = true 147 | csharp_new_line_before_members_in_object_initializers = true 148 | csharp_new_line_before_members_in_anonymous_types = true 149 | csharp_new_line_between_query_expression_clauses = true 150 | 151 | # Indentation preferences 152 | csharp_indent_case_contents = true 153 | csharp_indent_switch_labels = true 154 | csharp_indent_labels = flush_left 155 | 156 | # Space preferences 157 | csharp_space_after_cast = true 158 | csharp_space_after_keywords_in_control_flow_statements = true 159 | csharp_space_between_method_call_parameter_list_parentheses = false 160 | csharp_space_between_method_declaration_parameter_list_parentheses = false 161 | csharp_space_between_parentheses = false 162 | csharp_space_before_colon_in_inheritance_clause = true 163 | csharp_space_after_colon_in_inheritance_clause = true 164 | csharp_space_around_binary_operators = before_and_after 165 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 166 | csharp_space_between_method_call_name_and_opening_parenthesis = false 167 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 168 | csharp_space_after_comma = true 169 | csharp_space_after_dot = false 170 | 171 | # Wrapping preferences 172 | csharp_preserve_single_line_statements = false 173 | csharp_preserve_single_line_blocks = true -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Core/NsvSlicedBlock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SliceVisualizer.Configuration; 3 | using SliceVisualizer.Models; 4 | using UnityEngine; 5 | using UnityEngine.UI; 6 | using Zenject; 7 | 8 | namespace SliceVisualizer.Core 9 | { 10 | internal class NsvSlicedBlock : MonoBehaviour, IDisposable 11 | { 12 | private PluginConfig _config = null!; 13 | private ColorManager _colorManager = null!; 14 | 15 | private Transform _blockTransform = null!; 16 | private Transform _sliceTransform = null!; 17 | private Transform _missedAreaTransform = null!; 18 | private Transform _sliceGroupTransform = null!; 19 | private SpriteRenderer _background = null!; 20 | private SpriteRenderer _arrow = null!; 21 | private SpriteRenderer _circle = null!; 22 | private SpriteRenderer _missedArea = null!; 23 | private SpriteRenderer _slice = null!; 24 | 25 | private float _aliveTime; 26 | private bool _isDirectional; 27 | public bool isActive { get; private set; } 28 | private Color _color; 29 | private Color _saberColor; 30 | private Color _arrowColor; 31 | private Color _missedAreaColor; 32 | private Color _sliceColor; 33 | private bool _needsUpdate; 34 | 35 | [Inject] 36 | internal void Construct(NsvAssetLoader assetLoader, ColorManager colorManager) 37 | { 38 | _colorManager = colorManager; 39 | _config = PluginConfig.Instance; 40 | 41 | BuildNote(assetLoader); 42 | } 43 | 44 | private void BuildNote(NsvAssetLoader assetLoader) 45 | { 46 | /* 47 | * The hierarchy is the following: 48 | * (RootBlockGroup 49 | * RoundRect 50 | * Circle 51 | * Arrow 52 | * (SliceGroup 53 | * MissedArea 54 | * Slice)) 55 | */ 56 | 57 | _blockTransform = gameObject.AddComponent(); 58 | gameObject.AddComponent(); 59 | 60 | _blockTransform.localScale = Vector3.one * _config.CubeScale; 61 | _blockTransform.localRotation = Quaternion.identity; 62 | _blockTransform.localPosition = Vector3.zero; 63 | 64 | /* 65 | { 66 | var blockMaskGO = new GameObject("BlockMask"); 67 | var maskTransform = blockMaskGO.AddComponent(); 68 | maskTransform.SetParent(blockTransform); 69 | maskTransform.localScale = Vector3.one; 70 | maskTransform.localRotation = Quaternion.identity; 71 | var halfWidth = Assets.RRect.rect.width / Assets.RRect.pixelsPerUnit / 2.0f; 72 | var halfHeight = Assets.RRect.rect.height / Assets.RRect.pixelsPerUnit / 2.0f; 73 | maskTransform.localPosition = new Vector3(-halfWidth, -halfHeight, 0f); 74 | 75 | var blockMask = blockMaskGO.AddComponent(); 76 | blockMask.sprite = Assets.RRect; 77 | blockMask.backSortingOrder = -4; 78 | blockMask.frontSortingOrder = 0; 79 | } 80 | */ 81 | //Log.Info(string.Format("transform.rotation: {0}", blockMaskGO.transform.localRotation)); 82 | //Log.Info(string.Format("transform.position: {0}", blockMaskGO.transform.localPosition)); 83 | 84 | { 85 | // Construct the note body 86 | var backgroundGO = new GameObject("RoundRect"); 87 | var background = backgroundGO.AddComponent(); 88 | var backgroundTransform = backgroundGO.AddComponent(); 89 | background.material = assetLoader.UINoGlowMaterial; 90 | // background.color = new Color(1.0f, 0.5f, 0.5f, 1.0f); 91 | // background.sortingLayerID = SortingLayerID; 92 | background.sortingOrder = -4; 93 | backgroundTransform.SetParent(_blockTransform); 94 | backgroundTransform.localScale = Vector3.one; 95 | backgroundTransform.localRotation = Quaternion.identity; 96 | if (assetLoader.RRect != null) 97 | { 98 | background.sprite = assetLoader.RRect; 99 | var halfWidth = assetLoader.RRect.rect.width / assetLoader.RRect.pixelsPerUnit / 2.0f; 100 | var halfHeight = assetLoader.RRect.rect.height / assetLoader.RRect.pixelsPerUnit / 2.0f; 101 | backgroundTransform.localPosition = new Vector3(-halfWidth, -halfHeight, 0f); 102 | } 103 | 104 | _background = background; 105 | } 106 | 107 | { 108 | // Construct the small circle in the center 109 | var circleGO = new GameObject("Circle"); 110 | var circle = circleGO.AddComponent(); 111 | var circleTransform = circleGO.AddComponent(); 112 | circle.material = assetLoader.UINoGlowMaterial; 113 | circle.color = _config.CenterColor; 114 | // circle.sortingLayerID = SortingLayerID; 115 | circle.sortingOrder = -3; 116 | circleTransform.SetParent(_blockTransform); 117 | circleTransform.localScale = Vector3.one * _config.CenterScale; 118 | circleTransform.localRotation = Quaternion.identity; 119 | if (assetLoader.Circle != null) 120 | { 121 | var sprite = assetLoader.Circle; 122 | circle.sprite = sprite; 123 | var halfWidth = _config.ArrowScale * sprite.rect.width / sprite.pixelsPerUnit / 2.0f; 124 | var centerOffset = halfWidth - _config.ArrowScale * sprite.rect.height / sprite.pixelsPerUnit; 125 | circleTransform.localPosition = new Vector3(-halfWidth, centerOffset, 0f); 126 | } 127 | 128 | _circle = circle; 129 | } 130 | 131 | { 132 | // Construct the directional arrow 133 | var arrowGO = new GameObject("Arrow"); 134 | var arrow = arrowGO.AddComponent(); 135 | var arrowTransform = arrowGO.AddComponent(); 136 | arrow.material = assetLoader.UINoGlowMaterial; 137 | arrow.color = _config.ArrowColor; 138 | // arrow.sortingLayerID = SortingLayerID; 139 | arrow.sortingOrder = -2; 140 | arrowTransform.SetParent(_blockTransform); 141 | arrowTransform.localScale = Vector3.one * _config.ArrowScale; 142 | arrowTransform.localRotation = Quaternion.identity; 143 | if (assetLoader.Arrow != null) 144 | { 145 | var sprite = assetLoader.Arrow; 146 | arrow.sprite = sprite; 147 | var halfWidth = _config.ArrowScale * sprite.rect.width / sprite.pixelsPerUnit / 2.0f; 148 | var centerOffset = halfWidth - _config.ArrowScale * sprite.rect.height / sprite.pixelsPerUnit; 149 | arrowTransform.localPosition = new Vector3(-halfWidth, centerOffset, 0f); 150 | } 151 | 152 | _arrow = arrow; 153 | } 154 | 155 | { 156 | // Construct the slice UI 157 | var sliceGroupGO = new GameObject("SliceGroup"); 158 | var sliceGroupTransform = sliceGroupGO.AddComponent(); 159 | sliceGroupTransform.SetParent(_blockTransform); 160 | sliceGroupTransform.localScale = Vector3.one; 161 | sliceGroupTransform.localRotation = Quaternion.identity; 162 | sliceGroupTransform.localPosition = Vector3.zero; 163 | _sliceGroupTransform = sliceGroupTransform; 164 | 165 | { 166 | // missed area background 167 | var missedAreaGO = new GameObject("MissedArea"); 168 | var missedArea = missedAreaGO.AddComponent(); 169 | var missedAreaTransform = missedAreaGO.AddComponent(); 170 | missedArea.material = assetLoader.UINoGlowMaterial; 171 | missedArea.sprite = assetLoader.White; 172 | missedArea.color = _config.MissedAreaColor; 173 | // missedArea.sortingLayerID = SortingLayerID; 174 | missedArea.sortingOrder = -1; 175 | // missedArea.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; 176 | missedAreaTransform.SetParent(sliceGroupTransform); 177 | missedAreaTransform.localScale = new Vector3(0f, 1f, 1f); 178 | missedAreaTransform.localRotation = Quaternion.identity; 179 | missedAreaTransform.localPosition = new Vector3(0f, -0.5f, 0f); 180 | _missedAreaTransform = missedAreaTransform; 181 | _missedArea = missedArea; 182 | } 183 | 184 | { 185 | // slice line 186 | var sliceGO = new GameObject("Slice"); 187 | var slice = sliceGO.AddComponent(); 188 | var sliceTransform = sliceGO.AddComponent(); 189 | slice.material = assetLoader.UINoGlowMaterial; 190 | slice.sprite = assetLoader.White; 191 | slice.color = _config.SliceColor; 192 | // slice.sortingLayerID = SortingLayerID; 193 | slice.sortingOrder = 0; 194 | // slice.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; 195 | sliceTransform.SetParent(sliceGroupTransform); 196 | sliceTransform.localScale = new Vector3(_config.SliceWidth, 1f, 1f); 197 | sliceTransform.localRotation = Quaternion.identity; 198 | sliceTransform.localPosition = new Vector3(-_config.SliceWidth / 2f, -0.5f, 0f); 199 | _sliceTransform = sliceTransform; 200 | _slice = slice; 201 | } 202 | } 203 | 204 | SetActive(false); 205 | } 206 | 207 | internal void Init(Transform parent) 208 | { 209 | gameObject.GetComponent().SetParent(parent); 210 | } 211 | 212 | internal void SetData(NoteController noteController, NoteCutInfo noteCutInfo, NoteData noteData) 213 | { 214 | // Extract cube rotation from actual cube rotation 215 | var cubeRotation = _config.RotationFromCubeTransform 216 | ? noteController.noteTransform.localRotation.eulerAngles.z 217 | : noteData.cutDirection.RotationAngle(); 218 | // Using built-in rotationAngle conversion. 219 | // Please note that it's range is from [-180° <=> 180°[ compared to [0° <=> 360°[, but that shouldn't make a difference. 220 | 221 | SetCubeState(noteController, noteCutInfo, noteData, cubeRotation); 222 | 223 | SetSliceState(noteController, noteCutInfo, cubeRotation); 224 | SetActive(true); 225 | } 226 | 227 | private void SetCubeState(NoteController noteController, NoteCutInfo noteCutInfo, NoteData noteData, float cubeRotation) 228 | { 229 | float cubeX; 230 | float cubeY; 231 | if (_config.PositionFromCubeTransform) 232 | { 233 | const float positionScaling = 2.0f; 234 | var position = noteController.noteTransform.position * positionScaling; 235 | cubeX = position.x; 236 | cubeY = position.y; 237 | } 238 | else 239 | { 240 | var positionOffset = new Vector2(-1.5f, 1.5f); 241 | cubeX = noteData.lineIndex + positionOffset.x; 242 | cubeY = (int) noteData.noteLineLayer + positionOffset.y; 243 | } 244 | 245 | _aliveTime = 0f; 246 | 247 | _isDirectional = noteData.cutDirection != NoteCutDirection.Any && noteData.cutDirection != NoteCutDirection.None; 248 | 249 | if (_config.UseCustomColors) 250 | { 251 | _color = noteData.colorType == ColorType.ColorA ? _config.LeftColor : _config.RightColor; 252 | _saberColor = noteCutInfo.saberType == SaberType.SaberA ? _config.LeftColor : _config.RightColor; 253 | } 254 | else 255 | { 256 | // This should work due to colors of both type A and B match for their respective saber and blocks 257 | _color = _colorManager.ColorForType(noteData.colorType); 258 | _saberColor = _colorManager.ColorForSaberType(noteCutInfo.saberType); 259 | } 260 | 261 | _background.color = _color; 262 | _blockTransform.localRotation = Quaternion.Euler(0f, 0f, cubeRotation); 263 | _blockTransform.localPosition = new Vector3(cubeX, cubeY, 0f); 264 | 265 | var arrowAlpha = _isDirectional ? 1f : 0f; 266 | _arrow.color = Fade(_config.ArrowColor, arrowAlpha); 267 | } 268 | 269 | private void SetSliceState(NoteController noteController, NoteCutInfo noteCutInfo, float cubeRotation) 270 | { 271 | var combinedDirection = new Vector3(-noteCutInfo.cutNormal.y, noteCutInfo.cutNormal.x, 0f); 272 | var sliceAngle = Mathf.Atan2(combinedDirection.y, combinedDirection.x) * Mathf.Rad2Deg; 273 | // The default cube rotation is "arrow down", so we rotate slices 90 degrees counter-clockwise 274 | sliceAngle += 90f; 275 | var sliceOffset = noteCutInfo.cutDistanceToCenter; 276 | // why is this different? 277 | // var sliceOffset = Vector3.Dot(info.cutNormal, info.cutPoint 278 | 279 | // Cuts to the left of center have a negative offset 280 | var center = noteController.noteTransform.position; 281 | if (Vector3.Dot(noteCutInfo.cutNormal, noteCutInfo.cutPoint - center) > 0f) 282 | { 283 | sliceOffset = -sliceOffset; 284 | } 285 | 286 | sliceOffset = _config.ScoreScaling.ApplyScaling(sliceOffset, _config.ScoreScaleMin, _config.ScoreScaleMax); 287 | 288 | if (noteCutInfo.saberTypeOK) 289 | { 290 | _missedAreaColor = _config.MissedAreaColor; 291 | _sliceColor = _config.SliceColor; 292 | } 293 | else 294 | { 295 | _missedAreaColor = _saberColor; 296 | _sliceColor = _saberColor; 297 | } 298 | 299 | _arrowColor = noteCutInfo.directionOK ? _config.ArrowColor : _config.BadDirectionColor; 300 | 301 | _arrow.color = _arrowColor; 302 | _missedArea.color = _missedAreaColor; 303 | _slice.color = _slice.color; 304 | 305 | _sliceGroupTransform.localRotation = Quaternion.Euler(0f, 0f, sliceAngle - cubeRotation); 306 | _sliceGroupTransform.localPosition = Vector3.zero; 307 | 308 | _missedAreaTransform.localRotation = Quaternion.identity; 309 | _missedAreaTransform.localScale = new Vector3(sliceOffset, 1f, 1f); 310 | 311 | _sliceTransform.localRotation = Quaternion.identity; 312 | _sliceTransform.localPosition = new Vector3(sliceOffset - _config.SliceWidth * 0.5f, -0.5f, 0f); 313 | } 314 | 315 | internal bool ExternalUpdate(float delta = 0f) 316 | { 317 | _aliveTime += delta; 318 | if (_aliveTime > _config.CubeLifetime) 319 | { 320 | SetActive(false); 321 | return true; 322 | } 323 | 324 | var t = _aliveTime / _config.CubeLifetime; 325 | var blockPosition = _blockTransform.localPosition; 326 | var arrowAlpha = (_isDirectional ? 1f : 0f) * _config.UIOpacity; 327 | blockPosition.z = Mathf.Lerp(-_config.PopDistance, _config.PopDistance, t); 328 | _blockTransform.localPosition = blockPosition; 329 | 330 | // calculate pop strength 331 | var popStrength = 0f; 332 | if (_aliveTime < _config.PopEnd) 333 | { 334 | popStrength = InvLerp(_config.PopEnd, 0.0f, t); 335 | _needsUpdate = true; 336 | } 337 | 338 | // calculate fade out opacity 339 | var alpha = _config.UIOpacity; 340 | if (_aliveTime > _config.FadeStart) 341 | { 342 | var fadeT = InvLerp(_config.FadeStart, 1.0f, t); 343 | alpha *= Mathf.Lerp(1f, 0f, fadeT); 344 | _needsUpdate = true; 345 | } 346 | 347 | if (_needsUpdate) 348 | { 349 | _background.color = Fade(Pop(_color, popStrength), alpha); 350 | _arrow.color = Fade(_arrowColor, arrowAlpha * alpha); 351 | _circle.color = Fade(_config.CenterColor, alpha); 352 | _missedArea.color = Fade(_missedAreaColor, alpha); 353 | _slice.color = Fade(_sliceColor, alpha); 354 | _needsUpdate = false; 355 | } 356 | return false; 357 | } 358 | 359 | private static Color Fade(Color color, float alpha) 360 | { 361 | color.a *= alpha; 362 | return color; 363 | } 364 | 365 | private static float InvLerp(float start, float end, float x) 366 | { 367 | return Mathf.Clamp((x - start) / (end - start), 0f, 1f); 368 | } 369 | 370 | private static Color Pop(Color color, float amount) 371 | { 372 | return color * (1.0f - amount) + amount * Color.white; 373 | } 374 | 375 | public void SetActive(bool isActive) 376 | { 377 | this.isActive = isActive; 378 | gameObject.SetActive(isActive); 379 | _background.gameObject.SetActive(isActive); 380 | _arrow.gameObject.SetActive(isActive); 381 | _circle.gameObject.SetActive(isActive); 382 | _missedArea.gameObject.SetActive(isActive); 383 | _slice.gameObject.SetActive(isActive); 384 | } 385 | 386 | public void Dispose() 387 | { 388 | SetActive(false); 389 | _background = null!; 390 | _arrow = null!; 391 | _circle = null!; 392 | _missedArea = null!; 393 | _slice = null!; 394 | } 395 | } 396 | } 397 | --------------------------------------------------------------------------------