Przeglądaj źródła

Merge pull request #4 from halsafar/devel

Merge development into production.
Stephen Damm 6 lat temu
rodzic
commit
eb29eeb58d

+ 9 - 3
README.md

@@ -3,19 +3,25 @@ A plugin for customizing the in-game song browser.
 
 *This mod works on both the Steam and Oculus Store versions.*
 
+## Screenshot
+
+![Alt text](/Screenshot.png?raw=true "Screenshot")
+
 ## Features
 - Marking a song as favorite
 - Currently supports these sorting methods:
-  - Default: `authorName` then `songName`.
+  - Default: By song name.
+  - Author: By song author name then by song name.
   - Favorite: Anything marked favorite followed by the Default method.
   - Original: Match the original sorting you would normally get after SongLoaderPlugin.
   - Newest: Sort songs by their last write time.
 - Clicking a sorting method will resort the song list immediately.
 
 ## Status
-- Working!
+- Mostly Working!
 
 ### Known Issues
-- Massive custom song collections seem to be causing issues.
+- Might be some issues with game modes other than SoloStandard.
+- Some issues might occur if a new song is added in-game.
 - Add to favorites button shows wrong text sometimes in `not favorite` sort mode.
 

BIN
Screenshot.png


+ 0 - 50
SongBrowserPlugin/DataAccess/BeatSaberSongList.cs

@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using UnityEngine;
-
-namespace SongBrowserPlugin.DataAccess
-{
-    public class BeatSaberSongList : IBeatSaberSongList
-    {
-        private Logger _log = new Logger("BeatSaberSongList");
-
-        /// <summary>
-        /// Fetch the existing song list.
-        /// </summary>
-        /// <returns></returns>
-        public List<LevelStaticData> AcquireSongList()
-        {
-            _log.Debug("AcquireSongList()");
-
-            Stopwatch stopwatch = Stopwatch.StartNew();
-
-            var gameScenesManager = Resources.FindObjectsOfTypeAll<GameScenesManager>().FirstOrDefault();
-            var gameDataModel = PersistentSingleton<GameDataModel>.instance;
-
-            List<LevelStaticData> songList = gameDataModel.gameStaticData.worldsData[0].levelsData.ToList();
-
-            stopwatch.Stop();
-            _log.Info("Acquiring Song List from Beat Saber took {0}ms", stopwatch.ElapsedMilliseconds);
-
-            return songList;
-        }
-
-        /// <summary>
-        /// Helper to overwrite the existing song list.
-        /// </summary>
-        /// <param name="newSongList"></param>
-        public void OverwriteBeatSaberSongList(List<LevelStaticData> newSongList)
-        {
-            Stopwatch stopwatch = Stopwatch.StartNew();
-
-            var gameDataModel = PersistentSingleton<GameDataModel>.instance;
-            ReflectionUtil.SetPrivateField(gameDataModel.gameStaticData.worldsData[0], "_levelsData", newSongList.ToArray());
-
-            stopwatch.Stop();
-            _log.Info("Overwriting the beat saber song list took {0}ms", stopwatch.ElapsedMilliseconds);
-        }
-    }
-}

+ 0 - 13
SongBrowserPlugin/DataAccess/IBeatSaberSongList.cs

@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace SongBrowserPlugin.DataAccess
-{
-    public interface IBeatSaberSongList
-    {
-        List<LevelStaticData> AcquireSongList();
-        void OverwriteBeatSaberSongList(List<LevelStaticData> newSongList);
-    }
-}

+ 6 - 7
SongBrowserPlugin/SongBrowserSettings.cs

@@ -4,12 +4,13 @@ using System.IO;
 using System.Xml.Serialization;
 
 
-namespace SongBrowserPlugin
+namespace SongBrowserPlugin.DataAccess
 {
     [Serializable]
     public enum SongSortMode
     {
-        Default,        
+        Default,
+        Author,
         Favorites,
         Original,
         Newest,
@@ -22,7 +23,7 @@ namespace SongBrowserPlugin
         public List<String> favorites;
 
         [NonSerialized]
-        private static Logger Log = new Logger("SongBrowserPlugin-Settings");
+        private static Logger Log = new Logger("SongBrowserSettings");
 
         /// <summary>
         /// Constructor.
@@ -48,7 +49,7 @@ namespace SongBrowserPlugin
         /// <returns>SongBrowserSettings</returns>
         public static SongBrowserSettings Load()
         {
-            Log.Debug("Load Song Browser Settings");
+            Log.Trace("Load()");
             SongBrowserSettings retVal = null;
 
             String settingsFilePath = SongBrowserSettings.SettingsPath();
@@ -67,12 +68,10 @@ namespace SongBrowserPlugin
                 XmlSerializer serializer = new XmlSerializer(typeof(SongBrowserSettings));
                 
                 retVal = (SongBrowserSettings)serializer.Deserialize(fs);
-
-                Log.Debug("sortMode: " + retVal.sortMode);
             }
             catch (Exception e)
             {
-                Log.Exception("Unable to deserialize song browser settings file: " + e.Message);
+                Log.Exception("Unable to deserialize song browser settings file: ", e);
 
                 // Return default settings
                 retVal = new SongBrowserSettings();

+ 50 - 9
SongBrowserPlugin/Logger.cs

@@ -3,49 +3,90 @@
 
 namespace SongBrowserPlugin
 {
+    public enum LogLevel
+    {
+        Trace,
+        Debug,
+        Info,
+        Warn,
+        Error
+    }
     public class Logger
     {
         private string loggerName;
+        private LogLevel _LogLevel = LogLevel.Trace;
+        private ConsoleColor _defaultFgColor;
 
         public Logger(string _name)
         {
             loggerName = _name;
+            _defaultFgColor = ConsoleColor.Gray;
         }
 
-        public static void StaticLog(string message)
+        public void ResetForegroundColor()
         {
-            Console.ForegroundColor = ConsoleColor.Green;
-            Console.WriteLine("[SongBrowserPlugin @ " + DateTime.Now.ToString("HH:mm") + "] " + message);
+            Console.ForegroundColor = _defaultFgColor;
+        }
+
+        public void Trace(string format, params object[] args)
+        {
+            if (_LogLevel > LogLevel.Trace)
+            {
+                return;
+            }
+            Console.ForegroundColor = ConsoleColor.Cyan;
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + " - Trace] " + String.Format(format, args));
+            ResetForegroundColor();
         }
 
         public void Debug(string format, params object[] args)
         {
+            if (_LogLevel > LogLevel.Debug)
+            {
+                return;
+            }
+
             Console.ForegroundColor = ConsoleColor.Magenta;
-            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format(format, args));
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + " - Debug] " + String.Format(format, args));
+            ResetForegroundColor();
         }
 
         public void Info(string format, params object[] args)
         {
+            if (_LogLevel > LogLevel.Info)
+            {
+                return;
+            }
+
             Console.ForegroundColor = ConsoleColor.Green;
-            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format(format, args));
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + " - Info] " + String.Format(format, args));
+            ResetForegroundColor();
         }
 
         public void Warning(string format, params object[] args)
         {
+            if (_LogLevel > LogLevel.Warn)
+            {
+                return;
+            }
+
             Console.ForegroundColor = ConsoleColor.Blue;
-            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format(format, args));
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + " - Warning] " + String.Format(format, args));
+            ResetForegroundColor();
         }
 
         public void Error(string format, params object[] args)
         {
             Console.ForegroundColor = ConsoleColor.Yellow;
-            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format(format, args));
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + " - Error] " + String.Format(format, args));
+            ResetForegroundColor();
         }
 
-        public void Exception(string format, params object[] args)
+        public void Exception(string message, Exception e)
         {
             Console.ForegroundColor = ConsoleColor.Red;
-            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format(format, args));
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format("{0}-{1}\n{2}", message, e.Message, e.StackTrace));
+            ResetForegroundColor();
         }
 
     }

+ 4 - 13
SongBrowserPlugin/Plugin.cs

@@ -1,14 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using UnityEngine;
-using UnityEngine.SceneManagement;
-using VRUI;
+using UnityEngine.SceneManagement;
 using IllusionPlugin;
-using TMPro;
-using UnityEngine.UI;
-using System.Collections;
 
 
 namespace SongBrowserPlugin
@@ -22,7 +13,7 @@ namespace SongBrowserPlugin
 
 		public string Version
 		{
-			get { return "v1.0-rc3"; }
+			get { return "v2.0-beta"; }
 		}
 		
 		public void OnApplicationStart()
@@ -47,8 +38,8 @@ namespace SongBrowserPlugin
         public void OnLevelWasLoaded(int level)
 		{
             //Console.WriteLine("OnLevelWasLoaded=" + level);            
-            if (level != SongBrowserMasterViewController.MenuIndex) return;
-            SongBrowserMasterViewController.OnLoad();
+            if (level != SongBrowserApplication.MenuIndex) return;
+            SongBrowserApplication.OnLoad();
         }
 
 		public void OnLevelWasInitialized(int level)

+ 0 - 42
SongBrowserPlugin/ReflectionUtil.cs

@@ -1,42 +0,0 @@
-using System.Reflection;
-
-namespace SongBrowserPlugin
-{
-	public static class ReflectionUtil
-	{
-		public static void SetPrivateField(object obj, string fieldName, object value)
-		{
-			var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
-			prop.SetValue(obj, value);
-		}
-		
-		public static T GetPrivateField<T>(object obj, string fieldName)
-		{
-			var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
-			var value = prop.GetValue(obj);
-			return (T) value;
-		}
-		
-		public static void SetPrivateProperty(object obj, string propertyName, object value)
-		{
-			var prop = obj.GetType().GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
-			prop.SetValue(obj, value, null);
-		}
-
-		public static void InvokePrivateMethod(object obj, string methodName, object[] methodParams)
-		{
-			MethodInfo dynMethod = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
-			dynMethod.Invoke(obj, methodParams);
-		}
-
-        public static object InvokeMethod<T>(this T o, string methodName, params object[] args)
-        {
-            var mi = o.GetType().GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-            if (mi != null)
-            {
-                return mi.Invoke(o, args);
-            }
-            return null;
-        }
-    }
-}

