Browse Source

Massive refactor for BeatSaber update. (#3)

* Refactored to work with the new Beat Saber update.
Mostly working.  Still some bugs to work out.

* Delete idea of DataAccessor.
Disable the in-game test runner for now.

* Everything appears to be working.
Lots of Debug code included in this commit.

* Big cleanup. Marked beta.
Moved some UI files.  Removed all need for ReflectionUtils (SongLoader IPA
patch adds what we need).
Removed unused imports;

* More renames, remove dead code, general cleanup.

* Actually enable the delete button.
Rename function to proper name.
More cleanup.

* Enable delete through SongLoaderPlugin.

* Actually delete directory from filesystem on song delete.

* Right aligned sort buttons in the UI.
Make Add to favorite button large, aligned with PlayButton.
Make delete button smaller, more out of the way.

* Cleaned up logging.

* Updated README. Added a screenshot.

* Fix set icon method.

* Hack fix for remembering last selected song between levels.

* Protect against a level going missing while song is playing.
Stephen Damm 5 years ago
parent
commit
09c84b25f7

+ 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);
-    }
-}

+ 4 - 6
SongBrowserPlugin/SongBrowserSettings.cs

@@ -4,7 +4,7 @@ using System.IO;
 using System.Xml.Serialization;
 
 
-namespace SongBrowserPlugin
+namespace SongBrowserPlugin.DataAccess
 {
     [Serializable]
     public enum SongSortMode
@@ -23,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.
@@ -49,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();
@@ -68,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();

+ 41 - 6
SongBrowserPlugin/Logger.cs

@@ -3,9 +3,18 @@
 
 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)
@@ -19,38 +28,64 @@ namespace SongBrowserPlugin
             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();
         }
 

+ 3 - 15
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 "v2.0"; }
+			get { return "v2.0-beta"; }
 		}
 		
 		public void OnApplicationStart()
@@ -47,10 +38,7 @@ namespace SongBrowserPlugin
         public void OnLevelWasLoaded(int level)
 		{
             //Console.WriteLine("OnLevelWasLoaded=" + level);            
-            //if (level != SongBrowserMasterViewController.MenuIndex) return;
-            //SongBrowserMasterViewController.OnLoad();
-
-            if (level != SongBrowserMasterViewController.MenuIndex) return;
+            if (level != SongBrowserApplication.MenuIndex) return;
             SongBrowserApplication.OnLoad();
         }
 

+ 0 - 54
SongBrowserPlugin/ReflectionUtil.cs

@@ -1,54 +0,0 @@
-using System;
-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;
-        }
-
-        public static object InvokeStaticMethod(Type t, string methodName, params object[] args)
-        {
-            var mi = t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
-            if (mi != null)
-            {
-                return mi.Invoke(obj: null, parameters: args);
-            }
-            return null;
-        }       
-    }
-}

+ 77 - 105
SongBrowserPlugin/SongBrowserApplication.cs

@@ -1,41 +1,39 @@
-using SongLoaderPlugin;
-using System;
-using System.Collections.Generic;
+using UnityEngine;
 using System.Linq;
-using System.Reflection;
-using UnityEngine;
+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 static SongBrowserApplication Instance;
+        public const int MenuIndex = 1;
 
-        private Logger _log = new Logger("SongBrowserMasterViewController");
+        public static SongBrowserApplication Instance;
 
-        // BeatSaber UI Elements
-        private SongSelectionMasterViewController _songSelectionMasterView;
-        private SongDetailViewController _songDetailViewController;
-        private SongListViewController _songListViewController;
-        private MainMenuViewController _mainMenuViewController;
-        private MenuMasterViewController _menuMasterViewController;
+        private Logger _log = new Logger("SongBrowserApplication");
 
         // Song Browser UI Elements
-        private SongBrowserMasterViewController _songBrowserMasterViewController;
-
+        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>();
-            //DontDestroyOnLoad(gameObject);
         }
 
         /// <summary>
