瀏覽代碼

- proof of concept complete

Stephen Damm 6 年之前
父節點
當前提交
10857e8e82

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+# BeatSaberSongBrowser
+A plugin for customizing the in-game song browser.
+
+*This mod works on both the Steam and Oculus Store versions.*
+
+## Installation Instructions
+
+

+ 54 - 0
SongBrowserPlugin/Logger.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SongBrowserPlugin
+{
+    class Logger
+    {
+        private string loggerName;
+
+        public Logger(string _name)
+        {
+            loggerName = _name;
+        }
+
+        public static void StaticLog(string message)
+        {
+            Console.ForegroundColor = ConsoleColor.Green;
+            Console.WriteLine("[SongBrowserPlugin @ " + DateTime.Now.ToString("HH:mm") + "] " + message);
+        }
+
+        public void Debug(string format, params object[] args)
+        {
+            Console.ForegroundColor = ConsoleColor.Magenta;
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + String.Format(format, args));
+        }
+
+        public void Info(string message)
+        {
+            Console.ForegroundColor = ConsoleColor.Green;
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + message);
+        }
+
+        public void Warning(string message)
+        {
+            Console.ForegroundColor = ConsoleColor.Blue;
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + message);
+        }
+
+        public void Error(string message)
+        {
+            Console.ForegroundColor = ConsoleColor.Yellow;
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + message);
+        }
+
+        public void Exception(string message)
+        {
+            Console.ForegroundColor = ConsoleColor.Red;
+            Console.WriteLine("[" + loggerName + " @ " + DateTime.Now.ToString("HH:mm") + "] " + message);
+        }
+
+    }
+}

+ 70 - 0
SongBrowserPlugin/Plugin.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using VRUI;
+using IllusionPlugin;
+using TMPro;
+using UnityEngine.UI;
+using System.Collections;
+
+
+namespace SongBrowserPlugin
+{
+	public class Plugin : IPlugin
+	{	
+		public string Name
+		{
+			get { return "Song Browser"; }
+		}
+
+		public string Version
+		{
+			get { return "v1.0-alpha"; }
+		}
+		
+		public void OnApplicationStart()
+		{
+
+        }
+
+		public void OnApplicationQuit()
+		{      
+
+		}
+
+        private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene)
+        {
+            
+        }
+
+        private void SceneManager_sceneLoaded(Scene arg0, LoadSceneMode arg1)
+        {
+        }
+
+        public void OnLevelWasLoaded(int level)
+		{            
+            //Console.WriteLine("OnLevelWasLoaded=" + level);
+
+            if (level != SongBrowser.MenuIndex) return;
+            SongBrowser.OnLoad();
+        }
+
+		public void OnLevelWasInitialized(int level)
+		{
+            //Console.WriteLine("OnLevelWasInitialized=" + level);
+        }
+
+		public void OnUpdate()
+		{
+			
+		}
+
+		public void OnFixedUpdate()
+		{
+			
+		}
+	}
+}

+ 35 - 0
SongBrowserPlugin/Properties/AssemblyInfo.cs

@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SongBrowserPlugin")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("SongBrowserPlugin")]
+[assembly: AssemblyCopyright("Copyright ©  2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("0e98323b-bb8c-44d9-ae42-55222aade6ea")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 32 - 0
SongBrowserPlugin/ReflectionUtil.cs

@@ -0,0 +1,32 @@
+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);
+		}
+	}
+}

File diff suppressed because it is too large
+ 1367 - 0
SongBrowserPlugin/SimpleJSON.cs


+ 229 - 0
SongBrowserPlugin/SongBrowser.cs

