浏览代码

Big Refactor. UI is now generated by hijacking the SongSelectionMasterViewController used with our own instance. This offers more flexibility and means any way the user can get to the song list they will see the modded song browser.

Stephen Damm 6 年之前
父节点
当前提交
46dd4faf9d

+ 5 - 2
SongBrowserPlugin/Plugin.cs

@@ -22,7 +22,7 @@ namespace SongBrowserPlugin
 
 		public string Version
 		{
-			get { return "v1.0-rc3"; }
+			get { return "v2.0"; }
 		}
 		
 		public void OnApplicationStart()
@@ -47,8 +47,11 @@ namespace SongBrowserPlugin
         public void OnLevelWasLoaded(int level)
 		{
             //Console.WriteLine("OnLevelWasLoaded=" + level);            
+            //if (level != SongBrowserMasterViewController.MenuIndex) return;
+            //SongBrowserMasterViewController.OnLoad();
+
             if (level != SongBrowserMasterViewController.MenuIndex) return;
-            SongBrowserMasterViewController.OnLoad();
+            SongBrowserApplication.OnLoad();
         }
 
 		public void OnLevelWasInitialized(int level)

+ 14 - 2
SongBrowserPlugin/ReflectionUtil.cs

@@ -1,8 +1,10 @@
-using System.Reflection;
+using System;
+using System.Reflection;
+
 
 namespace SongBrowserPlugin
 {
-	public static class ReflectionUtil
+    public static class ReflectionUtil
 	{
 		public static void SetPrivateField(object obj, string fieldName, object value)
 		{
@@ -38,5 +40,15 @@ namespace SongBrowserPlugin
             }
             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;
+        }       
     }
 }

+ 211 - 0
SongBrowserPlugin/SongBrowserApplication.cs