+ 181 - 0
SongBrowserPlugin/SongBrowserApplication.cs

@@ -0,0 +1,181 @@
+using UnityEngine;
+using System.Linq;
+using System;
+using SongLoaderPlugin.OverrideClasses;
+using UnityEngine.SceneManagement;
+using SongLoaderPlugin;
+using UnityEngine.UI;
+using SongBrowserPlugin.UI;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace SongBrowserPlugin
+{
+    public class SongBrowserApplication : MonoBehaviour
+    {
+        public const int MenuIndex = 1;
+
+        public static SongBrowserApplication Instance;
+
+        private Logger _log = new Logger("SongBrowserApplication");
+
+        // Song Browser UI Elements
+        private SongBrowserUI _songBrowserUI;
+        public Dictionary<String, Sprite> CachedIcons;
+        public Button ButtonTemplate;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        internal static void OnLoad()
+        {            
+            if (Instance != null)
+            {
+                return;
+            }
+            new GameObject("BeatSaber SongBrowser Mod").AddComponent<SongBrowserApplication>();
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        private void Awake()
+        {
+            _log.Trace("Awake()");
+
+            Instance = this;
+
+            _songBrowserUI = gameObject.AddComponent<SongBrowserUI>();
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public void Start()
+        {
+            _log.Trace("Start()");
+
+            AcquireUIElements();
+
+            StartCoroutine(WaitForSongListUI());
+        }
+
+        /// <summary>
+        /// Wait for the song list to be visible to draw it.
+        /// </summary>
+        /// <returns></returns>
+        private IEnumerator WaitForSongListUI()
+        {
+            _log.Trace("WaitForSongListUI()");
+
+            yield return new WaitUntil(delegate () { return Resources.FindObjectsOfTypeAll<StandardLevelSelectionFlowCoordinator>().Any(); });
+
+            _log.Debug("Found StandardLevelSelectionFlowCoordinator...");
+
+            _songBrowserUI.CreateUI();
+
+            if (SongLoaderPlugin.SongLoader.AreSongsLoaded)
+            {
+                OnSongLoaderLoadedSongs(null, SongLoader.CustomLevels);
+            }
+            else
+            {
+                SongLoader.SongsLoadedEvent += OnSongLoaderLoadedSongs;
+            }
+
+            _songBrowserUI.RefreshSongList();            
+        }
+
+        /// <summary>
+        /// Only gets called once during boot of BeatSaber.  
+        /// </summary>
+        /// <param name="loader"></param>
+        /// <param name="levels"></param>
+        private void OnSongLoaderLoadedSongs(SongLoader loader, List<CustomLevel> levels)
+        {
+            _log.Trace("OnSongLoaderLoadedSongs");
+            try
+            {
+                _songBrowserUI.UpdateSongList();
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception during OnSongLoaderLoadedSongs: ", e);
+            }
+        }
+
+        /// <summary>
+        /// Get a handle to the view controllers we are going to add elements to.
+        /// </summary>
+        public void AcquireUIElements()
+        {
+            _log.Trace("AcquireUIElements()");        
+            try
+            {
+                CachedIcons = new Dictionary<String, Sprite>();
+                foreach (Sprite sprite in Resources.FindObjectsOfTypeAll<Sprite>())
+                {
+                    if (CachedIcons.ContainsKey(sprite.name))
+                    {
+                        continue;
+                    }
+                    CachedIcons.Add(sprite.name, sprite);
+                }
+
+                ButtonTemplate = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "PlayButton"));
+
+                // Append our own event to appropriate events so we can refresh the song list before the user sees it.
+                MainFlowCoordinator mainFlow = Resources.FindObjectsOfTypeAll<MainFlowCoordinator>().First();
+                SoloModeSelectionViewController view = Resources.FindObjectsOfTypeAll<SoloModeSelectionViewController>().First();                
+                view.didFinishEvent += HandleSoloModeSelectionViewControllerDidSelectMode;
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception AcquireUIElements(): ", e);
+            }
+        }
+
+        /// <summary>
+        /// Perfect time to refresh the level list on first entry.
+        /// </summary>
+        /// <param name="arg1"></param>
+        /// <param name="arg2"></param>
+        private void HandleSoloModeSelectionViewControllerDidSelectMode(SoloModeSelectionViewController arg1, SoloModeSelectionViewController.SubMenuType arg2)
+        {
+            _log.Trace("HandleSoloModeSelectionViewControllerDidSelectMode()");
+            this._songBrowserUI.RefreshSongList();
+        }
+
+        /// <summary>
+        /// Helper for invoking buttons.
+        /// </summary>
+        /// <param name="buttonName"></param>
+        private void InvokeBeatSaberButton(String buttonName)
+        {
+            Button buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == buttonName));
+            buttonInstance.onClick.Invoke();
+        }
+
+        /// <summary>
+        /// Map some key presses directly to UI interactions to make testing easier.
+        /// </summary>
+        private void LateUpdate()
+        {
+            // z,x,c,v can be used to get into a song, b will hit continue button after song ends
+            if (Input.GetKeyDown(KeyCode.Z))
+            {
+                InvokeBeatSaberButton("SoloButton");
+            }
+
+            if (Input.GetKeyDown(KeyCode.X))
+            {
+                InvokeBeatSaberButton("StandardButton");
+            }
+
+            if (Input.GetKeyDown(KeyCode.B))
+            {
+                InvokeBeatSaberButton("ContinueButton");
+            }
+        }
+    }
+}

+ 0 - 532
SongBrowserPlugin/SongBrowserMasterViewController.cs