@@ -43,135 +41,109 @@ namespace SongBrowserPlugin
         /// </summary>
         private void Awake()
         {
+            _log.Trace("Awake()");
+
             Instance = this;
 
-            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;
-            SongLoader.SongsLoaded.AddListener(OnSongLoaderLoadedSongs);
+            _songBrowserUI = gameObject.AddComponent<SongBrowserUI>();
         }
 
-        private void OnSongLoaderLoadedSongs()
+        /// <summary>
+        /// 
+        /// </summary>
+        public void Start()
         {
-            try
-            {
-                _log.Debug("Attempting to take over the didSelectModeEvent Button");
-                SoloModeSelectionViewController view = Resources.FindObjectsOfTypeAll<SoloModeSelectionViewController>().First();
+            _log.Trace("Start()");
 
-                if (view.didSelectModeEvent != null)
-                {
-                    Delegate[] delegates = view.didSelectModeEvent.GetInvocationList();
-                    view.didSelectModeEvent -= delegates[0] as Action<SoloModeSelectionViewController, GameplayMode>;
-                }
+            AcquireUIElements();
 
-                view.didSelectModeEvent += HandleSoloModeSelectionViewControllerDidSelectMode;
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception during OnSongLoaderLoadedSongs: " + e);
-            }
+            StartCoroutine(WaitForSongListUI());
         }
 
         /// <summary>
-        /// Bind to some UI events.
+        /// Wait for the song list to be visible to draw it.
         /// </summary>
-        /// <param name="arg0"></param>
-        /// <param name="scene"></param>
-        private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene)
+        /// <returns></returns>
+        private IEnumerator WaitForSongListUI()
         {
-            if (scene.buildIndex != SongBrowserMasterViewController.MenuIndex)
-            {
-                return;
-            }
+            _log.Trace("WaitForSongListUI()");
 
-            AcquireUIElements();
+            yield return new WaitUntil(delegate () { return Resources.FindObjectsOfTypeAll<StandardLevelSelectionFlowCoordinator>().Any(); });
+
+            _log.Debug("Found StandardLevelSelectionFlowCoordinator...");
 
-            // Clone and override the default song-browser.
-            if (_songBrowserMasterViewController == null)
+            _songBrowserUI.CreateUI();
+
+            if (SongLoaderPlugin.SongLoader.AreSongsLoaded)
             {
-                _log.Debug("Attempting to clone SongBrowserMasterViewController");
-                _songBrowserMasterViewController = UIBuilder.CreateViewController<SongBrowserMasterViewController>(SongBrowserMasterViewController.Name);
-                System.Reflection.FieldInfo[] fields = typeof(SongSelectionMasterViewController).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
-                foreach (System.Reflection.FieldInfo field in fields)
-                {
-                    //_log.Debug(field.Name);
-                    field.SetValue(_songBrowserMasterViewController, field.GetValue(_songSelectionMasterView));
-                }
+                OnSongLoaderLoadedSongs(null, SongLoader.CustomLevels);
+            }
+            else
+            {
+                SongLoader.SongsLoadedEvent += OnSongLoaderLoadedSongs;
             }
 
-            _log.Debug("Overriding Song Browser");
-            ReflectionUtil.SetPrivateField(_menuMasterViewController, "_songSelectionMasterViewController", _songBrowserMasterViewController);
+            _songBrowserUI.RefreshSongList();            
         }
 
         /// <summary>
-        /// Get a handle to the view controllers we are going to add elements to.
+        /// Only gets called once during boot of BeatSaber.  
         /// </summary>