@@ -0,0 +1,211 @@
+using SongLoaderPlugin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.UI;
+
+namespace SongBrowserPlugin
+{
+    public class SongBrowserApplication : MonoBehaviour
+    {
+        public static SongBrowserApplication Instance;
+
+        private Logger _log = new Logger("SongBrowserMasterViewController");
+
+        // BeatSaber UI Elements
+        private SongSelectionMasterViewController _songSelectionMasterView;
+        private SongDetailViewController _songDetailViewController;
+        private SongListViewController _songListViewController;
+        private MainMenuViewController _mainMenuViewController;
+        private MenuMasterViewController _menuMasterViewController;
+
+        // Song Browser UI Elements
+        private SongBrowserMasterViewController _songBrowserMasterViewController;
+
+        public Dictionary<String, Sprite> CachedIcons;
+        public Button ButtonTemplate;
+
+        internal static void OnLoad()
+        {
+            if (Instance != null)
+            {
+                return;
+            }
+            new GameObject("BeatSaber SongBrowser Mod").AddComponent<SongBrowserApplication>();
+            //DontDestroyOnLoad(gameObject);
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        private void Awake()
+        {
+            Instance = this;
+
+            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;
+            SongLoader.SongsLoaded.AddListener(OnSongLoaderLoadedSongs);
+        }
+
+        private void OnSongLoaderLoadedSongs()
+        {
+            try
+            {
+                _log.Debug("Attempting to take over the didSelectModeEvent Button");
+                SoloModeSelectionViewController view = Resources.FindObjectsOfTypeAll<SoloModeSelectionViewController>().First();
+
+                if (view.didSelectModeEvent != null)
+                {
+                    Delegate[] delegates = view.didSelectModeEvent.GetInvocationList();
+                    view.didSelectModeEvent -= delegates[0] as Action<SoloModeSelectionViewController, GameplayMode>;
+                }
+
+                view.didSelectModeEvent += HandleSoloModeSelectionViewControllerDidSelectMode;
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception during OnSongLoaderLoadedSongs: " + e);
+            }
+        }
+
+        /// <summary>
+        /// Bind to some UI events.
+        /// </summary>
+        /// <param name="arg0"></param>
+        /// <param name="scene"></param>
+        private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene)
+        {
+            if (scene.buildIndex != SongBrowserMasterViewController.MenuIndex)
+            {
+                return;
+            }
+
+            AcquireUIElements();
+
+            // Clone and override the default song-browser.
+            if (_songBrowserMasterViewController == null)
+            {
+                _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));
+                }
+            }
+
+            _log.Debug("Overriding Song Browser");
+            ReflectionUtil.SetPrivateField(_menuMasterViewController, "_songSelectionMasterViewController", _songBrowserMasterViewController);
+        }
+
+        /// <summary>
+        /// Get a handle to the view controllers we are going to add elements to.
+        /// </summary>
+        public void AcquireUIElements()
+        {
+            _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);
+            }
+
+            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();
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception AcquireUIElements(): " + e);
+            }
+        }
+
+        /// <summary>
+        /// Hijack the result of clicking into the song browser.
+        /// </summary>
+        /// <param name="viewController"></param>
+        /// <param name="gameplayMode"></param>
+        private void HandleSoloModeSelectionViewControllerDidSelectMode(SoloModeSelectionViewController viewController, GameplayMode gameplayMode)
+        {
+            _log.Debug("Hi jacking solo mode buttons");
+            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);
+
+                _songBrowserMasterViewController.Init(null, LevelStaticData.Difficulty.Easy, levelsForGameplayMode, useLocalLeaderboards, showDismissButton, showPlayerStats, gameplayMode);
+                viewController.PresentModalViewController(_songBrowserMasterViewController, null, false);
+                _songSelectionMasterView = _songBrowserMasterViewController;
+
+                _log.Debug("Success hijacking ...");
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception replacing in-game song browser: {0}\n{1}", e.Message, e.StackTrace);
+            }
+        }
+
+        // Token: 0x06000C43 RID: 3139 RVA: 0x00035DE8 File Offset: 0x00033FE8
+        private bool ArePlayerStatsUsed(GameplayMode gameplayMode)
+        {
+            return gameplayMode == GameplayMode.SoloStandard || gameplayMode == GameplayMode.SoloNoArrows || gameplayMode == GameplayMode.SoloOneSaber;
+        }
+
+        /// <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 Update()
+        {
+            // 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("FreePlayButton");
+            }
+
+            if (Input.GetKeyDown(KeyCode.B))
+            {
+                InvokeBeatSaberButton("ContinueButton");
+            }
+        }
+    }
+}

+ 125 - 234
SongBrowserPlugin/SongBrowserMasterViewController.cs