@@ -1,532 +0,0 @@
-using UnityEngine;
-using System.Linq;
-using System;
-using System.Collections.Generic;
-using System.Security.Cryptography;
-using UnityEngine.Events;
-using UnityEngine.SceneManagement;
-using UnityEngine.UI;
-using System.Text;
-using HMUI;
-using System.Text.RegularExpressions;
-using System.IO;
-using System.Threading;
-
-namespace SongBrowserPlugin
-{
-    public class SongSortButton
-    {
-        public SongSortMode SortMode;
-        public Button Button;
-    }
-
-    public class SongBrowserMasterViewController : MonoBehaviour
-    {       
-        // Which Scene index to run on
-        public const int MenuIndex = 1;
-
-        private Logger _log = new Logger("SongBrowserMasterViewController");
-
-        // Private UI fields
-        private SongSelectionMasterViewController _songSelectionMasterView;
-        private SongDetailViewController _songDetailViewController;
-        private SongListViewController _songListViewController;
-        private MainMenuViewController _mainMenuViewController;
-
-        private Dictionary<String, Sprite> _icons;
-
-        private Button _buttonInstance;
-
-        private List<SongSortButton> _sortButtonGroup;
-        
-        private Button _addFavoriteButton;
-        private String _addFavoriteButtonText = null;
-
-        // Model
-        private SongBrowserModel _model;
-
-        /// <summary>
-        /// Unity OnLoad
-        /// </summary>
-        public static void OnLoad()
-        {
-            if (Instance != null) return;
-            new GameObject("Song Browser").AddComponent<SongBrowserMasterViewController>();
-        }
-
-        public static SongBrowserMasterViewController Instance;
-
-        /// <summary>
-        /// Builds the UI for this plugin.
-        /// </summary>
-        private void Awake()
-        {
-            _log.Debug("Awake()");
-
-            Instance = this;
-          
-            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;
-
-            SongLoaderPlugin.SongLoader.SongsLoaded.AddListener(OnSongLoaderLoadedSongs);
-
-            DontDestroyOnLoad(gameObject);
-        }
-
-        /// <summary>
-        /// Get a handle to the view controllers we are going to add elements to.
-        /// </summary>
-        public void AcquireUIElements()
-        {
-            _icons = new Dictionary<String, Sprite>();
-            foreach (Sprite sprite in Resources.FindObjectsOfTypeAll<Sprite>())
-            {
-                if (_icons.ContainsKey(sprite.name))
-                {
-                    continue;
-                }
-                _icons.Add(sprite.name, sprite);
-            }
-
-            try
-            {
-                _buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "PlayButton"));
-                _mainMenuViewController = Resources.FindObjectsOfTypeAll<MainMenuViewController>().First();
-                _songSelectionMasterView = Resources.FindObjectsOfTypeAll<SongSelectionMasterViewController>().First();                
-                _songDetailViewController = Resources.FindObjectsOfTypeAll<SongDetailViewController>().First();
-                _songListViewController = Resources.FindObjectsOfTypeAll<SongListViewController>().First();
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception AcquireUIElements(): " + e);
-            }
-        }
-
-        /// <summary>
-        /// Builds the SongBrowser UI
-        /// </summary>
-        public void CreateUI()
-        {
-            _log.Debug("CreateUI");
-
-            // _icons.ForEach(i => Console.WriteLine(i.ToString()));
-
-            try
-            {                               
-                RectTransform rect = _songSelectionMasterView.transform as RectTransform;
-
-                // Create Sorting Songs By-Buttons
-                _sortButtonGroup = new List<SongSortButton>
-                {
-                    CreateSortButton(rect, "PlayButton", "Fav", 3, "AllDirectionsIcon", 30f, 77.5f, 15f, 5f, SongSortMode.Favorites),
-                    CreateSortButton(rect, "PlayButton", "Def", 3, "AllDirectionsIcon", 15f, 77.5f, 15f, 5f, SongSortMode.Default),
-                    CreateSortButton(rect, "PlayButton", "Org", 3, "AllDirectionsIcon", 0f, 77.5f, 15f, 5f, SongSortMode.Original),
-                    CreateSortButton(rect, "PlayButton", "New", 3, "AllDirectionsIcon", -15f, 77.5f, 15f, 5f, SongSortMode.Newest)
-                };
-
-                // Creaate Add to Favorites Button
-                RectTransform transform = _songDetailViewController.transform as RectTransform;
-                _addFavoriteButton = UIBuilder.CreateUIButton(transform, "QuitButton", _buttonInstance);
-                (_addFavoriteButton.transform as RectTransform).anchoredPosition = new Vector2(45f, 5f);
-                (_addFavoriteButton.transform as RectTransform).sizeDelta = new Vector2(10f, 10f);                
-
-                if (_addFavoriteButtonText == null)
-                {
-                    LevelStaticData level = getSelectedSong();
-                    RefreshAddFavoriteButton(level);
-                }
-                
-                UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
-                //UIBuilder.SetButtonIcon(ref _addFavoriteButton, _icons["AllDirectionsIcon"]);
-                UIBuilder.SetButtonTextSize(ref _addFavoriteButton, 3);
-                UIBuilder.SetButtonIconEnabled(ref _addFavoriteButton, false);
-
-                _addFavoriteButton.onClick.RemoveAllListeners();
-                _addFavoriteButton.onClick.AddListener(delegate () {                    
-                    ToggleSongInFavorites();
-                });
-
-                RefreshUI();
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception CreateUI: " + e.Message);
-            }
-        }
-
-        /// <summary>
-        /// Generic create sort button.
-        /// </summary>
-        /// <param name="rect"></param>
-        /// <param name="templateButtonName"></param>
-        /// <param name="buttonText"></param>
-        /// <param name="iconName"></param>
-        /// <param name="x"></param>
-        /// <param name="y"></param>
-        /// <param name="w"></param>
-        /// <param name="h"></param>
-        /// <param name="action"></param>
-        private SongSortButton CreateSortButton(RectTransform rect, string templateButtonName, string buttonText, float fontSize, string iconName, float x, float y, float w, float h, SongSortMode sortMode)
-        {
-            SongSortButton sortButton = new SongSortButton();
-            Button newButton = UIBuilder.CreateUIButton(rect, templateButtonName, _buttonInstance);
-            
-            newButton.interactable = true;
-            (newButton.transform as RectTransform).anchoredPosition = new Vector2(x, y);
-            (newButton.transform as RectTransform).sizeDelta = new Vector2(w, h);
-
-            UIBuilder.SetButtonText(ref newButton, buttonText);
-            //UIBuilder.SetButtonIconEnabled(ref _originalButton, false);
-            UIBuilder.SetButtonIcon(ref newButton, _icons[iconName]);
-            UIBuilder.SetButtonTextSize(ref newButton, fontSize);
-
-            newButton.onClick.RemoveAllListeners();
-            newButton.onClick.AddListener(delegate () {
-                _log.Debug("Sort button - {0} - pressed.", sortMode.ToString());
-                _model.Settings.sortMode = sortMode;
-                _model.Settings.Save();
-                UpdateSongList();
-            });
-
-            sortButton.Button = newButton;
-            sortButton.SortMode = sortMode;
-
-            return sortButton;
-        }
-
-        /// <summary>
-        /// Bind to some UI events.
-        /// </summary>
-        /// <param name="arg0"></param>
-        /// <param name="scene"></param>
-        private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene)
-        {
-            //_log.Debug("scene.buildIndex==" + scene.buildIndex);
-            try
-            {
-                if (scene.buildIndex == SongBrowserMasterViewController.MenuIndex)
-                {
-                    _log.Debug("SceneManagerOnActiveSceneChanged - Setting Up UI");
-
-                    AcquireUIElements();
-
-                    if (_model == null)
-                    {
-                        _model = new SongBrowserModel();
-                    }
-                    _model.Init(new DataAccess.BeatSaberSongList() /*_songSelectionMasterView, _songListViewController*/);
-
-                    CreateUI();
-                                                            
-                    _songListViewController.didSelectSongEvent += OnDidSelectSongEvent;
-                }
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception during scene change: " + e);
-            }       
-        }
-
-        /// <summary>
-        /// Song Loader has loaded the songs, lets sort them.
-        /// </summary>
-        private void OnSongLoaderLoadedSongs()
-        {
-            _log.Debug("OnSongLoaderLoadedSongs");            
-            //RefreshAddFavoriteButton(sortedSongList[0]);
-
-            // Call into SongLoaderPlugin to get all the song info.
-            try
-            {
-                UpdateSongList();
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception trying to update song list: {0}", e);
-            }
-        }
-
-        /// <summary>
-        /// Adjust UI based on song selected.
-        /// Various ways of detecting if a song is not properly selected.  Seems most hit the first one.
-        /// </summary>
-        /// <param name="songListViewController"></param>
-        private void OnDidSelectSongEvent(SongListViewController songListViewController)
-        {
-            LevelStaticData level = getSelectedSong();
-            if (level == null)
-            {
-                _log.Debug("No song selected?");
-                return;
-            }
-
-            if (_model.Settings == null)
-            {
-                _log.Debug("Settings not instantiated yet?");
-                return;
-            }
-
-            RefreshAddFavoriteButton(level);
-        }
-
-        /// <summary>
-        /// Return LevelStaticData or null.
-        /// </summary>
-        private LevelStaticData getSelectedSong()
-        {
-            // song list not even visible
-            if (!_songSelectionMasterView.isActiveAndEnabled)
-            {
-                return null;
-            }
-
-            int selectedIndex = _songSelectionMasterView.GetSelectedSongIndex();
-            if (selectedIndex < 0)
-            {
-                return null;
-            }
-
-            LevelStaticData level = _songSelectionMasterView.GetLevelStaticDataForSelectedSong();
-
-            return level;
-        }
-
-        /// <summary>
-        /// Add/Remove song from favorites depending on if it already exists.
-        /// </summary>
-        private void ToggleSongInFavorites()
-        {
-            LevelStaticData songInfo = _songSelectionMasterView.GetLevelStaticDataForSelectedSong();
-            if (_model.Settings.favorites.Contains(songInfo.levelId))
-            {
-                _log.Info("Remove {0} from favorites", songInfo.name);
-                _model.Settings.favorites.Remove(songInfo.levelId);
-                _addFavoriteButtonText = "+1";
-            }
-            else
-            {
-                _log.Info("Add {0} to favorites", songInfo.name);
-                _model.Settings.favorites.Add(songInfo.levelId);
-                _addFavoriteButtonText = "-1";                
-            }
-
-            UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
-
-            _model.Settings.Save();
-        }
-
-        /// <summary>
-        /// Helper to quickly refresh add to favorites button
-        /// </summary>
-        /// <param name="levelId"></param>
-        private void RefreshAddFavoriteButton(LevelStaticData level)
-        {
-            var levelId = _songListViewController.levelId;
-            if (levelId == null)
-            {
-                if (level != null)
-                {
-                    levelId = level.levelId;
-                }
-            }
-
-            //if (level != null) _log.Debug(level.songName);
-            //if (level != null)
-            //    _log.Debug("Level.id=" + level.levelId);
-            //_log.Debug("_songListViewController.levelId=" + _songListViewController.levelId);
-
-            if (levelId == null)
-            {
-                _addFavoriteButtonText = "0";
-                return;
-            }
-
-            if (levelId == null)
-            {
-                levelId = level.levelId;
-            }
-
-            if (_model.Settings.favorites.Contains(levelId))
-            {
-                _addFavoriteButtonText = "-1";
-            }
-            else
-            {
-                _addFavoriteButtonText = "+1";                
-            }
-
-            UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
-        }
-
-        /// <summary>
-        /// Adjust the UI colors.
-        /// </summary>
-        public void RefreshUI()
-        {
-            // So far all we need to refresh is the sort buttons.
-            foreach (SongSortButton sortButton in _sortButtonGroup)
-            {
-                UIBuilder.SetButtonBorder(ref sortButton.Button, Color.black);
-                if (sortButton.SortMode == _model.Settings.sortMode)
-                {
-                    UIBuilder.SetButtonBorder(ref sortButton.Button, Color.red);
-                }
-            }            
-        }
-
-        /// <summary>
-        /// Try to refresh the song list.  Broken for now.
-        /// </summary>
-        public void RefreshSongList(List<LevelStaticData> songList)
-        {
-            _log.Debug("Attempting to refresh the song list view.");
-            try
-            {
-                // Check a couple of possible situations that we can't refresh
-                if (!_songSelectionMasterView.isActiveAndEnabled)
-                {
-                    _log.Debug("No song list to refresh.");
-                    return;
-                }
-
-                SongListTableView songTableView = _songListViewController.GetComponentInChildren<SongListTableView>();
-                if (songTableView == null)
-                {
-                    return;
-                }
-
-                TableView tableView = ReflectionUtil.GetPrivateField<TableView>(songTableView, "_tableView");
-                if (tableView == null)
-                {
-                    return;
-                }
-
-                // Convert to Array once in-case this is costly.
-                LevelStaticData[] songListArray = songList.ToArray();
-
-                // Refresh the master view
-                bool useLocalLeaderboards = ReflectionUtil.GetPrivateField<bool>(_songSelectionMasterView, "_useLocalLeaderboards");
-                bool showDismissButton = true;
-                bool showPlayerStats = ReflectionUtil.GetPrivateField<bool>(_songSelectionMasterView, "_showPlayerStats");
-                GameplayMode gameplayMode = ReflectionUtil.GetPrivateField<GameplayMode>(_songSelectionMasterView, "_gameplayMode");
-
-                _songSelectionMasterView.Init(
-                    _songSelectionMasterView.levelId,
-                    _songSelectionMasterView.difficulty,
-                    songListArray,
-                    useLocalLeaderboards, showDismissButton, showPlayerStats, gameplayMode
-                );
-
-                // Refresh the table views
-                ReflectionUtil.SetPrivateField(songTableView, "_levels", songListArray);
-                tableView.ReloadData();
-
-                // Clear Force selection of index 0 so we don't end up in a weird state.
-                songTableView.ClearSelection();
-                _songListViewController.SelectSong(0);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
-                
-
-                RefreshUI();
-                RefreshAddFavoriteButton(songList[0]);
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception refreshing song list: {0}", e.Message);
-            }
-        }
-
-        /// <summary>
-        /// Helper for updating the model (which updates the song list)
-        /// </summary>
-        public void UpdateSongList()
-        {
-           _model.UpdateSongLists(true);
-            RefreshSongList(_model.SortedSongList);
-        }
-
-        /// <summary>
-        /// Map some key presses directly to UI interactions to make testing easier.
-        /// </summary>
-        private void Update()
-        {
-            // cycle sort modes
-            if (Input.GetKeyDown(KeyCode.T))
-            {
-                if (_model.Settings.sortMode == SongSortMode.Favorites)
-                    _model.Settings.sortMode = SongSortMode.Newest;
-                else if (_model.Settings.sortMode == SongSortMode.Newest)
-                    _model.Settings.sortMode = SongSortMode.Original;
-                else if (_model.Settings.sortMode == SongSortMode.Original)
-                    _model.Settings.sortMode = SongSortMode.Default;
-                else if (_model.Settings.sortMode == SongSortMode.Default)
-                    _model.Settings.sortMode = SongSortMode.Favorites;
-
-                UpdateSongList();
-            }
-
-            // z,x,c,v can be used to get into a song, b will hit continue button after song ends
-            if (Input.GetKeyDown(KeyCode.Z))
-            {
-                Button _buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "SoloButton"));
-                _buttonInstance.onClick.Invoke();
-            }
-
-            if (Input.GetKeyDown(KeyCode.X))
-            {
-                Button _buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "FreePlayButton"));
-                _buttonInstance.onClick.Invoke();                
-            }
-
-            if (Input.GetKeyDown(KeyCode.C))
-            {                
-                _songListViewController.SelectSong(0);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
-
-                DifficultyViewController _difficultyViewController = Resources.FindObjectsOfTypeAll<DifficultyViewController>().First();
-                _difficultyViewController.SelectDifficulty(LevelStaticData.Difficulty.Hard);
-                _songSelectionMasterView.HandleDifficultyViewControllerDidSelectDifficulty(_difficultyViewController);
-            }
-
-            if (Input.GetKeyDown(KeyCode.V))
-            {
-                _songSelectionMasterView.HandleSongDetailViewControllerDidPressPlayButton(_songDetailViewController);
-            }
-
-            if (Input.GetKeyDown(KeyCode.B))
-            {
-                Button _buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "ContinueButton"));
-                _buttonInstance.onClick.Invoke();
-            }
-
-            // change song index
-            if (Input.GetKeyDown(KeyCode.N))
-            {
-                int newIndex = _songSelectionMasterView.GetSelectedSongIndex() - 1;
-
-                _songListViewController.SelectSong(newIndex);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
-
-                SongListTableView songTableView = Resources.FindObjectsOfTypeAll<SongListTableView>().First();
-                _songListViewController.HandleSongListTableViewDidSelectRow(songTableView, newIndex);
-            }
-
-            if (Input.GetKeyDown(KeyCode.M))
-            {
-                int newIndex = _songSelectionMasterView.GetSelectedSongIndex() + 1;
-
-                _songListViewController.SelectSong(newIndex);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
-
-                SongListTableView songTableView = Resources.FindObjectsOfTypeAll<SongListTableView>().First();
-                _songListViewController.HandleSongListTableViewDidSelectRow(songTableView, newIndex);
-            }
-
-            // add to favorites
-            if (Input.GetKeyDown(KeyCode.F))
-            {
-                ToggleSongInFavorites();
-            }
-        }
-    }
-}
- 