@@ -0,0 +1,229 @@
+using UnityEngine;
+using System.Linq;
+using System;
+using System.Collections.Generic;
+using UnityEngine.Events;
+using UnityEngine.SceneManagement;
+using UnityEngine.UI;
+
+
+namespace SongBrowserPlugin
+{
+    public class SongBrowser : MonoBehaviour
+    {       
+        public static readonly UnityEvent SongsLoaded = new UnityEvent();
+
+        public const int MenuIndex = 1;
+
+        private Logger _log = new Logger("SongBrowserPlugin");
+
+        private SongSelectionMasterViewController _songSelectionMasterView;
+        private SongDetailViewController _songDetailViewController;
+
+        private Button _buttonInstance;
+        private Button _favoriteButton;
+        private Button _addFavoriteButton;
+
+
+        private RectTransform _songSelectRectTransform;
+
+        public static List<Sprite> _icons = new List<Sprite>();
+
+
+        public static void OnLoad()
+        {
+            if (Instance != null) return;
+            new GameObject("Song Browser").AddComponent<SongBrowser>();
+        }
+
+        public static SongBrowser Instance;
+
+        /// <summary>
+        /// Builds the UI for this plugin.
+        /// </summary>
+        private void Awake()
+        {
+            Instance = this;
+
+            AcquireUIElements();
+            CreateUI();
+
+            SongBrowserSettings bs = SongBrowserSettings.Load();
+            bs.Save();
+
+            SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged;
+            SceneManagerOnActiveSceneChanged(new Scene(), new Scene());
+            
+            DontDestroyOnLoad(gameObject);
+        }
+
+        /// <summary>
+        /// Get a handle to the view controllers we are going to add elements to.
+        /// </summary>
+        public void AcquireUIElements()
+        {
+            foreach (Sprite sprite in Resources.FindObjectsOfTypeAll<Sprite>())
+            {
+                _icons.Add(sprite);
+            }
+
+            try
+            {
+                _buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "QuitButton"));
+
+                _songSelectionMasterView = Resources.FindObjectsOfTypeAll<SongSelectionMasterViewController>().First();
+
+                _songSelectRectTransform = _songSelectionMasterView.transform.parent as RectTransform;
+
+                _songDetailViewController = Resources.FindObjectsOfTypeAll<SongDetailViewController>().FirstOrDefault();
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception AcquireUIElements(): " + e);
+            }
+        }
+
+        /// <summary>
+        /// Builds the SongBrowser UI
+        /// </summary>
+        public void CreateUI()
+        {
+            _log.Debug("CreateUI");
+
+            // _icons.ForEach(i => Console.WriteLine(i.ToString()));
+
+            try
+            {
+                // Create Sorting Songs By-Buttons
+                _favoriteButton = UIBuilder.CreateUIButton(_songSelectRectTransform, "QuitButton", _buttonInstance);
+                (_favoriteButton.transform as RectTransform).anchoredPosition = new Vector2(0, 80f);
+                (_favoriteButton.transform as RectTransform).sizeDelta = new Vector2(20f, 10f);
+
+                UIBuilder.SetButtonText(ref _favoriteButton, "Fav!");            
+                UIBuilder.SetButtonIcon(ref _favoriteButton, _icons.First(x => (x.name == "SettingsIcon")));
+
+                _favoriteButton.onClick.AddListener(delegate () {
+                    _log.Info("Sorting by Favorites!");
+
+                });
+
+                // Creaate Add to Favorites Button
+                RectTransform transform = _songDetailViewController.transform as RectTransform;
+                _addFavoriteButton = UIBuilder.CreateUIButton(transform, "QuitButton", _buttonInstance);
+                (_addFavoriteButton.transform as RectTransform).anchoredPosition = new Vector2(40f, 0f);
+                (_addFavoriteButton.transform as RectTransform).sizeDelta = new Vector2(20f, 10f);
+
+                UIBuilder.SetButtonText(ref _addFavoriteButton, "+1");
+                UIBuilder.SetButtonIcon(ref _addFavoriteButton, _icons.First(x => (x.name == "AllDirectionsIcon")));
+
+                _addFavoriteButton.onClick.AddListener(delegate () {
+                    _log.Info("Add to Favorites!");
+
+                });
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception CreateUI: " + e.Message);
+            }
+        }
+
+        private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene)
+        {
+            //AcquireSongs();
+
+            Console.WriteLine("scene.buildIndex=" + scene.buildIndex);
+
+            try
+            {
+                MainMenuViewController _mainMenuViewController = Resources.FindObjectsOfTypeAll<MainMenuViewController>().First();
+                Button _buttonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "SoloButton"));
+                _buttonInstance.onClick.AddListener(delegate ()
+                {
+                    AcquireSongs();
+                });
+            }
+            catch (Exception e)
+            {
+                _log.Exception("Exception: " + e);
+            }            
+        }
+
+        private void OnDidSelectSongEvent(SongListViewController songListViewController)
+        {
+
+        }
+
+        public void AcquireSongs()
+        {
+            _log.Debug("SceneManager.GetActiveScene().buildIndex=" + SceneManager.GetActiveScene().buildIndex);
+            if (SceneManager.GetActiveScene().buildIndex != MenuIndex) return;
+            _log.Debug("Acquiring Songs");
+           
+            var gameScenesManager = Resources.FindObjectsOfTypeAll<GameScenesManager>().FirstOrDefault();
+
+            var gameDataModel = PersistentSingleton<GameDataModel>.instance;
+            var oldData = gameDataModel.gameStaticData.worldsData[0].levelsData.ToList();
+
+            _log.Debug("SongBrowser got oldData.Count={0}", oldData.Count);
+            oldData.ForEach(i => Console.WriteLine(i.levelId));
+
+
+            var sorted_old_data = oldData.AsQueryable().OrderBy(x => x.authorName).OrderBy(x => x.songName);
+
+            ReflectionUtil.SetPrivateField(gameDataModel.gameStaticData.worldsData[0], "_levelsData", sorted_old_data.ToArray());
+        }
+
+        /// <summary>
+        /// Map some key presses directly to UI interactions to make testing easier.
+        /// </summary>
+        private void Update()
+        {
+            if (Input.GetKeyDown(KeyCode.R))
+            {
+                AcquireSongs();
+            }
+
+            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();                
+            }
+
+            SongListViewController _songListViewController = Resources.FindObjectsOfTypeAll<SongListViewController>().First();
+
+            if (Input.GetKeyDown(KeyCode.C))
+            {                
+
+                _songListViewController.SelectSong(0);
+                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
+
+                DifficultyViewController _difficultyViewController = Resources.FindObjectsOfTypeAll<DifficultyViewController>().First();
+                _difficultyViewController.SelectDifficulty(LevelStaticData.Difficulty.Hard);
+                _songSelectionMasterView.HandleDifficultyViewControllerDidSelectDifficulty(_difficultyViewController);
+            }
+
+            if (Input.GetKeyDown(KeyCode.N))
+            {
+                _songListViewController.SelectSong(_songSelectionMasterView.GetSelectedSongIndex() - 1);
+                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
+            }
+
+            if (Input.GetKeyDown(KeyCode.M))
+            {
+                _songListViewController.SelectSong(_songSelectionMasterView.GetSelectedSongIndex() + 1);
+                _songSelectionMasterView.HandleSongListDidSelectSong(_songListViewController);
+            }
+
+            if (Input.GetKeyDown(KeyCode.F))
+            {
+                _favoriteButton.onClick.Invoke();
+            }
+        }
+    }
+}