-        public void AcquireUIElements()
+        /// <param name="loader"></param>
+        /// <param name="levels"></param>
+        private void OnSongLoaderLoadedSongs(SongLoader loader, List<CustomLevel> levels)
         {
-            _log.Debug("Acquiring important UI elements.");
-            CachedIcons = new Dictionary<String, Sprite>();
-            foreach (Sprite sprite in Resources.FindObjectsOfTypeAll<Sprite>())
-            {
-                if (CachedIcons.ContainsKey(sprite.name))
-                {
-                    continue;
-                }
-                CachedIcons.Add(sprite.name, sprite);
-            }
-
+            _log.Trace("OnSongLoaderLoadedSongs");
             try
             {
-                ButtonTemplate = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "PlayButton"));
-                _mainMenuViewController = Resources.FindObjectsOfTypeAll<MainMenuViewController>().First();
-                _menuMasterViewController = Resources.FindObjectsOfTypeAll<MenuMasterViewController>().First();
-                _songSelectionMasterView = Resources.FindObjectsOfTypeAll<SongSelectionMasterViewController>().First();
-                _songDetailViewController = Resources.FindObjectsOfTypeAll<SongDetailViewController>().First();
-                _songListViewController = Resources.FindObjectsOfTypeAll<SongListViewController>().First();
+                _songBrowserUI.UpdateSongList();
             }
             catch (Exception e)
             {
-                _log.Exception("Exception AcquireUIElements(): " + e);
+                _log.Exception("Exception during OnSongLoaderLoadedSongs: ", e);
             }
         }
 
         /// <summary>
-        /// Hijack the result of clicking into the song browser.
+        /// Get a handle to the view controllers we are going to add elements to.
         /// </summary>
-        /// <param name="viewController"></param>
-        /// <param name="gameplayMode"></param>
-        private void HandleSoloModeSelectionViewControllerDidSelectMode(SoloModeSelectionViewController viewController, GameplayMode gameplayMode)
+        public void AcquireUIElements()
         {
-            _log.Debug("Hi jacking solo mode buttons");
+            _log.Trace("AcquireUIElements()");        
             try
-            {                
-                ReflectionUtil.SetPrivateField(_menuMasterViewController, "_gameplayMode", gameplayMode);
-                ReflectionUtil.SetPrivateField(_menuMasterViewController, "_songSelectionMasterViewController", _songBrowserMasterViewController);
-
-                GameBuildMode gameBuildMode = ReflectionUtil.GetPrivateField<GameBuildMode>(_menuMasterViewController, "_gameBuildMode");
-                GameObject dismissButton = ReflectionUtil.GetPrivateField<GameObject>(_songSelectionMasterView, "_dismissButton");
-                ReflectionUtil.SetPrivateField(_songBrowserMasterViewController, "_dismissButton", dismissButton);
-
-                LevelStaticData[] levelsForGameplayMode = _menuMasterViewController.GetLevelsForGameplayMode(gameplayMode, gameBuildMode);
-
-                bool _canUseGlobalLeaderboards = ReflectionUtil.GetPrivateField<bool>(_menuMasterViewController, "_canUseGlobalLeaderboards");
-                bool showDismissButton = true;
-                bool useLocalLeaderboards = !_canUseGlobalLeaderboards || gameplayMode == GameplayMode.PartyStandard;
-                bool showPlayerStats = ArePlayerStatsUsed(gameplayMode);
+            {
+                CachedIcons = new Dictionary<String, Sprite>();
+                foreach (Sprite sprite in Resources.FindObjectsOfTypeAll<Sprite>())
+                {
+                    if (CachedIcons.ContainsKey(sprite.name))
+                    {
+                        continue;
+                    }
+                    CachedIcons.Add(sprite.name, sprite);
+                }
 
-                _songBrowserMasterViewController.Init(null, LevelStaticData.Difficulty.Easy, levelsForGameplayMode, useLocalLeaderboards, showDismissButton, showPlayerStats, gameplayMode);
-                viewController.PresentModalViewController(_songBrowserMasterViewController, null, false);
-                _songSelectionMasterView = _songBrowserMasterViewController;
+                ButtonTemplate = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "PlayButton"));
 
-                _log.Debug("Success hijacking ...");
+                // 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 replacing in-game song browser: {0}\n{1}", e.Message, e.StackTrace);
+                _log.Exception("Exception AcquireUIElements(): ", e);
             }
         }
 