+ 84 - 97
SongBrowserPlugin/SongBrowserModel.cs

@@ -1,11 +1,10 @@
-using HMUI;
-using SongBrowserPlugin.DataAccess;
+using SongBrowserPlugin.DataAccess;
+using SongLoaderPlugin;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
-using System.Text;
 using UnityEngine;
 
 namespace SongBrowserPlugin
@@ -13,22 +12,16 @@ namespace SongBrowserPlugin
     public class SongBrowserModel
     {
         private Logger _log = new Logger("SongBrowserModel");
-
-        private List<SongLoaderPlugin.CustomSongInfo> _customSongInfos;
-        private Dictionary<String, SongLoaderPlugin.CustomSongInfo> _levelIdToCustomSongInfo;
-        private Dictionary<String, double> _cachedLastWriteTimes;
+        
         private SongBrowserSettings _settings;
 
-        //private SongSelectionMasterViewController _songSelectionMasterView;
-        //private SongListViewController _songListViewController;
-
-        private IBeatSaberSongList _beatSaberSongAccessor;
-
-        private List<LevelStaticData> _sortedSongs;
-        private List<LevelStaticData> _originalSongs;    
-        private SongSortMode _cachedSortMode = default(SongSortMode);
+        private List<StandardLevelSO> _sortedSongs;
+        private List<StandardLevelSO> _originalSongs;
+        private Dictionary<String, SongLoaderPlugin.OverrideClasses.CustomLevel> _levelIdToCustomLevel;
+        private SongLoaderPlugin.OverrideClasses.CustomLevelCollectionSO _gameplayModeCollection;    
+        private Dictionary<String, double> _cachedLastWriteTimes;
 
-        private DateTime _cachedCustomSongDirLastWriteTIme = DateTime.MinValue;
+        public static String LastSelectedLevelId { get; set; }
 
         public SongBrowserSettings Settings
         {
@@ -38,7 +31,7 @@ namespace SongBrowserPlugin
             }
         }
 
-        public List<LevelStaticData> SortedSongList
+        public List<StandardLevelSO> SortedSongList
         {
             get
             {
@@ -46,6 +39,14 @@ namespace SongBrowserPlugin
             }
         }
 
+        public Dictionary<String, SongLoaderPlugin.OverrideClasses.CustomLevel> LevelIdToCustomSongInfos
+        {
+            get
+            {
+                return _levelIdToCustomLevel;
+            }
+        }
+
         /// <summary>
         /// Constructor.
         /// </summary>
@@ -59,76 +60,53 @@ namespace SongBrowserPlugin
         /// </summary>
         /// <param name="songSelectionMasterView"></param>
         /// <param name="songListViewController"></param>
-        public void Init(IBeatSaberSongList beatSaberSongAccessor /*SongSelectionMasterViewController songSelectionMasterView, SongListViewController songListViewController*/)
+        public void Init()
         {
-            _beatSaberSongAccessor = beatSaberSongAccessor;
             _settings = SongBrowserSettings.Load();
-
-            //_songSelectionMasterView = songSelectionMasterView;
-            //_songListViewController = songListViewController;
+            _log.Info("Settings loaded, sorting mode is: {0}", _settings.sortMode);
         }
 
         /// <summary>
         /// Get the song cache from the game.
+        /// TODO: This might not even be necessary anymore.  Need to test interactions with BeatSaverDownloader.
         /// </summary>
-        public void UpdateSongLists(bool updateSongInfos)
+        public void UpdateSongLists(GameplayMode gameplayMode)
         {
             String customSongsPath = Path.Combine(Environment.CurrentDirectory, "CustomSongs");
             DateTime currentLastWriteTIme = File.GetLastWriteTimeUtc(customSongsPath);
-            if (_cachedCustomSongDirLastWriteTIme == null || DateTime.Compare(currentLastWriteTIme, _cachedCustomSongDirLastWriteTIme) != 0)
-            {
-                _log.Debug("Custom Song directory has changed. Fetching new songs. Sorting song list.");
-        
-                // Get LastWriteTimes
-                var Epoch = new DateTime(1970, 1, 1);
-                string[] directories = Directory.GetDirectories(customSongsPath);
-                //_log.Debug("Directories: " + directories);
-                foreach (string dir in directories)
-                {
-                    // Flip slashes, match SongLoaderPlugin
-                    string slashed_dir = dir.Replace("\\", "/");
-
-                   //_log.Debug("Fetching LastWriteTime for {0}", slashed_dir);
-                    _cachedLastWriteTimes[slashed_dir] = (File.GetLastWriteTimeUtc(dir) - Epoch).TotalMilliseconds;
-                }
-
-                // Update song Infos
-                if (updateSongInfos)
-                {
-                    this.UpdateSongInfos();
-                }
+            string[] directories = Directory.GetDirectories(customSongsPath);
 
-                // Get new songs
-                _cachedCustomSongDirLastWriteTIme = currentLastWriteTIme;
-                _cachedSortMode = _settings.sortMode;
-                _originalSongs = this._beatSaberSongAccessor.AcquireSongList();
-                this.ProcessSongList();
-            }
-            else if (_settings.sortMode != _cachedSortMode)
-            {
-                _log.Debug("Sort mode has changed.  Sorting song list.");
-                _cachedSortMode = _settings.sortMode;
-                this.ProcessSongList();
-            }
-            else
+            // Get LastWriteTimes
+            var Epoch = new DateTime(1970, 1, 1);
+            foreach (string dir in directories)
             {
-                _log.Debug("Songs List and/or sort mode has not changed.");
+                // Flip slashes, match SongLoaderPlugin
+                string slashed_dir = dir.Replace("\\", "/");
+
+                //_log.Debug("Fetching LastWriteTime for {0}", slashed_dir);
+                _cachedLastWriteTimes[slashed_dir] = (File.GetLastWriteTimeUtc(dir) - Epoch).TotalMilliseconds;
             }
+
+            // Update song Infos
+            this.UpdateSongInfos(gameplayMode);
+                                
+            this.ProcessSongList();                       
         }
 
         /// <summary>
         /// Get the song infos from SongLoaderPluging
         /// </summary>
-        private void UpdateSongInfos()
+        private void UpdateSongInfos(GameplayMode gameplayMode)
         {
-            _log.Debug("Attempting to fetch song infos from song loader plugin.");
-            _customSongInfos = SongLoaderPlugin.SongLoader.CustomSongInfos;
-            _levelIdToCustomSongInfo = _customSongInfos.ToDictionary(x => x.levelId, x => x);
+            _log.Trace("UpdateSongInfos for Gameplay Mode {0}", gameplayMode);
 
-            /*_customSongInfos.ForEach(x =>
-            {
-                _log.Debug("path={0}", x.levelId);
-            });*/
+            SongLoaderPlugin.OverrideClasses.CustomLevelCollectionsForGameplayModes collections = SongLoaderPlugin.SongLoader.Instance.GetPrivateField<SongLoaderPlugin.OverrideClasses.CustomLevelCollectionsForGameplayModes>("_customLevelCollectionsForGameplayModes");
+            _gameplayModeCollection = collections.GetCollection(gameplayMode) as SongLoaderPlugin.OverrideClasses.CustomLevelCollectionSO;
+            _originalSongs = collections.GetLevels(gameplayMode).ToList();
+            _sortedSongs = _originalSongs;
+            _levelIdToCustomLevel = SongLoader.CustomLevels.ToDictionary(x => x.levelID, x => x);
+
+            _log.Debug("Song Browser knows about {0} songs from SongLoader...", _sortedSongs.Count);
         }
         
         /// <summary>