+ 120 - 0
SongBrowserPlugin/SongBrowserPlugin.csproj

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{6F9B6801-9F4B-4D1F-805D-271C95733814}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>SongBrowserPlugin</RootNamespace>
+    <AssemblyName>SongBrowserPlugin</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <LangVersion>6</LangVersion>
+    <TargetFrameworkProfile>
+    </TargetFrameworkProfile>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <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">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\IllusionPlugin.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.Data" />
+    <Reference Include="System.Xml" />
+    <Reference Include="TextMeshPro-1.0.55.2017.1.0b12, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\TextMeshPro-1.0.55.2017.1.0b12.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.AudioModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.AudioModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.ImageConversionModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.IMGUIModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.IMGUIModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.JSONSerializeModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.JSONSerializeModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.UI, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UI.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.UIElementsModule, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.UIModule, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UIModule.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.UnityWebRequestWWWModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Logger.cs" />
+    <Compile Include="ReflectionUtil.cs" />
+    <Compile Include="SimpleJSON.cs" />
+    <Compile Include="SongBrowser.cs" />
+    <Compile Include="Plugin.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="SongBrowserSettings.cs" />
+    <Compile Include="UIBuilder.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\IllusionPlugin\IllusionPlugin.csproj">
+      <Project>{e2848bfb-5432-42f4-8ae0-d2ec0cdf2f71}</Project>
+      <Name>IllusionPlugin</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="Internals\" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+         Other similar extension points exist, see Microsoft.Common.targets.
+    <Target Name="BeforeBuild">
+    </Target>
+    <Target Name="AfterBuild">
+    </Target>
+    -->
+</Project>