-        // Token: 0x06000C43 RID: 3139 RVA: 0x00035DE8 File Offset: 0x00033FE8
-        private bool ArePlayerStatsUsed(GameplayMode gameplayMode)
+        /// <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)
         {
-            return gameplayMode == GameplayMode.SoloStandard || gameplayMode == GameplayMode.SoloNoArrows || gameplayMode == GameplayMode.SoloOneSaber;
+            _log.Trace("HandleSoloModeSelectionViewControllerDidSelectMode()");
+            this._songBrowserUI.RefreshSongList();
         }
 
         /// <summary>
@@ -187,7 +159,7 @@ namespace SongBrowserPlugin
         /// <summary>
         /// Map some key presses directly to UI interactions to make testing easier.
         /// </summary>
-        private void Update()
+        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))
@@ -197,7 +169,7 @@ namespace SongBrowserPlugin
 
             if (Input.GetKeyDown(KeyCode.X))
             {
-                InvokeBeatSaberButton("FreePlayButton");
+                InvokeBeatSaberButton("StandardButton");
             }
 
             if (Input.GetKeyDown(KeyCode.B))

+ 0 - 503
SongBrowserPlugin/SongBrowserMasterViewController.cs

@@ -1,503 +0,0 @@
-using UnityEngine;
-using System.Linq;
-using System;
-using System.Collections.Generic;
-using UnityEngine.SceneManagement;
-using UnityEngine.UI;
-using HMUI;
-using System.IO;
-
-namespace SongBrowserPlugin
-{
-    public class SongSortButton
-    {
-        public SongSortMode SortMode;
-        public Button Button;
-    }
-
-    public class SongBrowserMasterViewController : SongSelectionMasterViewController
-    {
-        public const String Name = "SongBrowserMasterViewController";
-
-        public const int MenuIndex = 1;
-
-        private Logger _log = new Logger(Name);
-       
-        // 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;
-
-        // Model
-        private SongBrowserModel _model;
-
-        private bool _uiInitialized;
-
-        /// <summary>
-        /// Unity OnLoad
-        /// </summary>
-        public static void OnLoad()
-        {
-            if (Instance != null) return;
-            new GameObject("Song Browser Modded").AddComponent<SongBrowserMasterViewController>();
-        }
-
-        public static SongBrowserMasterViewController Instance;
-
-        /// <summary>
-        /// Builds the UI for this plugin.
-        /// </summary>
-        protected override void Awake()
-        {
-            _log.Debug("Awake()");
-
-            base.Awake();
-
-            InitModel();
-
-            _uiInitialized = false;
-            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;
-
-            _simpleDialogPromptViewControllerPrefab = Resources.FindObjectsOfTypeAll<SimpleDialogPromptViewController>().First();
-
-            this._deleteDialog = UnityEngine.Object.Instantiate<SimpleDialogPromptViewController>(this._simpleDialogPromptViewControllerPrefab);
-            this._deleteDialog.gameObject.SetActive(false);
-        }
-
-        /// <summary>
-        /// Override DidActivate to inject our UI elements.
-        /// </summary>
-        protected override void DidActivate()
-        {
-            _log.Debug("DidActivate()");
-
-            if (!_uiInitialized)
-            {
-                CreateUI();
-            }
-
-
-            try
-            {
-                //if (scene.buildIndex == SongBrowserMasterViewController.MenuIndex)
-                {
-                    _log.Debug("SceneManagerOnActiveSceneChanged - Setting Up UI");
-
-                    this._songListViewController.didSelectSongEvent += OnDidSelectSongEvent;
-
-                    UpdateSongList();
-                }
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception during scene change: " + e);
-            }
-
-            base.DidActivate();
-        }
-
-        /// <summary>
-        /// 
-        /// </summary>
-        private void InitModel()
-        {
-            if (_model == null)
-            {
-                _model = new SongBrowserModel();
-            }
-            _model.Init(new DataAccess.BeatSaberSongList());
-        }
-
-        /// <summary>
-        /// Builds the SongBrowser UI
-        /// </summary>
-        public void CreateUI()
-        {
-            _log.Debug("CreateUI");
-
-            try
-            {                               
-                RectTransform rect = this.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());
-                    _model.Settings.sortMode = sortMode;
-                    _model.Settings.Save();
-                    UpdateSongList();
-                };
-
-                _sortButtonGroup = new List<SongSortButton>
-                {
-                    UIBuilder.CreateSortButton(rect, "PlayButton", "Favorite", 3, "AllDirectionsIcon", 30f, 77.5f, 16f, 5f, SongSortMode.Favorites, onSortButtonClickEvent),
-                    UIBuilder.CreateSortButton(rect, "PlayButton", "Song", 3, "AllDirectionsIcon", 14f, 77.5f, 16f, 5f, SongSortMode.Default, onSortButtonClickEvent),
-                    UIBuilder.CreateSortButton(rect, "PlayButton", "Author", 3, "AllDirectionsIcon", -2f, 77.5f, 16f, 5f, SongSortMode.Author, onSortButtonClickEvent),
-                    UIBuilder.CreateSortButton(rect, "PlayButton", "Original", 3, "AllDirectionsIcon", -18f, 77.5f, 16f, 5f, SongSortMode.Original, onSortButtonClickEvent),
-                    UIBuilder.CreateSortButton(rect, "PlayButton", "Newest", 3, "AllDirectionsIcon", -34f, 77.5f, 16f, 5f, SongSortMode.Newest, onSortButtonClickEvent),
-                };
-
-                // Creaate Add to Favorites Button
-                _log.Debug("Creating add to favorites button...");
-
-                RectTransform transform = _songDetailViewController.transform as RectTransform;
-                _addFavoriteButton = UIBuilder.CreateUIButton(transform, "QuitButton", SongBrowserApplication.Instance.ButtonTemplate);
-                (_addFavoriteButton.transform as RectTransform).anchoredPosition = new Vector2(45f, 9f);
-                (_addFavoriteButton.transform as RectTransform).sizeDelta = new Vector2(10f, 5.0f);
-                
-                if (_addFavoriteButtonText == null)
-                {
-                    _log.Debug("Determinng if first selected song is a favorite.");
-                    LevelStaticData level = getSelectedSong();
-                    if (level != null)
-                    {
-                        RefreshAddFavoriteButton(level.levelId);
-                    }                    
-                }
-                
-                UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);                
-                UIBuilder.SetButtonTextSize(ref _addFavoriteButton, 3);
-                UIBuilder.SetButtonIconEnabled(ref _addFavoriteButton, false);                
-                _addFavoriteButton.onClick.RemoveAllListeners();
-                _addFavoriteButton.onClick.AddListener(delegate () {                    
-                    ToggleSongInFavorites();
-                });
-
-                // Create delete button
-                _log.Debug("Creating delete button...");
-
-                transform = _songDetailViewController.transform as RectTransform;
-                _deleteButton = UIBuilder.CreateUIButton(transform, "QuitButton", SongBrowserApplication.Instance.ButtonTemplate);
-                (_deleteButton.transform as RectTransform).anchoredPosition = new Vector2(45f, 0f);
-                (_deleteButton.transform as RectTransform).sizeDelta = new Vector2(16f, 5f);
-                UIBuilder.SetButtonText(ref _deleteButton, "Delete");
-                UIBuilder.SetButtonTextSize(ref _deleteButton, 3);
-                UIBuilder.SetButtonIconEnabled(ref _deleteButton, false);
-                _deleteButton.onClick.RemoveAllListeners();
-                _deleteButton.onClick.AddListener(delegate () {
-                    HandleDeleteSelectedSong();
-                });
-
-                RefreshUI();
-                _uiInitialized = true;
-            }
-            catch (Exception e)
-            {
-                _log.Exception("Exception CreateUI: {0}\n{1}", e.Message, e.StackTrace);
-            }
-        }
-
-        /// <summary>
-        /// Bind to some UI events.
-        /// </summary>
-        /// <param name="arg0"></param>
-        /// <param name="scene"></param>
-        private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene)
-        {
-            _uiInitialized = false;
-        }
-
-        /// <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.levelId);
-        }
-
-        /// <summary>
-        /// Return LevelStaticData or null.
-        /// </summary>
-        private LevelStaticData getSelectedSong()
-        {
-            // song browser not presenting
-            if (!this.beingPresented)
-            {
-                return null;
-            }
-
-            /*if (this._levelsStaticData == null)
-            {
-                return null;
-            }*/
-
-            int selectedIndex = this.GetSelectedSongIndex();
-            if (selectedIndex < 0)
-            {
-                return null;
-            }
-
-            LevelStaticData level = this.GetLevelStaticDataForSelectedSong();
-            return level;
-        }
-
-        /// <summary>
-        /// Pop up a delete dialog.
-        /// </summary>
-        private void HandleDeleteSelectedSong()
-        {
-            int selectedIndex = this.GetSelectedSongIndex();
-            if (selectedIndex < 0)
-            {
-                return;
-            }
-
-            LevelStaticData level = this.GetLevelStaticDataForSelectedSong();
-            SongLoaderPlugin.CustomSongInfo songInfo = _model.LevelIdToCustomSongInfos[level.levelId];
-
-            this._deleteDialog.Init("Delete song warning!", String.Format("<color=#00AAFF>Permanently delete song: {0}</color>\n  Do you want to continue?", songInfo.songName), "YES", "NO");
-
-            this._deleteDialog.didFinishEvent += this.HandleSimpleDialogPromptViewControllerDidFinish;
-            this.PresentModalViewController(this._deleteDialog, null, false);
-        }
-
-        /// <summary>
-        /// Handle delete dialog resolution.
-        /// </summary>
-        /// <param name="viewController"></param>
-        /// <param name="ok"></param>
-        public virtual void HandleSimpleDialogPromptViewControllerDidFinish(SimpleDialogPromptViewController viewController, bool ok)
-        {
-            viewController.didFinishEvent -= this.HandleSimpleDialogPromptViewControllerDidFinish;
-            if (!ok)
-            {
-                viewController.DismissModalViewController(null, false);
-            }
-            else
-            {
-                LevelStaticData level = this.GetLevelStaticDataForSelectedSong();
-                SongLoaderPlugin.CustomSongInfo songInfo = _model.LevelIdToCustomSongInfos[level.levelId];
-
-                viewController.DismissModalViewController(null, false);
-                _log.Debug("DELETING: {0}", songInfo.path);
-                //Directory.Delete(songInfo.path);
-            }
-        }
-
-        /// <summary>
-        /// Add/Remove song from favorites depending on if it already exists.
-        /// </summary>
-        private void ToggleSongInFavorites()
-        {
-            LevelStaticData songInfo = this.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(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(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 (!this.beingPresented)
-                {
-                    _log.Debug("No song list to refresh.");
-                    return;
-                }
-                             
-                // Convert to Array once in-case this is costly.
-                LevelStaticData[] songListArray = songList.ToArray();
-                
-                // Store on song browser
-                this._levelsStaticData = songListArray;
-                this._songListViewController.Init(songListArray);
-
-                // Refresh UI Elements in case something changed.
-                RefreshAddFavoriteButton(songList[0].levelId);
-
-                // Might not be fully presented yet.
-                SongListTableView songListTableView = this._songListViewController.GetComponentInChildren<SongListTableView>();
-                if (songListTableView == null || !songListTableView.isActiveAndEnabled)
-                {
-                    _log.Debug("SongListTableView not presenting yet, cannot refresh view yet.");
-                    return;
-                }
-
-                TableView tableView = ReflectionUtil.GetPrivateField<TableView>(songListTableView, "_tableView");
-                if (tableView == null)
-                {
-                    _log.Debug("TableView not presenting yet, cannot refresh view yet.");
-                    return;
-                }
-
-                // Refresh the list views and its table view
-                songListTableView.SetLevels(songListArray);
-                tableView.ScrollToRow(0, false);
-                tableView.ReloadData();
-
-                // Clear Force selection of index 0 so we don't end up in a weird state.
-                //songListTableView.ClearSelection();
-                _songListViewController.SelectSong(0);
-                this.HandleSongListDidSelectSong(_songListViewController);
-            }
-            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);
-            RefreshUI();
-        }
-
-        /// <summary>
-        /// 
-        /// </summary>
-        private void Update()
-        {
-            CheckDebugUserInput();
-        }
-
-        /// <summary>
-        /// Map some key presses directly to UI interactions to make testing easier.
-        /// </summary>
-        private void CheckDebugUserInput()
-        {
-            // cycle sort modes
-            if (Input.GetKeyDown(KeyCode.T))
-            {
-                _sortButtonLastPushedIndex = (_sortButtonLastPushedIndex + 1) % _sortButtonGroup.Count;
-                _sortButtonGroup[_sortButtonLastPushedIndex].Button.onClick.Invoke();                
-            }
-
-            // delete
-            if (Input.GetKeyDown(KeyCode.D))
-            {
-                _deleteButton.onClick.Invoke();
-            }
-
-            // 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))
-            {                
-                _songListViewController.SelectSong(0);
-                this.HandleSongListDidSelectSong(_songListViewController);
-
-                DifficultyViewController _difficultyViewController = Resources.FindObjectsOfTypeAll<DifficultyViewController>().First();
-                _difficultyViewController.SelectDifficulty(LevelStaticData.Difficulty.Hard);
-                this.HandleDifficultyViewControllerDidSelectDifficulty(_difficultyViewController);
-            }
-
-            if (Input.GetKeyDown(KeyCode.V))
-            {
-                this.HandleSongDetailViewControllerDidPressPlayButton(_songDetailViewController);
-            }
-
-            // change song index
-            if (Input.GetKeyDown(KeyCode.N))
-            {
-                int newIndex = this.GetSelectedSongIndex() - 1;
-
-                _songListViewController.SelectSong(newIndex);
-                this.HandleSongListDidSelectSong(_songListViewController);
-
-                SongListTableView songTableView = Resources.FindObjectsOfTypeAll<SongListTableView>().First();
-                _songListViewController.HandleSongListTableViewDidSelectRow(songTableView, newIndex);
-            }
-
-            if (Input.GetKeyDown(KeyCode.M))
-            {
-                int newIndex = this.GetSelectedSongIndex() + 1;
-
-                _songListViewController.SelectSong(newIndex);
-                this.HandleSongListDidSelectSong(_songListViewController);
-
-                SongListTableView songTableView = Resources.FindObjectsOfTypeAll<SongListTableView>().First();
-                _songListViewController.HandleSongListTableViewDidSelectRow(songTableView, newIndex);
-            }
-
-            // add to favorites
-            if (Input.GetKeyDown(KeyCode.F))
-            {
-                ToggleSongInFavorites();
-            }
-        }
-    }
-}
- 