@@ -136,80 +114,89 @@ namespace SongBrowserPlugin
         /// </summary>
         private void ProcessSongList()
         {
-            _log.Debug("ProcessSongList()");
-            Stopwatch stopwatch = Stopwatch.StartNew();
+            _log.Trace("ProcessSongList()");
 
             // Weights used for keeping the original songs in order
             // Invert the weights from the game so we can order by descending and make LINQ work with us...
             /*  Level4, Level2, Level9, Level5, Level10, Level6, Level7, Level1, Level3, Level8, */
             Dictionary<string, int> weights = new Dictionary<string, int>
             {
-                ["Level4"] = 10,
-                ["Level2"] = 9,
-                ["Level9"] = 8,
-                ["Level5"] = 7,
-                ["Level10"] = 6,
-                ["Level6"] = 5,
-                ["Level7"] = 4,
-                ["Level1"] = 3,
-                ["Level3"] = 2,
-                ["Level8"] = 1
+                ["Level4"] = 11,
+                ["Level2"] = 10,
+                ["Level9"] = 9,
+                ["Level5"] = 8,
+                ["Level10"] = 7,
+                ["Level6"] = 6,
+                ["Level7"] = 5,
+                ["Level1"] = 4,
+                ["Level3"] = 3,
+                ["Level8"] = 2,
+                ["Level11"] = 1
             };
 
-            /*_originalSongs.ForEach(x =>
+            // This has come in handy many times for debugging issues with Newest.
+            /*foreach (StandardLevelSO level in _originalSongs)
             {
-                if (_levelIdToCustomSongInfo.ContainsKey(x.levelId))
+                if (_levelIdToCustomLevel.ContainsKey(level.levelID))
                 {
-                    _log.Debug("_levelIdToCustomSongInfo.HasKey({0})",  x.levelId);
+                    _log.Debug("HAS KEY: {0}", level.levelID);
                 }
                 else
                 {
-                    _log.Debug("!_levelIdToCustomSongInfo.HasKey({0})", x.levelId);
+                    _log.Debug("Missing KEY: {0}", level.levelID);
                 }
-            });*/
+            }*/
+
+            Stopwatch stopwatch = Stopwatch.StartNew();
 
             switch (_settings.sortMode)
             {
                 case SongSortMode.Favorites:
-                    _log.Debug("Sorting song list as favorites");
+                    _log.Info("Sorting song list as favorites");
                     _sortedSongs = _originalSongs
                         .AsQueryable()
-                        .OrderBy(x => _settings.favorites.Contains(x.levelId) == false)
+                        .OrderBy(x => _settings.favorites.Contains(x.levelID) == false)
                         .ThenBy(x => x.songName)
-                        .ThenBy(x => x.authorName)
+                        .ThenBy(x => x.songAuthorName)
                         .ToList();
                     break;
                 case SongSortMode.Original:
-                    _log.Debug("Sorting song list as original");
+                    _log.Info("Sorting song list as original");
                     _sortedSongs = _originalSongs
                         .AsQueryable()
-                        .OrderByDescending(x => weights.ContainsKey(x.levelId) ? weights[x.levelId] : 0)
+                        .OrderByDescending(x => weights.ContainsKey(x.levelID) ? weights[x.levelID] : 0)
                         .ThenBy(x => x.songName)
                         .ToList();
                     break;
                 case SongSortMode.Newest:
-                    _log.Debug("Sorting song list as newest.");
+                    _log.Info("Sorting song list as newest.");
                     _sortedSongs = _originalSongs
                         .AsQueryable()
-                        .OrderBy(x => weights.ContainsKey(x.levelId) ? weights[x.levelId] : 0)
-                        .ThenByDescending(x => x.levelId.StartsWith("Level") ? weights[x.levelId] : _cachedLastWriteTimes[_levelIdToCustomSongInfo[x.levelId].path])
+                        .OrderBy(x => weights.ContainsKey(x.levelID) ? weights[x.levelID] : 0)
+                        .ThenByDescending(x => x.levelID.StartsWith("Level") ? weights[x.levelID] : _cachedLastWriteTimes[_levelIdToCustomLevel[x.levelID].customSongInfo.path])
+                        .ToList();
+                    break;
+                case SongSortMode.Author:
+                    _log.Info("Sorting song list by author");
+                    _sortedSongs = _originalSongs
+                        .AsQueryable()
+                        .OrderBy(x => x.songAuthorName)
+                        .ThenBy(x => x.songName)
                         .ToList();
                     break;
                 case SongSortMode.Default:
                 default:
-                    _log.Debug("Sorting song list as default");
+                    _log.Info("Sorting song list as default (songName)");
                     _sortedSongs = _originalSongs
                         .AsQueryable()
-                        .OrderBy(x => x.authorName)
-                        .ThenBy(x => x.songName)
+                        .OrderBy(x => x.songName)
+                        .ThenBy(x => x.songAuthorName)
                         .ToList();
                     break;
             }
 
             stopwatch.Stop();
             _log.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);
-
-            this._beatSaberSongAccessor.OverwriteBeatSaberSongList(_sortedSongs);
         }        
     }
 }

+ 11 - 14
SongBrowserPlugin/SongBrowserPlugin.csproj

@@ -9,7 +9,7 @@
     <AppDesignerFolder>Properties</AppDesignerFolder>
     <RootNamespace>SongBrowserPlugin</RootNamespace>
     <AssemblyName>SongBrowserPlugin</AssemblyName>
-    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <LangVersion>6</LangVersion>
     <TargetFrameworkProfile>
@@ -24,6 +24,7 @@
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
+    <Prefer32Bit>false</Prefer32Bit>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
@@ -33,9 +34,10 @@
     <DefineConstants>TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
+    <Prefer32Bit>false</Prefer32Bit>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+    <Reference Include="Assembly-CSharp">
       <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\Assembly-CSharp.dll</HintPath>
     </Reference>
     <Reference Include="IllusionPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
@@ -44,12 +46,7 @@
     <Reference Include="SongLoaderPlugin">
       <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Plugins\SongLoaderPlugin.dll</HintPath>
     </Reference>
-    <Reference Include="System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\System.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\System.Core.dll</HintPath>
-    </Reference>
+    <Reference Include="System" />
     <Reference Include="System.Data" />
     <Reference Include="System.Xml" />
     <Reference Include="TextMeshPro-1.0.55.2017.1.0b12, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
@@ -79,19 +76,19 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="DataAccess\BeatSaberSongList.cs" />
-    <Compile Include="DataAccess\IBeatSaberSongList.cs" />
     <Compile Include="Logger.cs" />
-    <Compile Include="ReflectionUtil.cs" />
-    <Compile Include="SongBrowserMasterViewController.cs" />
+    <Compile Include="SongBrowserApplication.cs" />
     <Compile Include="Plugin.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="SongBrowserModel.cs" />
-    <Compile Include="SongBrowserSettings.cs" />
-    <Compile Include="UIBuilder.cs" />
+    <Compile Include="DataAccess\SongBrowserSettings.cs" />
+    <Compile Include="UI\SongBrowserUI.cs" />
+    <Compile Include="UI\SongSortButton.cs" />
+    <Compile Include="UI\UIBuilder.cs" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Internals\" />
+    <Folder Include="Logging\" />
   </ItemGroup>
   <ItemGroup>
     <None Include="packages.config" />

+ 2 - 2
SongBrowserPlugin/SongBrowserPlugin.sln