+ 25 - 0
SongBrowserPlugin/SongBrowserPlugin.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27703.2026
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SongBrowserPlugin", "SongBrowserPlugin.csproj", "{6F9B6801-9F4B-4D1F-805D-271C95733814}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		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}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6F9B6801-9F4B-4D1F-805D-271C95733814}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {15DAC411-1718-4755-BAE2-16A58BAA70BE}
+	EndGlobalSection
+EndGlobal

+ 99 - 0
SongBrowserPlugin/SongBrowserSettings.cs

@@ -0,0 +1,99 @@
+using System;
+using System.IO;
+using System.Xml.Serialization;
+
+
+
+namespace SongBrowserPlugin
+{
+    [Serializable]
+    public enum SongSortMode
+    {
+        Default,
+        Favorites,
+    }
+
+    [Serializable]
+    public class SongBrowserSettings
+    {
+        public SongSortMode sortMode = default(SongSortMode);
+
+        [NonSerialized]
+        private static Logger Log = new Logger("SongBrowserPlugin-Settings");
+
+        /// <summary>
+        /// Constructor.
+        /// </summary>
+        public SongBrowserSettings()
+        {
+        }
+
+        /// <summary>
+        /// Helper to acquire settings path at runtime.
+        /// </summary>
+        /// <returns></returns>
+        public static String SettingsPath()
+        {
+            return Path.Combine(Environment.CurrentDirectory, "song_browser_settings.xml");
+        }
+
+        /// <summary>
+        /// Load the settings file for this plugin.
+        /// If we fail to load return Default settings.
+        /// </summary>
+        /// <returns>SongBrowserSettings</returns>
+        public static SongBrowserSettings Load()
+        {
+            Log.Debug("Load Song Browser Settings");
+            SongBrowserSettings retVal = null;
+
+            String settingsFilePath = SongBrowserSettings.SettingsPath();
+            if (!File.Exists(settingsFilePath))
+            {
+                Log.Debug("Settings file does not exist, returning defaults: " + settingsFilePath);
+                return new SongBrowserSettings();
+            }
+
+            // Deserialization from JSON            
+            FileStream fs = null;
+            try
+            {
+                fs = File.OpenRead(settingsFilePath);
+
+                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);
+
+                // Return default settings
+                retVal = new SongBrowserSettings();
+            }
+            finally
+            {
+                if (fs != null) { fs.Close(); }
+            }
+            
+            return retVal;
+        }
+
+        /// <summary>
+        /// Save this Settings insance to file.
+        /// </summary>
+        public void Save()
+        {            
+            String settingsFilePath = SongBrowserSettings.SettingsPath();
+
+            FileStream fs = new FileStream(settingsFilePath, FileMode.Create, FileAccess.Write);
+            
+            XmlSerializer serializer = new XmlSerializer(typeof(SongBrowserSettings));           
+            serializer.Serialize(fs, this);
+            
+            fs.Close();            
+        }
+    }
+}

+ 80 - 0
SongBrowserPlugin/UIBuilder.cs

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

+ 4 - 0
SongBrowserPlugin/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  
+</packages>