+ 69 - 102
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,20 +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 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;
-        private int _customSongDirTotalCount = -1;
+        public static String LastSelectedLevelId { get; set; }
 
         public SongBrowserSettings Settings
         {
@@ -36,7 +31,7 @@ namespace SongBrowserPlugin
             }
         }
 
-        public List<LevelStaticData> SortedSongList
+        public List<StandardLevelSO> SortedSongList
         {
             get
             {
@@ -44,11 +39,11 @@ namespace SongBrowserPlugin
             }
         }
 
-        public Dictionary<String, SongLoaderPlugin.CustomSongInfo> LevelIdToCustomSongInfos
+        public Dictionary<String, SongLoaderPlugin.OverrideClasses.CustomLevel> LevelIdToCustomSongInfos
         {
             get
             {
-                return _levelIdToCustomSongInfo;
+                return _levelIdToCustomLevel;
             }
         }
 
@@ -65,82 +60,53 @@ namespace SongBrowserPlugin
         /// </summary>
         /// <param name="songSelectionMasterView"></param>
         /// <param name="songListViewController"></param>
-        public void Init(IBeatSaberSongList beatSaberSongAccessor)
+        public void Init()
         {
-            _beatSaberSongAccessor = beatSaberSongAccessor;
             _settings = SongBrowserSettings.Load();
+            _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);
             string[] directories = Directory.GetDirectories(customSongsPath);