@@ -2,15 +2,10 @@
 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
 {
@@ -20,38 +15,34 @@ namespace SongBrowserPlugin
         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;
+    public class SongBrowserMasterViewController : SongSelectionMasterViewController
+    {
+        public const String Name = "SongBrowserMasterViewController";
 
-        private Button _buttonInstance;
+        public const int MenuIndex = 1;
 
-        private List<SongSortButton> _sortButtonGroup;
-        
+        private Logger _log = new Logger(Name);
+       
+        // New UI Elements
+        private List<SongSortButton> _sortButtonGroup;        
         private Button _addFavoriteButton;
         private String _addFavoriteButtonText = null;
 
+        // 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").AddComponent<SongBrowserMasterViewController>();
+            new GameObject("Song Browser Modded").AddComponent<SongBrowserMasterViewController>();
         }
 
         public static SongBrowserMasterViewController Instance;
@@ -59,46 +50,60 @@ namespace SongBrowserPlugin
         /// <summary>
         /// Builds the UI for this plugin.
         /// </summary>
-        private void Awake()
+        protected override void Awake()
         {
             _log.Debug("Awake()");
 
-            Instance = this;
-          
-            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;
+            base.Awake();
 
-            SongLoaderPlugin.SongLoader.SongsLoaded.AddListener(OnSongLoaderLoadedSongs);
+            InitModel();
 
-            DontDestroyOnLoad(gameObject);
+            _uiInitialized = false;
+            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;            
         }
 
         /// <summary>
-        /// Get a handle to the view controllers we are going to add elements to.
+        /// Override DidActivate to inject our UI elements.
         /// </summary>
-        public void AcquireUIElements()
+        protected override void DidActivate()
         {
-            _icons = new Dictionary<String, Sprite>();
-            foreach (Sprite sprite in Resources.FindObjectsOfTypeAll<Sprite>())
+            _log.Debug("DidActivate()");
+
+            if (!_uiInitialized)
             {
-                if (_icons.ContainsKey(sprite.name))
-                {
-                    continue;
-                }
-                _icons.Add(sprite.name, sprite);
+                CreateUI();
             }
 
+
             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();
+                //if (scene.buildIndex == SongBrowserMasterViewController.MenuIndex)
+                {
+                    _log.Debug("SceneManagerOnActiveSceneChanged - Setting Up UI");
+
+                    this._songListViewController.didSelectSongEvent += OnDidSelectSongEvent;
+
+                    UpdateSongList();
+                }
             }
             catch (Exception e)
             {
-                _log.Exception("Exception AcquireUIElements(): " + 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>
@@ -108,44 +113,54 @@ namespace SongBrowserPlugin
         {
             _log.Debug("CreateUI");
 
-            // _icons.ForEach(i => Console.WriteLine(i.ToString()));
-
             try
             {                               
-                RectTransform rect = _songSelectionMasterView.transform as RectTransform;
+                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>
                 {
-                    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)
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Fav", 3, "AllDirectionsIcon", 30f, 77.5f, 15f, 5f, SongSortMode.Favorites, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Def", 3, "AllDirectionsIcon", 15f, 77.5f, 15f, 5f, SongSortMode.Default, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "Org", 3, "AllDirectionsIcon", 0f, 77.5f, 15f, 5f, SongSortMode.Original, onSortButtonClickEvent),
+                    UIBuilder.CreateSortButton(rect, "PlayButton", "New", 3, "AllDirectionsIcon", -15f, 77.5f, 15f, 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", _buttonInstance);
+                _addFavoriteButton = UIBuilder.CreateUIButton(transform, "QuitButton", SongBrowserApplication.Instance.ButtonTemplate);
                 (_addFavoriteButton.transform as RectTransform).anchoredPosition = new Vector2(45f, 5f);
-                (_addFavoriteButton.transform as RectTransform).sizeDelta = new Vector2(10f, 10f);                
-
+                (_addFavoriteButton.transform as RectTransform).sizeDelta = new Vector2(10f, 10f);
+                
                 if (_addFavoriteButtonText == null)
                 {
-                    LevelStaticData level = getSelectedSong();
-                    RefreshAddFavoriteButton(level);
+                    _log.Debug("Determinng if first selected song is a favorite.");
+                    LevelStaticData level = getSelectedSong();                    
+                    RefreshAddFavoriteButton(level.levelId);
                 }
                 
                 UIBuilder.SetButtonText(ref _addFavoriteButton, _addFavoriteButtonText);
-                //UIBuilder.SetButtonIcon(ref _addFavoriteButton, _icons["AllDirectionsIcon"]);
+                //UIBuilder.SetButtonIcon(ref _addFavoriteButton, SongBrowserApplication.Instance.CachedIcons["AllDirectionsIcon"]);
                 UIBuilder.SetButtonTextSize(ref _addFavoriteButton, 3);
-                UIBuilder.SetButtonIconEnabled(ref _addFavoriteButton, false);
-
+                UIBuilder.SetButtonIconEnabled(ref _addFavoriteButton, false);                
                 _addFavoriteButton.onClick.RemoveAllListeners();
                 _addFavoriteButton.onClick.AddListener(delegate () {                    
                     ToggleSongInFavorites();
                 });
 
                 RefreshUI();
+                _uiInitialized = true;
             }
             catch (Exception e)
             {
@@ -154,95 +169,13 @@ namespace SongBrowserPlugin
         }
 
         /// <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);
-            }
+            _uiInitialized = false;
         }
 
         /// <summary>
@@ -265,7 +198,7 @@ namespace SongBrowserPlugin
                 return;
             }
 
-            RefreshAddFavoriteButton(level);
+            RefreshAddFavoriteButton(level.levelId);
         }
 
         /// <summary>