@@ -13,8 +13,8 @@ Global
 		Release|Any CPU = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Debug|Any CPU.Build.0 = Release|Any CPU
 		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Release|Any CPU.Build.0 = Release|Any CPU
 		{E5465EF3-B227-47BE-B7D1-624E0DAC275D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+ 482 - 0
SongBrowserPlugin/UI/SongBrowserUI.cs

@@ -0,0 +1,482 @@
+using UnityEngine;
+using System.Linq;
+using System;
+using System.Collections.Generic;
+using UnityEngine.UI;
+using HMUI;
+using VRUI;
+using SongBrowserPlugin.DataAccess;
+using System.IO;
+using SongLoaderPlugin;
+
+namespace SongBrowserPlugin.UI
+{
+    /// <summary>
+    /// Hijack the flow coordinator.  Have access to all StandardLevel easily.
+    /// </summary>
+    public class SongBrowserUI : MonoBehaviour
+    {
+        // Logging
+        public const String Name = "SongBrowserUI";
+        private Logger _log = new Logger(Name);
+
+        // Beat Saber UI Elements
+        StandardLevelSelectionFlowCoordinator _levelSelectionFlowCoordinator;
+        StandardLevelListViewController _levelListViewController;
+        StandardLevelDetailViewController _levelDetailViewController;
+        StandardLevelDifficultyViewController _levelDifficultyViewController;
+        StandardLevelSelectionNavigationController _levelSelectionNavigationController;
+
+        // New UI Elements
+        private List<SongSortButton> _sortButtonGroup;
+        private Button _addFavoriteButton;
+        private String _addFavoriteButtonText = null;
+        private SimpleDialogPromptViewController _simpleDialogPromptViewControllerPrefab;
+        private SimpleDialogPromptViewController _deleteDialog;
+        private Button _deleteButton;
+
+        // Debug
+        private int _sortButtonLastPushedIndex = 0;
+        private int _lastRow = 0;
+
+        // Model
+        private SongBrowserModel _model;
+
+        /// <summary>
+        /// Constructor
+        /// </summary>
+        public SongBrowserUI() : base()
+        {
+            if (_model == null)
+            {
+                _model = new SongBrowserModel();
+            }
+            _model.Init();
+            _sortButtonLastPushedIndex = (int)(_model.Settings.sortMode);
+        }
+
+        /// <summary>
+        /// Builds the UI for this plugin.
+        /// </summary>
+        public void CreateUI()
+        {
+            _log.Trace("CreateUI()");
+            try
+            {
+                if (_levelSelectionFlowCoordinator == null)
+                {
+                    _levelSelectionFlowCoordinator = Resources.FindObjectsOfTypeAll<StandardLevelSelectionFlowCoordinator>().First();
+                }
+
+                if (_levelListViewController == null)
+                {
+                    _levelListViewController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelListViewController>("_levelListViewController");
+                }
+
+                if (_levelDetailViewController == null)
+                {
+                    _levelDetailViewController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelDetailViewController>("_levelDetailViewController");
+                }
+
+                if (_levelSelectionNavigationController == null)
+                {
+                    _levelSelectionNavigationController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelSelectionNavigationController>("_levelSelectionNavigationController");
+                }
+
+                if (_levelDifficultyViewController == null)
+                {
+                    _levelDifficultyViewController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelDifficultyViewController>("_levelDifficultyViewController");
+                }
+
+                _simpleDialogPromptViewControllerPrefab = Resources.FindObjectsOfTypeAll<SimpleDialogPromptViewController>().First();
+
+                this._deleteDialog = UnityEngine.Object.Instantiate<SimpleDialogPromptViewController>(this._simpleDialogPromptViewControllerPrefab);
+                this._deleteDialog.gameObject.SetActive(false);
+
+                this.CreateUIElements();
+
+                _levelListViewController.didSelectLevelEvent += OnDidSelectLevelEvent;
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception during CreateUI: ", e);
+            }
+        }
+
+        /// <summary>
+        /// Builds the SongBrowser UI
+        /// </summary>
+        private void CreateUIElements()
+        {
+            _log.Trace("CreateUIElements");
+
+            try
+            {                               
+                RectTransform rect = this._levelSelectionNavigationController.transform as RectTransform;
+
+                // Create Sorting Songs By-Buttons
+                _log.Debug("Creating sort by buttons...");
+
+                System.Action<SongSortMode> onSortButtonClickEvent = delegate (SongSortMode sortMode) {
+                    _log.Debug("Sort button - {0} - pressed.", sortMode.ToString());
+                    SongBrowserModel.LastSelectedLevelId = null;
+
+                    _model.Settings.sortMode = sortMode;
+                    _model.Settings.Save();
+                    UpdateSongList();
+                    RefreshSongList();
+                };
+
+                _sortButtonGroup = new List<SongSortButton>
+                {
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Favorite", 3, "AllDirectionsIcon", 66, 74.5f, 16f, 5f, SongSortMode.Favorites, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Song", 3, "AllDirectionsIcon", 50f, 74.5f, 16f, 5f, SongSortMode.Default, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Author", 3, "AllDirectionsIcon", 34f, 74.5f, 16f, 5f, SongSortMode.Author, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Original", 3, "AllDirectionsIcon", 18f, 74.5f, 16f, 5f, SongSortMode.Original, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Newest", 3, "AllDirectionsIcon", 2f, 74.5f, 16f, 5f, SongSortMode.Newest, onSortButtonClickEvent),
+                };
+
+                // Creaate Add to Favorites Button
+                _log.Debug("Creating add to favorites button...");
+
+                RectTransform transform = this._levelDetailViewController.transform as RectTransform;
+                _addFavoriteButton = UIBuilder.CreateUIButton(transform, "QuitButton", SongBrowserApplication.Instance.ButtonTemplate);
+                (_addFavoriteButton.transform as RectTransform).anchoredPosition = new Vector2(40f, 5.75f);
+                (_addFavoriteButton.transform as RectTransform).sizeDelta = new Vector2(10f, 10f);
+                UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
+                UIBuilder.SetButtonTextSize(ref _addFavoriteButton, 3);
+                UIBuilder.SetButtonIconEnabled(ref _addFavoriteButton, false);
+                _addFavoriteButton.onClick.RemoveAllListeners();
+                _addFavoriteButton.onClick.AddListener(delegate () {
+                    ToggleSongInFavorites();
+                });
+
+                if (_addFavoriteButtonText == null)
+                {
+                    IStandardLevel level = this._levelListViewController.selectedLevel;
+                    if (level != null)
+                    {
+                        RefreshAddFavoriteButton(level.levelID);
+                    }                    
+                }
+
+                // Create delete button
+                _log.Debug("Creating delete button...");
+
+                transform = this._levelDetailViewController.transform as RectTransform;
+                _deleteButton = UIBuilder.CreateUIButton(transform, "QuitButton", SongBrowserApplication.Instance.ButtonTemplate);
+                (_deleteButton.transform as RectTransform).anchoredPosition = new Vector2(46f, 0f);
+                (_deleteButton.transform as RectTransform).sizeDelta = new Vector2(15f, 5f);
+                UIBuilder.SetButtonText(ref _deleteButton, "Delete");
+                UIBuilder.SetButtonTextSize(ref _deleteButton, 3);
+                UIBuilder.SetButtonIconEnabled(ref _deleteButton, false);
+                _deleteButton.onClick.RemoveAllListeners();
+                _deleteButton.onClick.AddListener(delegate () {
+                    HandleDeleteSelectedLevel();
+                });
+
+                RefreshUI();
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception CreateUIElements:", e);
+            }
+        }
+
+        /// <summary>
+        /// Adjust UI based on level selected.
+        /// Various ways of detecting if a level is not properly selected.  Seems most hit the first one.
+        /// </summary>
+        private void OnDidSelectLevelEvent(StandardLevelListViewController view, IStandardLevel level)
+        {
+            _log.Trace("OnDidSelectLevelEvent({0}", level.levelID);
+            if (level == null)
+            {
+                _log.Debug("No level selected?");
+                return;
+            }
+
+            if (_model.Settings == null)
+            {
+                _log.Debug("Settings not instantiated yet?");
+                return;
+            }
+
+            SongBrowserModel.LastSelectedLevelId = level.levelID;
+
+            RefreshAddFavoriteButton(level.levelID);
+        }
+
+        /// <summary>
+        /// Pop up a delete dialog.
+        /// </summary>
+        private void HandleDeleteSelectedLevel()
+        {
+            IStandardLevel level = this._levelListViewController.selectedLevel;
+            if (level == null)
+            {
+                _log.Info("No level selected, cannot delete nothing...");
+                return;
+            }
+
+            if (level.levelID.StartsWith("Level"))
+            {
+                _log.Debug("Cannot delete non-custom levels.");
+                return;
+            }
+
+            SongLoaderPlugin.OverrideClasses.CustomLevel customLevel = _model.LevelIdToCustomSongInfos[level.levelID];
+
+            this._deleteDialog.Init("Delete level warning!", String.Format("<color=#00AAFF>Permanently delete level: {0}</color>\n  Do you want to continue?", customLevel.songName), "YES", "NO");
+            this._deleteDialog.didFinishEvent += this.HandleDeleteDialogPromptViewControllerDidFinish;
+
+            this._levelSelectionNavigationController.PresentModalViewController(this._deleteDialog, null, false);
+        }
+
+        /// <summary>
+        /// Handle delete dialog resolution.
+        /// </summary>
+        /// <param name="viewController"></param>
+        /// <param name="ok"></param>
+        public void HandleDeleteDialogPromptViewControllerDidFinish(SimpleDialogPromptViewController viewController, bool ok)
+        {
+            viewController.didFinishEvent -= this.HandleDeleteDialogPromptViewControllerDidFinish;
+            if (!ok)
+            {
+                viewController.DismissModalViewController(null, false);
+            }
+            else
+            {
+                IStandardLevel level = this._levelListViewController.selectedLevel;
+                SongLoaderPlugin.OverrideClasses.CustomLevel customLevel = _model.LevelIdToCustomSongInfos[level.levelID];
+
+                viewController.DismissModalViewController(null, false);
+                _log.Debug("Deleting: {0}", customLevel.customSongInfo.path);
+                
+                FileAttributes attr = File.GetAttributes(customLevel.customSongInfo.path);
+                if (attr.HasFlag(FileAttributes.Directory))
+                    Directory.Delete(customLevel.customSongInfo.path);
+                else
+                    File.Delete(customLevel.customSongInfo.path);
+
+                SongLoaderPlugin.SongLoader.Instance.RemoveSongWithPath(customLevel.customSongInfo.path);
+            }
+        }
+
+        /// <summary>
+        /// Add/Remove song from favorites depending on if it already exists.
+        /// </summary>
+        private void ToggleSongInFavorites()
+        {
+            IStandardLevel songInfo = this._levelListViewController.selectedLevel;
+            if (_model.Settings.favorites.Contains(songInfo.levelID))
+            {
+                _log.Info("Remove {0} from favorites", songInfo.songName);
+                _model.Settings.favorites.Remove(songInfo.levelID);
+                _addFavoriteButtonText = "+1";
+            }
+            else
+            {
+                _log.Info("Add {0} to favorites", songInfo.songName);
+                _model.Settings.favorites.Add(songInfo.levelID);
+                _addFavoriteButtonText = "-1";                
+            }
+
+            UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
+
+            _model.Settings.Save();
+        }
+
+        /// <summary>
+        /// Helper to quickly refresh add to favorites button
+        /// </summary>
+        /// <param name="levelId"></param>
+        private void RefreshAddFavoriteButton(String levelId)
+        {
+            if (levelId == null)
+            {
+                _addFavoriteButtonText = "0";
+                return;
+            }
+
+            if (_model.Settings.favorites.Contains(levelId))
+            {
+                _addFavoriteButtonText = "-1";
+            }
+            else
+            {
+                _addFavoriteButtonText = "+1";                
+            }
+
+            UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
+        }
+
+        /// <summary>
+        /// Adjust the UI colors.
+        /// </summary>
+        public void RefreshUI()
+        {
+            // So far all we need to refresh is the sort buttons.
+            foreach (SongSortButton sortButton in _sortButtonGroup)
+            {
+                UIBuilder.SetButtonBorder(ref sortButton.Button, Color.black);
+                if (sortButton.SortMode == _model.Settings.sortMode)
+                {
+                    UIBuilder.SetButtonBorder(ref sortButton.Button, Color.red);
+                }
+            }            
+        }
+
+        /// <summary>
+        /// Try to refresh the song list.  Broken for now.
+        /// </summary>
+        public void RefreshSongList()
+        {
+            _log.Info("Refreshing the song list view.");
+            try
+            {
+                if (_model.SortedSongList == null)
+                {
+                    _log.Debug("Songs are not sorted yet, nothing to refresh.");
+                    return;
+                }
+
+                StandardLevelSO[] levels = _model.SortedSongList.ToArray();
+                StandardLevelListViewController songListViewController = this._levelSelectionFlowCoordinator.GetPrivateField<StandardLevelListViewController>("_levelListViewController");
+                StandardLevelListTableView _songListTableView = songListViewController.GetComponentInChildren<StandardLevelListTableView>();
+                ReflectionUtil.SetPrivateField(_songListTableView, "_levels", levels);
+                ReflectionUtil.SetPrivateField(songListViewController, "_levels", levels);            
+                TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_songListTableView, "_tableView");
+                tableView.ReloadData();
+
+                String selectedLevelID = null;
+                if (SongBrowserModel.LastSelectedLevelId != null)
+                {
+                    selectedLevelID = SongBrowserModel.LastSelectedLevelId;
+                    _log.Debug("Scrolling to row for level ID: {0}", selectedLevelID);                    
+                }
+                else
+                {
+                    selectedLevelID = levels.FirstOrDefault().levelID;
+                }
+
+                if (levels.Any(x => x.levelID == selectedLevelID))
+                {
+                    SelectAndScrollToLevel(_songListTableView, selectedLevelID);
+                }
+                
+                RefreshUI();                
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception refreshing song list:", e);
+            }
+        }
+
+        private void SelectAndScrollToLevel(StandardLevelListTableView table, string levelID)
+        {
+            int row = table.RowNumberForLevelID(levelID);
+            TableView _tableView = table.GetComponentInChildren<TableView>();
+            _tableView.SelectRow(row, true);
+            _tableView.ScrollToRow(row, true);
+        }
+
+        /// <summary>
+        /// Helper for updating the model (which updates the song list)c
+        /// </summary>
+        public void UpdateSongList()
+        {
+            _log.Trace("UpdateSongList()");
+
+            GameplayMode gameplayMode = _levelSelectionFlowCoordinator.GetPrivateField<GameplayMode>("_gameplayMode");
+            _model.UpdateSongLists(gameplayMode);                        
+        }
+
+        /// <summary>
+        /// Not normally called by the game-engine.  Dependent on SongBrowserApplication to call it.
+        /// </summary>
+        public void LateUpdate()
+        {
+            if (this._levelListViewController.isInViewControllerHierarchy)
+            {
+                CheckDebugUserInput();
+            }
+        }
+
+        /// <summary>
+        /// Map some key presses directly to UI interactions to make testing easier.
+        /// </summary>
+        private void CheckDebugUserInput()
+        {
+            try
+            {
+                // back
+                if (Input.GetKeyDown(KeyCode.Escape))
+                {
+                    this._levelSelectionNavigationController.DismissButtonWasPressed();
+                }
+
+                // cycle sort modes
+                if (Input.GetKeyDown(KeyCode.T))
+                {
+                    _sortButtonLastPushedIndex = (_sortButtonLastPushedIndex + 1) % _sortButtonGroup.Count;
+                    _sortButtonGroup[_sortButtonLastPushedIndex].Button.onClick.Invoke();
+                }
+
+                // delete
+                if (Input.GetKeyDown(KeyCode.D))
+                {
+                    if (_deleteDialog.isInViewControllerHierarchy)
+                    {
+                        return;
+                    }
+                    _deleteButton.onClick.Invoke();
+                }
+
+                StandardLevelListTableView levelListTableView = this._levelListViewController.GetComponentInChildren<StandardLevelListTableView>();
+
+                // z,x,c,v can be used to get into a song, b will hit continue button after song ends
+                if (Input.GetKeyDown(KeyCode.C))
+                {
+                    levelListTableView.SelectAndScrollToLevel(_model.SortedSongList[0].levelID);
+                    this._levelListViewController.HandleLevelListTableViewDidSelectRow(levelListTableView, 0);                    
+                    this._levelDifficultyViewController.HandleDifficultyTableViewDidSelectRow(null, 0);
+                    this._levelSelectionFlowCoordinator.HandleDifficultyViewControllerDidSelectDifficulty(_levelDifficultyViewController, _model.SortedSongList[0].GetDifficultyLevel(LevelDifficulty.Easy));
+                }
+
+                if (Input.GetKeyDown(KeyCode.V))
+                {
+                    this._levelSelectionFlowCoordinator.HandleLevelDetailViewControllerDidPressPlayButton(this._levelDetailViewController);
+                }
+
+                // change song index
+                if (Input.GetKeyDown(KeyCode.N))
+                {
+                    _lastRow = (_lastRow - 1) != -1 ? (_lastRow - 1) % this._model.SortedSongList.Count : 0;
+
+                    levelListTableView.SelectAndScrollToLevel(_model.SortedSongList[_lastRow].levelID);
+                    this._levelListViewController.HandleLevelListTableViewDidSelectRow(levelListTableView, _lastRow);
+                }
+
+                if (Input.GetKeyDown(KeyCode.M))
+                {
+                    _lastRow = (_lastRow + 1) % this._model.SortedSongList.Count;
+
+                    levelListTableView.SelectAndScrollToLevel(_model.SortedSongList[_lastRow].levelID);
+                    this._levelListViewController.HandleLevelListTableViewDidSelectRow(levelListTableView, _lastRow);
+                }
+
+                // add to favorites
+                if (Input.GetKeyDown(KeyCode.F))
+                {
+                    ToggleSongInFavorites();
+                }
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Debug Input caused Exception: ", e);
+            }
+        }
+    }
+}
+ 