-            int directoryCount = directories.Length;
-            int fileCount = Directory.GetFiles(customSongsPath, "*").Length;
-            int currentTotalCount = directoryCount + fileCount;
 
-            if (_cachedCustomSongDirLastWriteTIme == null || 
-                DateTime.Compare(currentLastWriteTIme, _cachedCustomSongDirLastWriteTIme) != 0 ||
-                currentTotalCount != this._customSongDirTotalCount)
+            // Get LastWriteTimes
+            var Epoch = new DateTime(1970, 1, 1);
+            foreach (string dir in directories)
             {
-                _log.Debug("Custom Song directory has changed. Fetching new songs. Sorting song list.");
-
-                this._customSongDirTotalCount = directoryCount + fileCount;
-
-                // Get LastWriteTimes
-                var Epoch = new DateTime(1970, 1, 1);
+                // Flip slashes, match SongLoaderPlugin
+                string slashed_dir = dir.Replace("\\", "/");
 
-                //_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();
-                }
-
-                // 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
-            {
-                _log.Debug("Songs List and/or sort mode has not changed.");
+                //_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>
@@ -148,88 +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.Debug("Sorting song list by author");
+                    _log.Info("Sorting song list by author");
                     _sortedSongs = _originalSongs
                         .AsQueryable()
-                        .OrderBy(x => x.authorName)
+                        .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.songName)
-                        .ThenBy(x => x.authorName)
+                        .ThenBy(x => x.songAuthorName)
                         .ToList();
                     break;
             }
 
             stopwatch.Stop();
             _log.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);
-
-            this._beatSaberSongAccessor.OverwriteBeatSaberSongList(_sortedSongs);
         }        
     }
 }

+ 10 - 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,20 +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="SongBrowserApplication.cs" />
-    <Compile Include="SongBrowserMasterViewController.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" />

+ 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;
+    }
+}

+ 16 - 3
SongBrowserPlugin/UIBuilder.cs

@@ -3,9 +3,9 @@ using System.Linq;
 using UnityEngine.UI;
 using TMPro;
 using VRUI;
+using SongBrowserPlugin.DataAccess;
 
-
-namespace SongBrowserPlugin
+namespace SongBrowserPlugin.UI
 {
     public static class UIBuilder
     {
@@ -28,6 +28,19 @@ namespace SongBrowserPlugin
         }
 
         /// <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>
@@ -148,7 +161,7 @@ namespace SongBrowserPlugin
         {
             if (button.GetComponentsInChildren<UnityEngine.UI.Image>().Count() > 1)
             {
-                button.GetComponentsInChildren<UnityEngine.UI.Image>()[1].sprite = icon;
+                button.GetComponentsInChildren<Image>().First(x => x.name == "Icon").sprite = icon;
             }            
         }
 

+ 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);*/
         }
     }
 }