@@ -273,19 +206,19 @@ namespace SongBrowserPlugin
         /// </summary>
         private LevelStaticData getSelectedSong()
         {
-            // song list not even visible
-            if (!_songSelectionMasterView.isActiveAndEnabled)
+            // song browser not presenting
+            if (!this.beingPresented)
             {
                 return null;
             }
 
-            int selectedIndex = _songSelectionMasterView.GetSelectedSongIndex();
+            int selectedIndex = this.GetSelectedSongIndex();
             if (selectedIndex < 0)
             {
                 return null;
             }
 
-            LevelStaticData level = _songSelectionMasterView.GetLevelStaticDataForSelectedSong();
+            LevelStaticData level = this.GetLevelStaticDataForSelectedSong();
 
             return level;
         }
@@ -295,7 +228,7 @@ namespace SongBrowserPlugin
         /// </summary>
         private void ToggleSongInFavorites()
         {
-            LevelStaticData songInfo = _songSelectionMasterView.GetLevelStaticDataForSelectedSong();
+            LevelStaticData songInfo = this.GetLevelStaticDataForSelectedSong();
             if (_model.Settings.favorites.Contains(songInfo.levelId))
             {
                 _log.Info("Remove {0} from favorites", songInfo.name);
@@ -318,33 +251,14 @@ namespace SongBrowserPlugin
         /// Helper to quickly refresh add to favorites button
         /// </summary>
         /// <param name="levelId"></param>
-        private void RefreshAddFavoriteButton(LevelStaticData level)
+        private void RefreshAddFavoriteButton(String levelId)
         {
-            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";
@@ -382,52 +296,46 @@ namespace SongBrowserPlugin
             try
             {
                 // Check a couple of possible situations that we can't refresh
-                if (!_songSelectionMasterView.isActiveAndEnabled)
+                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);
 
-                SongListTableView songTableView = _songListViewController.GetComponentInChildren<SongListTableView>();
-                if (songTableView == null)
+                // 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>(songTableView, "_tableView");
+                TableView tableView = ReflectionUtil.GetPrivateField<TableView>(songListTableView, "_tableView");
                 if (tableView == null)
                 {
+                    _log.Debug("TableView not presenting yet, cannot refresh view yet.");
                     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);
+                // 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.
-                songTableView.ClearSelection();
+                //songListTableView.ClearSelection();
                 _songListViewController.SelectSong(0);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
-                
-
-                RefreshUI();
-                RefreshAddFavoriteButton(songList[0]);
+                this.HandleSongListDidSelectSong(_songListViewController);
             }
             catch (Exception e)
             {
@@ -440,71 +348,54 @@ namespace SongBrowserPlugin
         /// </summary>
         public void UpdateSongList()
         {
-           _model.UpdateSongLists(true);
+            _model.UpdateSongLists(true);
             RefreshSongList(_model.SortedSongList);
+            RefreshUI();
         }
 
         /// <summary>
-        /// Map some key presses directly to UI interactions to make testing easier.
+        /// 
         /// </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))
             {
-                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();
+                _sortButtonLastPushedIndex = (_sortButtonLastPushedIndex + 1) % _sortButtonGroup.Count;
+                _sortButtonGroup[_sortButtonLastPushedIndex].Button.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.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);
+                this.HandleSongListDidSelectSong(_songListViewController);
 
                 DifficultyViewController _difficultyViewController = Resources.FindObjectsOfTypeAll<DifficultyViewController>().First();
                 _difficultyViewController.SelectDifficulty(LevelStaticData.Difficulty.Hard);
-                _songSelectionMasterView.HandleDifficultyViewControllerDidSelectDifficulty(_difficultyViewController);
+                this.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();
+                this.HandleSongDetailViewControllerDidPressPlayButton(_songDetailViewController);
             }
 
             // change song index
             if (Input.GetKeyDown(KeyCode.N))
             {
-                int newIndex = _songSelectionMasterView.GetSelectedSongIndex() - 1;
+                int newIndex = this.GetSelectedSongIndex() - 1;
 
                 _songListViewController.SelectSong(newIndex);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
+                this.HandleSongListDidSelectSong(_songListViewController);
 
                 SongListTableView songTableView = Resources.FindObjectsOfTypeAll<SongListTableView>().First();
                 _songListViewController.HandleSongListTableViewDidSelectRow(songTableView, newIndex);
@@ -512,10 +403,10 @@ namespace SongBrowserPlugin
 
             if (Input.GetKeyDown(KeyCode.M))
             {
-                int newIndex = _songSelectionMasterView.GetSelectedSongIndex() + 1;
+                int newIndex = this.GetSelectedSongIndex() + 1;
 
                 _songListViewController.SelectSong(newIndex);
-                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
+                this.HandleSongListDidSelectSong(_songListViewController);
 
                 SongListTableView songTableView = Resources.FindObjectsOfTypeAll<SongListTableView>().First();
                 _songListViewController.HandleSongListTableViewDidSelectRow(songTableView, newIndex);

+ 1 - 7
SongBrowserPlugin/SongBrowserModel.cs

@@ -19,9 +19,6 @@ namespace SongBrowserPlugin
         private Dictionary<String, double> _cachedLastWriteTimes;
         private SongBrowserSettings _settings;
 
-        //private SongSelectionMasterViewController _songSelectionMasterView;
-        //private SongListViewController _songListViewController;
-
         private IBeatSaberSongList _beatSaberSongAccessor;
 
         private List<LevelStaticData> _sortedSongs;
@@ -59,13 +56,10 @@ namespace SongBrowserPlugin
         /// </summary>
         /// <param name="songSelectionMasterView"></param>
         /// <param name="songListViewController"></param>
-        public void Init(IBeatSaberSongList beatSaberSongAccessor /*SongSelectionMasterViewController songSelectionMasterView, SongListViewController songListViewController*/)
+        public void Init(IBeatSaberSongList beatSaberSongAccessor)
         {
             _beatSaberSongAccessor = beatSaberSongAccessor;
             _settings = SongBrowserSettings.Load();
-
-            //_songSelectionMasterView = songSelectionMasterView;
-            //_songListViewController = songListViewController;
         }
 
         /// <summary>

+ 1 - 0
SongBrowserPlugin/SongBrowserPlugin.csproj

@@ -83,6 +83,7 @@
     <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" />

+ 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

+ 102 - 0
SongBrowserPlugin/UIBuilder.cs

@@ -2,11 +2,38 @@
 using System.Linq;
 using UnityEngine.UI;
 using TMPro;
+using VRUI;
+
 
 namespace SongBrowserPlugin
 {
     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>
+        /// 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)
@@ -21,6 +48,51 @@ namespace SongBrowserPlugin
             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>();
@@ -37,6 +109,11 @@ namespace SongBrowserPlugin
             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)
@@ -47,6 +124,11 @@ namespace SongBrowserPlugin
 
         }
 
+        /// <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)
@@ -57,6 +139,11 @@ namespace SongBrowserPlugin
 
         }
 
+        /// <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)
@@ -65,6 +152,11 @@ namespace SongBrowserPlugin
             }            
         }
 
+        /// <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)
@@ -73,6 +165,11 @@ namespace SongBrowserPlugin
             }
         }
 
+        /// <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())
@@ -82,6 +179,11 @@ namespace SongBrowserPlugin
 
         }
 
+        /// <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())