+ 11 - 0
SongBrowserPlugin/UI/SongSortButton.cs

@@ -0,0 +1,11 @@
+using SongBrowserPlugin.DataAccess;
+using UnityEngine.UI;
+
+namespace SongBrowserPlugin.UI
+{
+    public class SongSortButton
+    {
+        public SongSortMode SortMode;
+        public Button Button;
+    }
+}

+ 208 - 0
SongBrowserPlugin/UI/UIBuilder.cs

@@ -0,0 +1,208 @@
+using UnityEngine;
+using System.Linq;
+using UnityEngine.UI;
+using TMPro;
+using VRUI;
+using SongBrowserPlugin.DataAccess;
+
+namespace SongBrowserPlugin.UI
+{
+    public static class UIBuilder
+    {
+        /// <summary>
+        /// Create an empty BeatSaber VRUI view controller.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="name"></param>
+        /// <returns></returns>
+        public static T CreateViewController<T>(string name) where T : VRUIViewController
+        {
+            T vc = new GameObject(name).AddComponent<T>();
+
+            vc.rectTransform.anchorMin = new Vector2(0f, 0f);
+            vc.rectTransform.anchorMax = new Vector2(1f, 1f);
+            vc.rectTransform.sizeDelta = new Vector2(0f, 0f);
+            vc.rectTransform.anchoredPosition = new Vector2(0f, 0f);
+
+            return vc;
+        }
+
+        /// <summary>
+        /// Create empty FlowCoordinator
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="name"></param>
+        /// <returns></returns>
+        public static T CreateFlowCoordinator<T>(string name) where T : FlowCoordinator
+        {
+            T vc = new GameObject(name).AddComponent<T>();
+
+            return vc;
+        }
+
+        /// <summary>
+        /// Clone a Unity Button into a Button we control.
+        /// </summary>
+        /// <param name="parent"></param>
+        /// <param name="buttonTemplate"></param>
+        /// <param name="buttonInstance"></param>
+        /// <returns></returns>
+        static public Button CreateUIButton(RectTransform parent, string buttonTemplate, Button buttonInstance)
+        {
+            if (buttonInstance == null)
+            {
+                return null;
+            }
+
+            Button btn = UnityEngine.Object.Instantiate(Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == buttonTemplate)), parent, false);
+            UnityEngine.Object.DestroyImmediate(btn.GetComponent<GameEventOnUIButtonClick>());
+            btn.onClick = new Button.ButtonClickedEvent();
+
+            return btn;
+        }
+
+        /// <summary>
+        /// Generic create sort button.
+        /// </summary>
+        /// <param name="rect"></param>
+        /// <param name="templateButtonName"></param>
+        /// <param name="buttonText"></param>
+        /// <param name="iconName"></param>
+        /// <param name="x"></param>
+        /// <param name="y"></param>
+        /// <param name="w"></param>
+        /// <param name="h"></param>
+        /// <param name="action"></param>
+        public static SongSortButton CreateSortButton(RectTransform rect, string templateButtonName, string buttonText, float fontSize, string iconName, float x, float y, float w, float h, SongSortMode sortMode, System.Action<SongSortMode> onClickEvent)
+        {
+            SongSortButton sortButton = new SongSortButton();
+            Button newButton = UIBuilder.CreateUIButton(rect, templateButtonName, SongBrowserApplication.Instance.ButtonTemplate);
+
+            newButton.interactable = true;
+            (newButton.transform as RectTransform).anchoredPosition = new Vector2(x, y);
+            (newButton.transform as RectTransform).sizeDelta = new Vector2(w, h);
+
+            UIBuilder.SetButtonText(ref newButton, buttonText);
+            //UIBuilder.SetButtonIconEnabled(ref _originalButton, false);
+            UIBuilder.SetButtonIcon(ref newButton, SongBrowserApplication.Instance.CachedIcons[iconName]);
+            UIBuilder.SetButtonTextSize(ref newButton, fontSize);
+
+            newButton.onClick.RemoveAllListeners();
+            newButton.onClick.AddListener(delegate ()
+            {
+                onClickEvent(sortMode);
+            });
+            
+            sortButton.Button = newButton;
+            sortButton.SortMode = sortMode;
+
+            return sortButton;
+        }
+
+        /// <summary>
+        /// Generate TextMesh.
+        /// </summary>
+        /// <param name="parent"></param>
+        /// <param name="text"></param>
+        /// <param name="position"></param>
+        /// <returns></returns>
+        static public TextMeshProUGUI CreateText(RectTransform parent, string text, Vector2 position)
+        {
+            TextMeshProUGUI textMesh = new GameObject("TextMeshProUGUI_GO").AddComponent<TextMeshProUGUI>();
+            textMesh.rectTransform.SetParent(parent, false);
+            textMesh.text = text;
+            textMesh.fontSize = 4;
+            textMesh.color = Color.white;
+            textMesh.font = Resources.Load<TMP_FontAsset>("Teko-Medium SDF No Glow");
+            textMesh.rectTransform.anchorMin = new Vector2(0.5f, 1f);
+            textMesh.rectTransform.anchorMax = new Vector2(0.5f, 1f);
+            textMesh.rectTransform.sizeDelta = new Vector2(60f, 10f);
+            textMesh.rectTransform.anchoredPosition = position;
+
+            return textMesh;
+        }
+
+        /// <summary>
+        /// Adjust a Button text.
+        /// </summary>
+        /// <param name="button"></param>
+        /// <param name="text"></param>
+        static public void SetButtonText(ref Button button, string text)
+        {
+            if (button.GetComponentInChildren<TextMeshProUGUI>() != null)
+            {
+
+                button.GetComponentInChildren<TextMeshProUGUI>().text = text;
+            }
+
+        }
+
+        /// <summary>
+        /// Adjust button text size.
+        /// </summary>
+        /// <param name="button"></param>
+        /// <param name="fontSize"></param>
+        static public void SetButtonTextSize(ref Button button, float fontSize)
+        {
+            if (button.GetComponentInChildren<TextMeshProUGUI>() != null)
+            {
+                button.GetComponentInChildren<TextMeshProUGUI>().fontSize = fontSize;
+            }
+
+
+        }
+
+        /// <summary>
+        /// Set a button icon.
+        /// </summary>
+        /// <param name="button"></param>
+        /// <param name="icon"></param>
+        static public void SetButtonIcon(ref Button button, Sprite icon)
+        {
+            if (button.GetComponentsInChildren<UnityEngine.UI.Image>().Count() > 1)
+            {
+                button.GetComponentsInChildren<Image>().First(x => x.name == "Icon").sprite = icon;
+            }            
+        }
+
+        /// <summary>
+        /// Disable a button icon.
+        /// </summary>
+        /// <param name="button"></param>
+        /// <param name="enabled"></param>
+        static public void SetButtonIconEnabled(ref Button button, bool enabled)
+        {
+            if (button.GetComponentsInChildren<UnityEngine.UI.Image>().Count() > 1)
+            {
+                button.GetComponentsInChildren<UnityEngine.UI.Image>()[1].enabled = enabled;
+            }
+        }
+
+        /// <summary>
+        /// Adjust button background color.
+        /// </summary>
+        /// <param name="button"></param>
+        /// <param name="background"></param>
+        static public void SetButtonBackground(ref Button button, Sprite background)
+        {
+            if (button.GetComponentsInChildren<Image>().Any())
+            {
+                button.GetComponentsInChildren<UnityEngine.UI.Image>()[0].sprite = background;
+            }
+
+        }
+
+        /// <summary>
+        /// Adjust button border.
+        /// </summary>
+        /// <param name="button"></param>
+        /// <param name="color"></param>
+        static public void SetButtonBorder(ref Button button, Color color)
+        {
+            if (button.GetComponentsInChildren<Image>().Any())
+            {
+                button.GetComponentsInChildren<UnityEngine.UI.Image>()[0].color = color;
+            }
+        }
+    }
+}

+ 0 - 93
SongBrowserPlugin/UIBuilder.cs

@@ -1,93 +0,0 @@
-using UnityEngine;
-using System.Linq;
-using UnityEngine.UI;
-using TMPro;
-
-namespace SongBrowserPlugin
-{
-    public static class UIBuilder
-    {
-        static public Button CreateUIButton(RectTransform parent, string buttonTemplate, Button buttonInstance)
-        {
-            if (buttonInstance == null)
-            {
-                return null;
-            }
-
-            Button btn = UnityEngine.Object.Instantiate(Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == buttonTemplate)), parent, false);
-            UnityEngine.Object.DestroyImmediate(btn.GetComponent<GameEventOnUIButtonClick>());
-            btn.onClick = new Button.ButtonClickedEvent();
-
-            return btn;
-        }
-
-        static public TextMeshProUGUI CreateText(RectTransform parent, string text, Vector2 position)
-        {
-            TextMeshProUGUI textMesh = new GameObject("TextMeshProUGUI_GO").AddComponent<TextMeshProUGUI>();
-            textMesh.rectTransform.SetParent(parent, false);
-            textMesh.text = text;
-            textMesh.fontSize = 4;
-            textMesh.color = Color.white;
-            textMesh.font = Resources.Load<TMP_FontAsset>("Teko-Medium SDF No Glow");
-            textMesh.rectTransform.anchorMin = new Vector2(0.5f, 1f);
-            textMesh.rectTransform.anchorMax = new Vector2(0.5f, 1f);
-            textMesh.rectTransform.sizeDelta = new Vector2(60f, 10f);
-            textMesh.rectTransform.anchoredPosition = position;
-
-            return textMesh;
-        }
-
-        static public void SetButtonText(ref Button button, string text)
-        {
-            if (button.GetComponentInChildren<TextMeshProUGUI>() != null)
-            {
-
-                button.GetComponentInChildren<TextMeshProUGUI>().text = text;
-            }
-
-        }
-
-        static public void SetButtonTextSize(ref Button button, float fontSize)
-        {
-            if (button.GetComponentInChildren<TextMeshProUGUI>() != null)
-            {
-                button.GetComponentInChildren<TextMeshProUGUI>().fontSize = fontSize;
-            }
-
-
-        }
-
-        static public void SetButtonIcon(ref Button button, Sprite icon)
-        {
-            if (button.GetComponentsInChildren<UnityEngine.UI.Image>().Count() > 1)
-            {
-                button.GetComponentsInChildren<UnityEngine.UI.Image>()[1].sprite = icon;
-            }            
-        }
-
-        static public void SetButtonIconEnabled(ref Button button, bool enabled)
-        {
-            if (button.GetComponentsInChildren<UnityEngine.UI.Image>().Count() > 1)
-            {
-                button.GetComponentsInChildren<UnityEngine.UI.Image>()[1].enabled = enabled;
-            }
-        }
-
-        static public void SetButtonBackground(ref Button button, Sprite background)
-        {
-            if (button.GetComponentsInChildren<Image>().Any())
-            {
-                button.GetComponentsInChildren<UnityEngine.UI.Image>()[0].sprite = background;
-            }
-
-        }
-
-        static public void SetButtonBorder(ref Button button, Color color)
-        {
-            if (button.GetComponentsInChildren<Image>().Any())
-            {
-                button.GetComponentsInChildren<UnityEngine.UI.Image>()[0].color = color;
-            }
-        }
-    }
-}

+ 4 - 7
SongBrowserPluginTest/SongBrowserPluginTest.csproj

@@ -9,7 +9,7 @@
     <AppDesignerFolder>Properties</AppDesignerFolder>
     <RootNamespace>SongBrowserPluginTest</RootNamespace>
     <AssemblyName>SongBrowserPluginTest</AssemblyName>
-    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <LangVersion>6</LangVersion>
     <TargetFrameworkProfile>
@@ -24,6 +24,7 @@
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
+    <Prefer32Bit>false</Prefer32Bit>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
@@ -33,6 +34,7 @@
     <DefineConstants>TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
+    <Prefer32Bit>false</Prefer32Bit>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
@@ -44,12 +46,7 @@
     <Reference Include="SongLoaderPlugin">
       <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Plugins\SongLoaderPlugin.dll</HintPath>
     </Reference>
-    <Reference Include="System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\System.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
-      <HintPath>D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\System.Core.dll</HintPath>
-    </Reference>
+    <Reference Include="System" />
     <Reference Include="System.Data" />
     <Reference Include="System.Xml" />
     <Reference Include="TextMeshPro-1.0.55.2017.1.0b12, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">

+ 4 - 32
SongBrowserPluginTest/Tests/SongBrowserModelTests.cs

@@ -7,46 +7,18 @@ using SongBrowserPlugin;
 
 namespace SongBrowserPluginTests
 {
-    class MockBeatSaberSongList : SongBrowserPlugin.DataAccess.IBeatSaberSongList
-    {
-        List<LevelStaticData> testList;
-
-        public MockBeatSaberSongList()
-        {
-            testList = new List<LevelStaticData>();
-            for (int i = 0; i < 10000; i++)
-            {
-                LevelStaticData level = new LevelStaticData();
-                SongBrowserPlugin.ReflectionUtil.SetPrivateField(level, "_songName", "SongName" + i);
-                SongBrowserPlugin.ReflectionUtil.SetPrivateField(level, "_authorName", "AuthorName" + i);
-                SongBrowserPlugin.ReflectionUtil.SetPrivateField(level, "_levelId", "LevelId" + i);
-                testList.Add(level);
-            }
-
-        }
-
-        public List<LevelStaticData> AcquireSongList()
-        {
-            return testList;
-        }
-
-        public void OverwriteBeatSaberSongList(List<LevelStaticData> newSongList)
-        {
-            return;
-        }
-    }
-
+    
     class SongBrowserModelTests : ISongBrowserTest
     {
         private Logger _log = new Logger("SongBrowserModelTests");
 
         public void RunTest()
         {
-            _log.Info("SongBrowserModelTests - All tests in Milliseconds");
+            /*_log.Info("SongBrowserModelTests - All tests in Milliseconds");
 
             Stopwatch stopwatch = Stopwatch.StartNew();
             SongBrowserModel model = new SongBrowserModel();
-            model.Init(new MockBeatSaberSongList());
+            model.Init();
             stopwatch.Stop();
             _log.Info("Created a bunch of LevelStaticData: {0}", stopwatch.ElapsedMilliseconds);
 
@@ -82,7 +54,7 @@ namespace SongBrowserPluginTests
                 var m3 = model.SortedSongList.ToArray();
             }
             stopwatch.Stop();
-            _log.Info("Converting big list into array a bunch of times: {0}", stopwatch.ElapsedMilliseconds);
+            _log.Info("Converting big list into array a bunch of times: {0}", stopwatch.ElapsedMilliseconds);*/
         }
     }
 }