├── SongLoaderPlugin ├── LogSeverity.cs ├── IScriptableObjectResetable.cs ├── packages.config ├── OverrideClasses │ ├── CustomBeatmapDataSO.cs │ ├── CustomLevelCollectionSO.cs │ ├── CustomLevelCollectionForGameplayMode.cs │ ├── CustomLevelCollectionsForGameplayModes.cs │ └── CustomLevel.cs ├── Plugin.cs ├── SongLoaderPlugin.sln ├── ScriptableObjectPool.cs ├── Properties │ └── AssemblyInfo.cs ├── Utils.cs ├── ReflectionUtil.cs ├── NoteHitVolumeChanger.cs ├── CustomSongInfo.cs ├── ProgressBar.cs ├── SongLoaderPlugin.csproj ├── Internals │ └── Unzip.cs ├── SongLoader.cs └── SimpleJSON.cs ├── LICENSE ├── README.md └── .gitignore /SongLoaderPlugin/LogSeverity.cs: -------------------------------------------------------------------------------- 1 | namespace SongLoaderPlugin 2 | { 3 | public enum LogSeverity 4 | { 5 | Info, 6 | Warn, 7 | Error 8 | } 9 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/IScriptableObjectResetable.cs: -------------------------------------------------------------------------------- 1 | namespace SongLoaderPlugin 2 | { 3 | public interface IScriptableObjectResetable 4 | { 5 | void Reset(); 6 | } 7 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SongLoaderPlugin/OverrideClasses/CustomBeatmapDataSO.cs: -------------------------------------------------------------------------------- 1 | namespace SongLoaderPlugin.OverrideClasses 2 | { 3 | public class CustomBeatmapDataSO : BeatmapDataSO, IScriptableObjectResetable 4 | { 5 | public string jsonData 6 | { 7 | get { return _jsonData; } 8 | } 9 | public void Reset() 10 | { 11 | 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/OverrideClasses/CustomLevelCollectionSO.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SongLoaderPlugin.OverrideClasses 4 | { 5 | public class CustomLevelCollectionSO : StandardLevelCollectionSO 6 | { 7 | public List LevelList { get; private set; } 8 | 9 | public void Init(StandardLevelSO[] newLevels) 10 | { 11 | LevelList = new List(newLevels); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/OverrideClasses/CustomLevelCollectionForGameplayMode.cs: -------------------------------------------------------------------------------- 1 | namespace SongLoaderPlugin.OverrideClasses 2 | { 3 | public class CustomLevelCollectionForGameplayMode : LevelCollectionsForGameplayModes.LevelCollectionForGameplayMode 4 | { 5 | public CustomLevelCollectionForGameplayMode(GameplayMode gameplayMode, StandardLevelCollectionSO newLevelCollection) 6 | { 7 | _levelCollection = newLevelCollection; 8 | _gameplayMode = gameplayMode; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/Plugin.cs: -------------------------------------------------------------------------------- 1 | using IllusionPlugin; 2 | using UnityEngine; 3 | 4 | namespace SongLoaderPlugin 5 | { 6 | public class Plugin : IPlugin 7 | { 8 | public string Name 9 | { 10 | get { return "Song Loader Plugin"; } 11 | } 12 | 13 | public string Version 14 | { 15 | get { return "v4.2.2"; } 16 | } 17 | 18 | public void OnApplicationStart() 19 | { 20 | 21 | } 22 | 23 | public void OnApplicationQuit() 24 | { 25 | PlayerPrefs.DeleteKey("lbPatched"); 26 | } 27 | 28 | public void OnLevelWasLoaded(int level) 29 | { 30 | 31 | } 32 | 33 | public void OnLevelWasInitialized(int level) 34 | { 35 | if (level != SongLoader.MenuIndex) return; 36 | SongLoader.OnLoad(); 37 | } 38 | 39 | public void OnUpdate() 40 | { 41 | 42 | } 43 | 44 | public void OnFixedUpdate() 45 | { 46 | 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/SongLoaderPlugin.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SongLoaderPlugin", "SongLoaderPlugin.csproj", "{6F9B6801-9F4B-4D1F-805D-271C95733814}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {6F9B6801-9F4B-4D1F-805D-271C95733814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {6F9B6801-9F4B-4D1F-805D-271C95733814}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {6F9B6801-9F4B-4D1F-805D-271C95733814}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {6F9B6801-9F4B-4D1F-805D-271C95733814}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /SongLoaderPlugin/ScriptableObjectPool.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | namespace SongLoaderPlugin 6 | { 7 | public class ScriptableObjectPool where T : ScriptableObject, IScriptableObjectResetable 8 | { 9 | private readonly List _pool = new List(); 10 | private readonly List _createdObj = new List(); 11 | 12 | public T Get() 13 | { 14 | if (_pool.Count == 0) 15 | { 16 | var newObj = ScriptableObject.CreateInstance(); 17 | _createdObj.Add(newObj); 18 | return newObj; 19 | } 20 | 21 | var fromPool = _pool.First(); 22 | _pool.RemoveAt(0); 23 | return fromPool; 24 | } 25 | 26 | public void Return(T obj) 27 | { 28 | obj.Reset(); 29 | _pool.Add(obj); 30 | } 31 | 32 | public void ReturnAll() 33 | { 34 | _pool.Clear(); 35 | _createdObj.ForEach(x => x.Reset()); 36 | _pool.AddRange(_createdObj); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 xyonico 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SongLoaderPlugin/OverrideClasses/CustomLevelCollectionsForGameplayModes.cs: -------------------------------------------------------------------------------- 1 | namespace SongLoaderPlugin.OverrideClasses 2 | { 3 | public class CustomLevelCollectionsForGameplayModes : LevelCollectionsForGameplayModes 4 | { 5 | public override StandardLevelSO[] GetLevels(GameplayMode gameplayMode) 6 | { 7 | foreach (var levelCollectionForGameplayMode in _collections) 8 | { 9 | if (levelCollectionForGameplayMode.gameplayMode == gameplayMode) 10 | { 11 | var customLevelCollections = levelCollectionForGameplayMode as CustomLevelCollectionForGameplayMode; 12 | if (customLevelCollections != null) 13 | { 14 | var customLevelCollection = customLevelCollections.levelCollection as CustomLevelCollectionSO; 15 | if (customLevelCollection != null) 16 | { 17 | return customLevelCollection.LevelList.ToArray(); 18 | } 19 | } 20 | return levelCollectionForGameplayMode.levelCollection.levels; 21 | } 22 | } 23 | return null; 24 | } 25 | 26 | public void SetCollections(LevelCollectionForGameplayMode[] levelCollectionForGameplayModes) 27 | { 28 | _collections = levelCollectionForGameplayModes; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("SongLoaderPlugin")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("SongLoaderPlugin")] 12 | [assembly: AssemblyCopyright("Copyright © 2018")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("6F9B6801-9F4B-4D1F-805D-271C95733814")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("4.2.2")] 35 | [assembly: AssemblyFileVersion("4.2.2")] -------------------------------------------------------------------------------- /SongLoaderPlugin/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | using System.IO; 5 | 6 | namespace SongLoaderPlugin 7 | { 8 | public static class Utils 9 | { 10 | public static TEnum ToEnum(this string strEnumValue, TEnum defaultValue) 11 | { 12 | if (!Enum.IsDefined(typeof(TEnum), strEnumValue)) 13 | return defaultValue; 14 | 15 | return (TEnum)Enum.Parse(typeof(TEnum), strEnumValue); 16 | } 17 | 18 | public static string CreateMD5FromString(string input) 19 | { 20 | // Use input string to calculate MD5 hash 21 | using (var md5 = MD5.Create()) 22 | { 23 | var inputBytes = Encoding.ASCII.GetBytes(input); 24 | var hashBytes = md5.ComputeHash(inputBytes); 25 | 26 | // Convert the byte array to hexadecimal string 27 | var sb = new StringBuilder(); 28 | for (int i = 0; i < hashBytes.Length; i++) 29 | { 30 | sb.Append(hashBytes[i].ToString("X2")); 31 | } 32 | return sb.ToString(); 33 | } 34 | } 35 | 36 | public static bool CreateMD5FromFile(string path, out string hash) 37 | { 38 | hash = ""; 39 | if (!File.Exists(path)) return false; 40 | using (var md5 = MD5.Create()) 41 | { 42 | using (var stream = File.OpenRead(path)) 43 | { 44 | var hashBytes = md5.ComputeHash(stream); 45 | 46 | // Convert the byte array to hexadecimal string 47 | var sb = new StringBuilder(); 48 | foreach (var hashByte in hashBytes) 49 | { 50 | sb.Append(hashByte.ToString("X2")); 51 | } 52 | 53 | hash = sb.ToString(); 54 | return true; 55 | } 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/ReflectionUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEngine; 4 | 5 | namespace SongLoaderPlugin 6 | { 7 | public static class ReflectionUtil 8 | { 9 | public static void SetPrivateField(this object obj, string fieldName, object value) 10 | { 11 | var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 12 | prop.SetValue(obj, value); 13 | } 14 | 15 | public static T GetPrivateField(this object obj, string fieldName) 16 | { 17 | var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 18 | var value = prop.GetValue(obj); 19 | return (T) value; 20 | } 21 | 22 | public static void SetPrivateProperty(this object obj, string propertyName, object value) 23 | { 24 | var prop = obj.GetType().GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 25 | prop.SetValue(obj, value, null); 26 | } 27 | 28 | public static void InvokePrivateMethod(this object obj, string methodName, object[] methodParams) 29 | { 30 | MethodInfo dynMethod = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); 31 | dynMethod.Invoke(obj, methodParams); 32 | } 33 | 34 | public static Component CopyComponent(Component original, Type originalType, Type overridingType, 35 | GameObject destination) 36 | { 37 | var copy = destination.AddComponent(overridingType); 38 | var fields = originalType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | 39 | BindingFlags.GetField); 40 | foreach (var field in fields) 41 | { 42 | field.SetValue(copy, field.GetValue(original)); 43 | } 44 | 45 | return copy; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SongLoaderPlugin/NoteHitVolumeChanger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace SongLoaderPlugin 7 | { 8 | public static class NoteHitVolumeChanger 9 | { 10 | public static bool PrefabFound { get; private set; } 11 | private static NoteCutSoundEffect _noteCutSoundEffect; 12 | private static float _normalVolume; 13 | private static float _normalMissVolume; 14 | 15 | //Code snippet comes from Taz's NoteHitVolume plugin: 16 | //https://github.com/taz030485/NoteHitVolume/blob/master/NoteHitVolume/NoteHitVolume.cs 17 | public static void SetVolume(float hitVolume, float missVolume) 18 | { 19 | hitVolume = Mathf.Clamp01(hitVolume); 20 | missVolume = Mathf.Clamp01(missVolume); 21 | var pooled = false; 22 | if (_noteCutSoundEffect == null) 23 | { 24 | var noteCutSoundEffectManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); 25 | if (noteCutSoundEffectManager == null) return; 26 | _noteCutSoundEffect = 27 | noteCutSoundEffectManager.GetPrivateField("_noteCutSoundEffectPrefab"); 28 | pooled = true; 29 | PrefabFound = true; 30 | } 31 | 32 | if (_normalVolume == 0) 33 | { 34 | _normalVolume = _noteCutSoundEffect.GetPrivateField("_goodCutVolume"); 35 | _normalMissVolume = _noteCutSoundEffect.GetPrivateField("_badCutVolume"); 36 | } 37 | 38 | var newGoodVolume = _normalVolume * hitVolume; 39 | var newBadVolume = _normalMissVolume * missVolume; 40 | _noteCutSoundEffect.SetPrivateField("_goodCutVolume", newGoodVolume); 41 | _noteCutSoundEffect.SetPrivateField("_badCutVolume", newBadVolume); 42 | 43 | if (pooled) 44 | { 45 | var pool = Resources.FindObjectsOfTypeAll(); 46 | foreach (var effect in pool) 47 | { 48 | if (effect.name.Contains("Clone")) 49 | { 50 | effect.SetPrivateField("_goodCutVolume", newGoodVolume); 51 | effect.SetPrivateField("_badCutVolume", newBadVolume); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/CustomSongInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace SongLoaderPlugin 5 | { 6 | [Serializable] 7 | public class CustomSongInfo 8 | { 9 | public string songName = "Missing name"; 10 | public string songSubName = string.Empty; 11 | [Obsolete("This has been renamed to songAuthorName to match game implementation")] 12 | public string authorName = string.Empty; 13 | public string songAuthorName; 14 | public float beatsPerMinute = 100; 15 | public float previewStartTime = 12; 16 | public float previewDuration = 10; 17 | public float songTimeOffset; 18 | public float shuffle; 19 | public float shufflePeriod; 20 | public string environmentName = "DefaultEnvironment"; 21 | public string audioPath; 22 | public string coverImagePath = "cover.jpg"; 23 | public bool oneSaber; 24 | public float noteHitVolume = 1; 25 | public float noteMissVolume = 1; 26 | public DifficultyLevel[] difficultyLevels; 27 | public string path; 28 | public string levelId; 29 | 30 | [Serializable] 31 | public class DifficultyLevel 32 | { 33 | public string difficulty; 34 | public int difficultyRank; 35 | [Obsolete("audioPath has been moved to the song info. " + 36 | "If the song audioPath is empty, it will try to use the audioPath in the first difficulty it finds.")] 37 | public string audioPath; 38 | public string jsonPath; 39 | public string json; 40 | public float noteJumpMovementSpeed; 41 | } 42 | 43 | public string GetIdentifier() 44 | { 45 | var combinedJson = ""; 46 | foreach (var diffLevel in difficultyLevels) 47 | { 48 | if (!File.Exists(path + "/" + diffLevel.jsonPath)) 49 | { 50 | continue; 51 | } 52 | 53 | diffLevel.json = File.ReadAllText(path + "/" + diffLevel.jsonPath); 54 | combinedJson += diffLevel.json; 55 | } 56 | 57 | var hash = Utils.CreateMD5FromString(combinedJson); 58 | levelId = hash + "∎" + string.Join("∎", songName, songSubName, GetSongAuthor(), beatsPerMinute.ToString()) + "∎"; 59 | return levelId; 60 | } 61 | 62 | public string GetSongAuthor() 63 | { 64 | if (songAuthorName == null) 65 | { 66 | songAuthorName = authorName; 67 | } 68 | 69 | return songAuthorName; 70 | } 71 | 72 | public string GetAudioPath() 73 | { 74 | if (!string.IsNullOrEmpty(audioPath)) return audioPath; 75 | 76 | foreach (var difficultyLevel in difficultyLevels) 77 | { 78 | if (string.IsNullOrEmpty(difficultyLevel.audioPath)) continue; 79 | return difficultyLevel.audioPath; 80 | } 81 | 82 | return null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BeatSaberSongLoader 2 | A plugin for adding custom songs into Beat Saber. 3 | 4 | *This mod works on both the Steam and Oculus Store versions.* 5 | 6 | ## Installation Instructions 7 | 1. Download the latest release from here: https://github.com/xyonico/BeatSaberSongLoader/releases 8 | 2. Extract the .zip file into the `Oculus Apps\Software\hyperbolic-magnetism-beat-saber` for Oculus Home OR `steamapps\common\Beat Saber` for Steam. (The one with Beat Saber.exe) 9 | 10 | The Beat Saber folder should look something like this: 11 | * `Beat Saber_Data` 12 | * `CustomSongs` 13 | * `IPA` 14 | * `Plugins` 15 | * `Beat Saber (Patch & Launch)` 16 | * `Beat Saber.exe` 17 | * `IPA.exe` 18 | * `Mono.Cecil.dll` 19 | * `UnityPlayer.dll` 20 | 3. Done! 21 | 22 | ## Usage 23 | 1. Launch Beat Saber through the platform you purchased it on. 24 | 2. Go to 'Solo' -> 'Standard' and your custom song will be available to play at the bottom of the list. 25 | 26 | 27 | ## Installing Custom Songs 28 | The following files must be placed within their own folder inside the "CustomSongs" folder. 29 | 30 | Required files: 31 | 1. cover.jpg (Size 256x256) 32 | -This is the picture shown next to song in the selection screen. 33 | -The name can be whatever you want, make sure its the same as the one found in info.json 34 | -Only supported image types are jpg and png 35 | 2. song.wav / song.ogg 36 | -This is your song you would like to load 37 | -Name must be the same as in info.json 38 | -Only supported audio types are wav and ogg 39 | 3. easy.json / normal.json / hard.json / expert.json 40 | -This is the note chart for each difficulty 41 | -Names must match the "jsonPath" in info.json 42 | -Use a Beat Saber editor to make your own note chart for the song 43 | 4. info.json 44 | -Contains the info for the song 45 | 46 | The following is a template for you to use: 47 | ```json 48 | { 49 | "songName":"YourSongName", 50 | "songSubName":"ft. Name", 51 | "songAuthorName":"AuthorName", 52 | "beatsPerMinute":179.0, 53 | "previewStartTime":12.0, 54 | "previewDuration":10.0, 55 | "audioPath":"YourSong.ogg", 56 | "coverImagePath":"cover.jpg", 57 | "environmentName":"DefaultEnvironment", 58 | "songTimeOffset":-2, 59 | "shuffle":1, 60 | "shufflePeriod":0.2, 61 | "oneSaber":true, 62 | "difficultyLevels": [ 63 | { "difficulty":"Expert", "difficultyRank":4, "jsonPath":"expert.json" }, 64 | { "difficulty":"Easy", "difficultyRank":0, "jsonPath":"easy.json" } 65 | ] 66 | } 67 | ``` 68 | ___ 69 | 70 | ### info.json Explanation 71 | ``` 72 | "songName" - Name of your song 73 | "songSubName" - Text rendered in smaller letters next to song name. (ft. Artist) 74 | "beatsPerMinute" - BPM of the song you are using 75 | "previewStartTime" - How many seconds into the song the preview should start 76 | "previewDuration" - Time in seconds the song will be previewed in selection screen 77 | "coverImagePath" - Cover image name 78 | "environmentName" - Game environment to be used 79 | "songTimeOffset" - Time in seconds of how early a song should start. Negative numbers for starting the song later 80 | "shuffle" - Time in number of beats how much a note should shift 81 | "shufflePeriod" - Time in number of beats how often a note should shift. Don't ask me why this is a feature, I don't know 82 | "oneSaber" - true or false if it should appear in the one saber list 83 | 84 | All possible environmentNames: 85 | -DefaultEnvironment 86 | -BigMirrorEnvironment 87 | -TriangleEnvironment 88 | -NiceEnvironment 89 | 90 | "difficultyLevels": [ 91 | { 92 | "difficulty": This can only be set to Easy, Normal, Hard, Expert or ExpertPlus, 93 | "difficultyRank": Currently unused whole number for ranking difficulty, 94 | "jsonPath": The name of the json file for this specific difficulty 95 | } 96 | ] 97 | ``` 98 | 99 | # Keyboard Shortcuts 100 | *(Make sure Beat Saber's window is in focus when using these shortcuts)* 101 | --- 102 | * Press Ctrl+R when in the main menu to do a full refresh. (This means removing deleted songs and updating existing songs) 103 | * Press R when in main menu to do a quick refresh (This will only add new songs in the CustomSongs folder) 104 | -------------------------------------------------------------------------------- /SongLoaderPlugin/OverrideClasses/CustomLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace SongLoaderPlugin.OverrideClasses 8 | { 9 | public class CustomLevel : StandardLevelSO, IScriptableObjectResetable 10 | { 11 | public CustomSongInfo customSongInfo { get; private set; } 12 | public bool AudioClipLoading { get; set; } 13 | public bool BPMAndNoteSpeedFixed { get; private set; } 14 | 15 | public void Init(CustomSongInfo newCustomSongInfo) 16 | { 17 | customSongInfo = newCustomSongInfo; 18 | _levelID = customSongInfo.GetIdentifier(); 19 | _songName = customSongInfo.songName; 20 | _songSubName = customSongInfo.songSubName; 21 | _songAuthorName = customSongInfo.GetSongAuthor(); 22 | _beatsPerMinute = customSongInfo.beatsPerMinute; 23 | _songTimeOffset = customSongInfo.songTimeOffset; 24 | _shuffle = customSongInfo.shuffle; 25 | _shufflePeriod = customSongInfo.shufflePeriod; 26 | _previewStartTime = customSongInfo.previewStartTime; 27 | _previewDuration = customSongInfo.previewDuration; 28 | _environmentSceneInfo = LoadSceneInfo(customSongInfo.environmentName); 29 | } 30 | 31 | public void SetAudioClip(AudioClip newAudioClip) 32 | { 33 | _audioClip = newAudioClip; 34 | } 35 | 36 | public void SetCoverImage(Sprite newCoverImage) 37 | { 38 | _coverImage = newCoverImage; 39 | } 40 | 41 | public void SetDifficultyBeatmaps(DifficultyBeatmap[] newDifficultyBeatmaps) 42 | { 43 | _difficultyBeatmaps = newDifficultyBeatmaps; 44 | } 45 | 46 | private static SceneInfo LoadSceneInfo(string environmentName) 47 | { 48 | var sceneInfo = Resources.Load("SceneInfo/" + environmentName + "SceneInfo"); 49 | return sceneInfo == null ? Resources.Load("SceneInfo/DefaultEnvironmentSceneInfo") : sceneInfo; 50 | } 51 | 52 | public void FixBPMAndGetNoteJumpMovementSpeed() 53 | { 54 | if (BPMAndNoteSpeedFixed) return; 55 | var bpms = new Dictionary {{_beatsPerMinute, 0}}; 56 | foreach (var diffLevel in customSongInfo.difficultyLevels) 57 | { 58 | if (string.IsNullOrEmpty(diffLevel.json)) continue; 59 | float bpm, noteSpeed; 60 | GetBPMAndNoteJump(diffLevel.json, out bpm, out noteSpeed); 61 | if (bpm > 0) 62 | { 63 | if (bpms.ContainsKey(bpm)) 64 | { 65 | bpms[bpm]++; 66 | } 67 | else 68 | { 69 | bpms.Add(bpm, 1); 70 | } 71 | } 72 | 73 | var diffBeatmap = _difficultyBeatmaps.FirstOrDefault(x => 74 | diffLevel.difficulty.ToEnum(LevelDifficulty.Normal) == x.difficulty); 75 | var customBeatmap = diffBeatmap as CustomDifficultyBeatmap; 76 | if (customBeatmap == null) continue; 77 | if (customBeatmap.noteJumpMovementSpeed > 0) continue; 78 | customBeatmap.SetNoteJumpMovementSpeed(noteSpeed); 79 | } 80 | 81 | _beatsPerMinute = bpms.OrderByDescending(x => x.Value).First().Key; 82 | 83 | foreach (var difficultyBeatmap in _difficultyBeatmaps) 84 | { 85 | var customBeatmap = difficultyBeatmap as CustomDifficultyBeatmap; 86 | if (customBeatmap == null) continue; 87 | customBeatmap.BeatmapDataSO.SetRequiredDataForLoad(_beatsPerMinute, _shuffle, _shufflePeriod); 88 | customBeatmap.BeatmapDataSO.Load(); 89 | } 90 | 91 | BPMAndNoteSpeedFixed = true; 92 | } 93 | 94 | //This is quicker than using a JSON parser 95 | private void GetBPMAndNoteJump(string json, out float bpm, out float noteJumpSpeed) 96 | { 97 | bpm = 0; 98 | noteJumpSpeed = 0; 99 | var split = json.Split(':'); 100 | for (var i = 0; i < split.Length; i++) 101 | { 102 | if (split[i].Contains("_beatsPerMinute")) 103 | { 104 | bpm = Convert.ToSingle(split[i + 1].Split(',')[0], CultureInfo.InvariantCulture); 105 | } 106 | 107 | if (split[i].Contains("_noteJumpSpeed")) 108 | { 109 | noteJumpSpeed = Convert.ToSingle(split[i + 1].Split(',')[0], CultureInfo.InvariantCulture); 110 | } 111 | } 112 | } 113 | 114 | public class CustomDifficultyBeatmap : DifficultyBeatmap 115 | { 116 | public CustomDifficultyBeatmap(IStandardLevel parentLevel, LevelDifficulty difficulty, int difficultyRank, float noteJumpMovementSpeed, BeatmapDataSO beatmapData) : base(parentLevel, difficulty, difficultyRank, noteJumpMovementSpeed, beatmapData) 117 | { 118 | } 119 | 120 | public CustomBeatmapDataSO BeatmapDataSO 121 | { 122 | get { return _beatmapData as CustomBeatmapDataSO; } 123 | } 124 | 125 | public void SetNoteJumpMovementSpeed(float newNoteJumpMovementSpeed) 126 | { 127 | _noteJumpMovementSpeed = newNoteJumpMovementSpeed; 128 | } 129 | } 130 | 131 | public void Reset() 132 | { 133 | _audioClip = null; 134 | BPMAndNoteSpeedFixed = false; 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/ProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections; 3 | using SongLoaderPlugin.OverrideClasses; 4 | using TMPro; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | using UnityEngine.UI; 8 | 9 | namespace SongLoaderPlugin 10 | { 11 | public class ProgressBar : MonoBehaviour 12 | { 13 | private Canvas _canvas; 14 | private TMP_Text _creditText; 15 | private TMP_Text _headerText; 16 | private Image _loadingBackg; 17 | private Image _loadingBar; 18 | 19 | private static readonly Vector3 Position = new Vector3(0, 2.5f, 2.5f); 20 | private static readonly Vector3 Rotation = new Vector3(0, 0, 0); 21 | private static readonly Vector3 Scale = new Vector3(0.01f, 0.01f, 0.01f); 22 | 23 | private static readonly Vector2 CanvasSize = new Vector2(100, 50); 24 | 25 | private static readonly Vector2 CreditPosition = new Vector2(0, 22); 26 | private const string CreditText = "Song Loader Plugin by xyonico"; 27 | private const float CreditFontSize = 9f; 28 | private static readonly Vector2 HeaderPosition = new Vector2(0, 15); 29 | private static readonly Vector2 HeaderSize = new Vector2(100, 20); 30 | private const string HeaderText = "Loading songs..."; 31 | private const float HeaderFontSize = 15f; 32 | 33 | private static readonly Vector2 LoadingBarSize = new Vector2(100, 10); 34 | private static readonly Color BackgroundColor = new Color(0, 0, 0, 0.2f); 35 | 36 | private bool _showingMessage; 37 | 38 | public static ProgressBar Create() 39 | { 40 | return new GameObject("Progress Bar").AddComponent(); 41 | } 42 | 43 | public void ShowMessage(string message, float time) 44 | { 45 | _showingMessage = true; 46 | _headerText.text = message; 47 | _loadingBar.enabled = false; 48 | _loadingBackg.enabled = false; 49 | _canvas.enabled = true; 50 | StartCoroutine(DisableCanvasRoutine(time)); 51 | } 52 | 53 | public void ShowMessage(string message) 54 | { 55 | _showingMessage = true; 56 | _headerText.text = message; 57 | _loadingBar.enabled = false; 58 | _loadingBackg.enabled = false; 59 | _canvas.enabled = true; 60 | } 61 | 62 | private void OnEnable() 63 | { 64 | SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged; 65 | SongLoader.LoadingStartedEvent += SongLoaderOnLoadingStartedEvent; 66 | SongLoader.SongsLoadedEvent += SongLoaderOnSongsLoadedEvent; 67 | } 68 | 69 | private void OnDisable() 70 | { 71 | SceneManager.activeSceneChanged -= SceneManagerOnActiveSceneChanged; 72 | SongLoader.LoadingStartedEvent -= SongLoaderOnLoadingStartedEvent; 73 | SongLoader.SongsLoadedEvent -= SongLoaderOnSongsLoadedEvent; 74 | } 75 | 76 | private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene arg1) 77 | { 78 | if (arg1.buildIndex == 1) 79 | { 80 | if (_showingMessage) 81 | { 82 | _canvas.enabled = true; 83 | } 84 | } 85 | else 86 | { 87 | _canvas.enabled = false; 88 | } 89 | } 90 | 91 | private void SongLoaderOnLoadingStartedEvent(SongLoader obj) 92 | { 93 | _showingMessage = false; 94 | _headerText.text = HeaderText; 95 | _loadingBar.enabled = true; 96 | _loadingBackg.enabled = true; 97 | _canvas.enabled = true; 98 | } 99 | 100 | private void SongLoaderOnSongsLoadedEvent(SongLoader arg1, List arg2) 101 | { 102 | _showingMessage = false; 103 | _headerText.text = arg2.Count + " songs loaded"; 104 | _loadingBar.enabled = false; 105 | _loadingBackg.enabled = false; 106 | StartCoroutine(DisableCanvasRoutine(5f)); 107 | } 108 | 109 | private IEnumerator DisableCanvasRoutine(float time) 110 | { 111 | yield return new WaitForSecondsRealtime(time); 112 | _canvas.enabled = false; 113 | _showingMessage = false; 114 | } 115 | 116 | private void Awake() 117 | { 118 | gameObject.transform.position = Position; 119 | gameObject.transform.eulerAngles = Rotation; 120 | gameObject.transform.localScale = Scale; 121 | 122 | _canvas = gameObject.AddComponent(); 123 | _canvas.renderMode = RenderMode.WorldSpace; 124 | _canvas.enabled = false; 125 | var rectTransform = _canvas.transform as RectTransform; 126 | rectTransform.sizeDelta = CanvasSize; 127 | 128 | _creditText = new GameObject("Credit").AddComponent(); 129 | rectTransform = _creditText.transform as RectTransform; 130 | rectTransform.SetParent(_canvas.transform, false); 131 | rectTransform.anchoredPosition = CreditPosition; 132 | rectTransform.sizeDelta = HeaderSize; 133 | _creditText.text = CreditText; 134 | _creditText.fontSize = CreditFontSize; 135 | 136 | _headerText = new GameObject("Header").AddComponent(); 137 | rectTransform = _headerText.transform as RectTransform; 138 | rectTransform.SetParent(_canvas.transform, false); 139 | rectTransform.anchoredPosition = HeaderPosition; 140 | rectTransform.sizeDelta = HeaderSize; 141 | _headerText.text = HeaderText; 142 | _headerText.fontSize = HeaderFontSize; 143 | 144 | _loadingBackg = new GameObject("Background").AddComponent(); 145 | rectTransform = _loadingBackg.transform as RectTransform; 146 | rectTransform.SetParent(_canvas.transform, false); 147 | rectTransform.sizeDelta = LoadingBarSize; 148 | _loadingBackg.color = BackgroundColor; 149 | 150 | _loadingBar = new GameObject("Loading Bar").AddComponent(); 151 | rectTransform = _loadingBar.transform as RectTransform; 152 | rectTransform.SetParent(_canvas.transform, false); 153 | rectTransform.sizeDelta = LoadingBarSize; 154 | var tex = Texture2D.whiteTexture; 155 | var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.one * 0.5f, 100, 1); 156 | _loadingBar.sprite = sprite; 157 | _loadingBar.type = Image.Type.Filled; 158 | _loadingBar.fillMethod = Image.FillMethod.Horizontal; 159 | _loadingBar.color = new Color(1, 1, 1, 0.5f); 160 | 161 | DontDestroyOnLoad(gameObject); 162 | } 163 | 164 | private void Update() 165 | { 166 | if (!_canvas.enabled) return; 167 | _loadingBar.fillAmount = SongLoader.LoadingProgress; 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /SongLoaderPlugin/SongLoaderPlugin.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {6F9B6801-9F4B-4D1F-805D-271C95733814} 8 | Library 9 | Properties 10 | SongLoaderPlugin 11 | SongLoaderPlugin 12 | v4.6 13 | 512 14 | 6 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\Assembly-CSharp.dll 38 | 39 | 40 | ..\..\IPA\IllusionPlugin\bin\Release\IllusionPlugin.dll 41 | 42 | 43 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\System.dll 44 | 45 | 46 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\System.Core.dll 47 | 48 | 49 | 50 | 51 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\TextMeshPro-1.0.55.2017.1.0b12.dll 52 | 53 | 54 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.dll 55 | 56 | 57 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.AudioModule.dll 58 | 59 | 60 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll 61 | 62 | 63 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll 64 | 65 | 66 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.IMGUIModule.dll 67 | 68 | 69 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.JSONSerializeModule.dll 70 | 71 | 72 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UI.dll 73 | 74 | 75 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll 76 | 77 | 78 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UIModule.dll 79 | 80 | 81 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll 82 | 83 | 84 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestModule.dll 85 | 86 | 87 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestTextureModule.dll 88 | 89 | 90 | D:\SteamLibrary\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 124 | -------------------------------------------------------------------------------- /SongLoaderPlugin/Internals/Unzip.cs: -------------------------------------------------------------------------------- 1 | // Unzip class for .NET 3.5 Client Profile or Mono 2.10 2 | // Written by Alexey Yakovlev 3 | // https://github.com/yallie/unzip 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.IO; 9 | using System.IO.Compression; 10 | using System.Linq; 11 | using System.Text; 12 | 13 | namespace SongLoaderPlugin.Internals 14 | { 15 | /// 16 | /// Unzip helper class. 17 | /// 18 | internal class Unzip : IDisposable 19 | { 20 | /// 21 | /// Zip archive entry. 22 | /// 23 | public class Entry 24 | { 25 | /// 26 | /// Gets or sets the name of a file or a directory. 27 | /// 28 | public string Name { get; set; } 29 | 30 | /// 31 | /// Gets or sets the comment. 32 | /// 33 | public string Comment { get; set; } 34 | 35 | /// 36 | /// Gets or sets the CRC32. 37 | /// 38 | public uint Crc32 { get; set; } 39 | 40 | /// 41 | /// Gets or sets the compressed size of the file. 42 | /// 43 | public int CompressedSize { get; set; } 44 | 45 | /// 46 | /// Gets or sets the original size of the file. 47 | /// 48 | public int OriginalSize { get; set; } 49 | 50 | /// 51 | /// Gets or sets a value indicating whether this is deflated. 52 | /// 53 | public bool Deflated { get; set; } 54 | 55 | /// 56 | /// Gets a value indicating whether this is a directory. 57 | /// 58 | public bool IsDirectory { get { return Name.EndsWith("/"); } } 59 | 60 | /// 61 | /// Gets or sets the timestamp. 62 | /// 63 | public DateTime Timestamp { get; set; } 64 | 65 | /// 66 | /// Gets a value indicating whether this is a file. 67 | /// 68 | public bool IsFile { get { return !IsDirectory; } } 69 | 70 | [EditorBrowsable(EditorBrowsableState.Never)] 71 | public int HeaderOffset { get; set; } 72 | 73 | [EditorBrowsable(EditorBrowsableState.Never)] 74 | public int DataOffset { get; set; } 75 | } 76 | 77 | /// 78 | /// CRC32 calculation helper. 79 | /// 80 | public class Crc32Calculator 81 | { 82 | private static readonly uint[] Crc32Table = 83 | { 84 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 85 | 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 86 | 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 87 | 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 88 | 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 89 | 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 90 | 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 91 | 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 92 | 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 93 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 94 | 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 95 | 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 96 | 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 97 | 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 98 | 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 99 | 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 100 | 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 101 | 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 102 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 103 | 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 104 | 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 105 | 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 106 | 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 107 | 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 108 | 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 109 | 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 110 | 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 111 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 112 | 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 113 | 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 114 | 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 115 | 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, 116 | }; 117 | 118 | private uint crcValue = 0xffffffff; 119 | 120 | public uint Crc32 { get { return crcValue ^ 0xffffffff; } } 121 | 122 | public void UpdateWithBlock(byte[] buffer, int numberOfBytes) 123 | { 124 | for (var i = 0; i < numberOfBytes; i++) 125 | { 126 | crcValue = (crcValue >> 8) ^ Crc32Table[buffer[i] ^ crcValue & 0xff]; 127 | } 128 | } 129 | } 130 | 131 | /// 132 | /// Provides data for the ExtractProgress event. 133 | /// 134 | public class FileProgressEventArgs : ProgressChangedEventArgs 135 | { 136 | /// 137 | /// Initializes a new instance of the class. 138 | /// 139 | /// The current file. 140 | /// The total files. 141 | /// Name of the file. 142 | public FileProgressEventArgs(int currentFile, int totalFiles, string fileName) 143 | : base(totalFiles != 0 ? currentFile * 100 / totalFiles : 100, fileName) 144 | { 145 | CurrentFile = currentFile; 146 | TotalFiles = totalFiles; 147 | FileName = fileName; 148 | } 149 | 150 | /// 151 | /// Gets the current file. 152 | /// 153 | public int CurrentFile { get; private set; } 154 | 155 | /// 156 | /// Gets the total files. 157 | /// 158 | public int TotalFiles { get; private set; } 159 | 160 | /// 161 | /// Gets the name of the file. 162 | /// 163 | public string FileName { get; private set; } 164 | } 165 | 166 | private const int EntrySignature = 0x02014B50; 167 | private const int FileSignature = 0x04034b50; 168 | private const int DirectorySignature = 0x06054B50; 169 | private const int BufferSize = 16 * 1024; 170 | 171 | /// 172 | /// Occurs when a file or a directory is extracted from an archive. 173 | /// 174 | public event EventHandler ExtractProgress; 175 | 176 | /// 177 | /// Initializes a new instance of the class. 178 | /// 179 | /// Name of the file. 180 | public Unzip(string fileName) 181 | : this(File.OpenRead(fileName)) 182 | { 183 | } 184 | 185 | /// 186 | /// Initializes a new instance of the class. 187 | /// 188 | /// The stream. 189 | public Unzip(Stream stream) 190 | { 191 | Stream = stream; 192 | Reader = new BinaryReader(Stream); 193 | } 194 | 195 | private Stream Stream { get; set; } 196 | 197 | private BinaryReader Reader { get; set; } 198 | 199 | /// 200 | /// Performs application-defined tasks associated with 201 | /// freeing, releasing, or resetting unmanaged resources. 202 | /// 203 | public void Dispose() 204 | { 205 | if (Stream != null) 206 | { 207 | Stream.Dispose(); 208 | Stream = null; 209 | } 210 | 211 | if (Reader != null) 212 | { 213 | Reader.Close(); 214 | Reader = null; 215 | } 216 | } 217 | 218 | /// 219 | /// Extracts the contents of the zip file to the given directory. 220 | /// 221 | /// Name of the directory. 222 | public void ExtractToDirectory(string directoryName) 223 | { 224 | for (int index = 0; index < Entries.Length; index++) 225 | { 226 | var entry = Entries[index]; 227 | 228 | // create target directory for the file 229 | var fileName = Path.Combine(directoryName, entry.Name); 230 | var dirName = Path.GetDirectoryName(fileName); 231 | Directory.CreateDirectory(dirName); 232 | 233 | // save file if it is not only a directory 234 | if (!entry.IsDirectory) 235 | { 236 | Extract(entry.Name, fileName); 237 | } 238 | 239 | var extractProgress = ExtractProgress; 240 | if (extractProgress != null) 241 | { 242 | extractProgress(this, new FileProgressEventArgs(index + 1, Entries.Length, entry.Name)); 243 | } 244 | } 245 | } 246 | 247 | /// 248 | /// Extracts the specified file to the specified name. 249 | /// 250 | /// Name of the file in zip archive. 251 | /// Name of the output file. 252 | public void Extract(string fileName, string outputFileName) 253 | { 254 | var entry = GetEntry(fileName); 255 | 256 | using (var outStream = File.Create(outputFileName)) 257 | { 258 | Extract(entry, outStream); 259 | } 260 | 261 | var fileInfo = new FileInfo(outputFileName); 262 | if (fileInfo.Length != entry.OriginalSize) 263 | { 264 | throw new InvalidDataException(string.Format( 265 | "Corrupted archive: {0} has an uncompressed size {1} which does not match its expected size {2}", 266 | outputFileName, fileInfo.Length, entry.OriginalSize)); 267 | } 268 | 269 | File.SetLastWriteTime(outputFileName, entry.Timestamp); 270 | } 271 | 272 | private Entry GetEntry(string fileName) 273 | { 274 | fileName = fileName.Replace("\\", "/").Trim().TrimStart('/'); 275 | var entry = Entries.FirstOrDefault(e => e.Name == fileName); 276 | 277 | if (entry == null) 278 | { 279 | throw new FileNotFoundException("File not found in the archive: " + fileName); 280 | } 281 | 282 | return entry; 283 | } 284 | 285 | /// 286 | /// Extracts the specified file to the output . 287 | /// 288 | /// Name of the file in zip archive. 289 | /// The output stream. 290 | public void Extract(string fileName, Stream outputStream) 291 | { 292 | Extract(GetEntry(fileName), outputStream); 293 | } 294 | 295 | /// 296 | /// Extracts the specified entry. 297 | /// 298 | /// Zip file entry to extract. 299 | /// The stream to write the data to. 300 | /// is thrown when the file header signature doesn't match. 301 | public void Extract(Entry entry, Stream outputStream) 302 | { 303 | // check file signature 304 | Stream.Seek(entry.HeaderOffset, SeekOrigin.Begin); 305 | if (Reader.ReadInt32() != FileSignature) 306 | { 307 | throw new InvalidDataException("File signature doesn't match."); 308 | } 309 | 310 | // move to file data 311 | Stream.Seek(entry.DataOffset, SeekOrigin.Begin); 312 | var inputStream = Stream; 313 | if (entry.Deflated) 314 | { 315 | inputStream = new DeflateStream(Stream, CompressionMode.Decompress, true); 316 | } 317 | 318 | // allocate buffer, prepare for CRC32 calculation 319 | var count = entry.OriginalSize; 320 | var bufferSize = Math.Min(BufferSize, entry.OriginalSize); 321 | var buffer = new byte[bufferSize]; 322 | var crc32Calculator = new Crc32Calculator(); 323 | 324 | while (count > 0) 325 | { 326 | // decompress data 327 | var read = inputStream.Read(buffer, 0, bufferSize); 328 | if (read == 0) 329 | { 330 | break; 331 | } 332 | 333 | crc32Calculator.UpdateWithBlock(buffer, read); 334 | 335 | // copy to the output stream 336 | outputStream.Write(buffer, 0, read); 337 | count -= read; 338 | } 339 | 340 | if (crc32Calculator.Crc32 != entry.Crc32) 341 | { 342 | throw new InvalidDataException(string.Format( 343 | "Corrupted archive: CRC32 doesn't match on file {0}: expected {1:x8}, got {2:x8}.", 344 | entry.Name, entry.Crc32, crc32Calculator.Crc32)); 345 | } 346 | } 347 | 348 | /// 349 | /// Gets the file names. 350 | /// 351 | public IEnumerable FileNames 352 | { 353 | get 354 | { 355 | return Entries.Select(e => e.Name).Where(f => !f.EndsWith("/")).OrderBy(f => f); 356 | } 357 | } 358 | 359 | private Entry[] entries; 360 | 361 | /// 362 | /// Gets zip file entries. 363 | /// 364 | public Entry[] Entries 365 | { 366 | get 367 | { 368 | if (entries == null) 369 | { 370 | entries = ReadZipEntries().ToArray(); 371 | } 372 | 373 | return entries; 374 | } 375 | } 376 | 377 | private IEnumerable ReadZipEntries() 378 | { 379 | if (Stream.Length < 22) 380 | { 381 | yield break; 382 | } 383 | 384 | Stream.Seek(-22, SeekOrigin.End); 385 | 386 | // find directory signature 387 | while (Reader.ReadInt32() != DirectorySignature) 388 | { 389 | if (Stream.Position <= 5) 390 | { 391 | yield break; 392 | } 393 | 394 | // move 1 byte back 395 | Stream.Seek(-5, SeekOrigin.Current); 396 | } 397 | 398 | // read directory properties 399 | Stream.Seek(6, SeekOrigin.Current); 400 | var entries = Reader.ReadUInt16(); 401 | var difSize = Reader.ReadInt32(); 402 | var dirOffset = Reader.ReadUInt32(); 403 | Stream.Seek(dirOffset, SeekOrigin.Begin); 404 | 405 | // read directory entries 406 | for (int i = 0; i < entries; i++) 407 | { 408 | if (Reader.ReadInt32() != EntrySignature) 409 | { 410 | continue; 411 | } 412 | 413 | // read file properties 414 | // TODO: Replace with a proper class to make this method a lot shorter. 415 | Reader.ReadInt32(); 416 | bool utf8 = (Reader.ReadInt16() & 0x0800) != 0; 417 | short method = Reader.ReadInt16(); 418 | int timestamp = Reader.ReadInt32(); 419 | uint crc32 = Reader.ReadUInt32(); 420 | int compressedSize = Reader.ReadInt32(); 421 | int fileSize = Reader.ReadInt32(); 422 | short fileNameSize = Reader.ReadInt16(); 423 | short extraSize = Reader.ReadInt16(); 424 | short commentSize = Reader.ReadInt16(); 425 | int headerOffset = Reader.ReadInt32(); 426 | Reader.ReadInt32(); 427 | int fileHeaderOffset = Reader.ReadInt32(); 428 | var fileNameBytes = Reader.ReadBytes(fileNameSize); 429 | Stream.Seek(extraSize, SeekOrigin.Current); 430 | var fileCommentBytes = Reader.ReadBytes(commentSize); 431 | var fileDataOffset = CalculateFileDataOffset(fileHeaderOffset); 432 | 433 | // decode zip file entry 434 | var encoder = utf8 ? Encoding.UTF8 : Encoding.Default; 435 | yield return new Entry 436 | { 437 | Name = encoder.GetString(fileNameBytes), 438 | Comment = encoder.GetString(fileCommentBytes), 439 | Crc32 = crc32, 440 | CompressedSize = compressedSize, 441 | OriginalSize = fileSize, 442 | HeaderOffset = fileHeaderOffset, 443 | DataOffset = fileDataOffset, 444 | Deflated = method == 8, 445 | Timestamp = ConvertToDateTime(timestamp) 446 | }; 447 | } 448 | } 449 | 450 | private int CalculateFileDataOffset(int fileHeaderOffset) 451 | { 452 | var position = Stream.Position; 453 | Stream.Seek(fileHeaderOffset + 26, SeekOrigin.Begin); 454 | var fileNameSize = Reader.ReadInt16(); 455 | var extraSize = Reader.ReadInt16(); 456 | 457 | var fileOffset = (int)Stream.Position + fileNameSize + extraSize; 458 | Stream.Seek(position, SeekOrigin.Begin); 459 | return fileOffset; 460 | } 461 | 462 | /// 463 | /// Converts DOS timestamp to a instance. 464 | /// 465 | /// The dos timestamp. 466 | /// The instance. 467 | public static DateTime ConvertToDateTime(int dosTimestamp) 468 | { 469 | return new DateTime((dosTimestamp >> 25) + 1980, (dosTimestamp >> 21) & 15, (dosTimestamp >> 16) & 31, 470 | (dosTimestamp >> 11) & 31, (dosTimestamp >> 5) & 63, (dosTimestamp & 31) * 2); 471 | } 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /SongLoaderPlugin/SongLoader.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Linq; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using SimpleJSON; 9 | using SongLoaderPlugin.Internals; 10 | using SongLoaderPlugin.OverrideClasses; 11 | using UnityEngine.Networking; 12 | using UnityEngine.SceneManagement; 13 | 14 | namespace SongLoaderPlugin 15 | { 16 | public class SongLoader : MonoBehaviour 17 | { 18 | public static event Action LoadingStartedEvent; 19 | public static event Action> SongsLoadedEvent; 20 | public static List CustomLevels = new List(); 21 | public static bool AreSongsLoaded { get; private set; } 22 | public static bool AreSongsLoading { get; private set; } 23 | public static float LoadingProgress { get; private set; } 24 | 25 | public const int MenuIndex = 1; 26 | 27 | private static readonly Dictionary LoadedSprites = new Dictionary(); 28 | private static readonly Dictionary LoadedAudioClips = new Dictionary(); 29 | 30 | private LeaderboardScoreUploader _leaderboardScoreUploader; 31 | private MainFlowCoordinator _mainFlowCoordinator; 32 | private StandardLevelDetailViewController _standardLevelDetailViewController; 33 | 34 | private CustomLevelCollectionsForGameplayModes _customLevelCollectionsForGameplayModes; 35 | private CustomLevelCollectionSO _standardLevelCollection; 36 | private CustomLevelCollectionSO _oneSaberLevelCollection; 37 | private CustomLevelCollectionSO _noArrowsLevelCollection; 38 | private CustomLevelCollectionSO _partyLevelCollection; 39 | 40 | private readonly ScriptableObjectPool _customLevelPool = new ScriptableObjectPool(); 41 | private readonly ScriptableObjectPool _beatmapDataPool = new ScriptableObjectPool(); 42 | 43 | private ProgressBar _progressBar; 44 | 45 | private HMTask _loadingTask; 46 | private bool _loadingCancelled; 47 | 48 | public static readonly AudioClip TemporaryAudioClip = AudioClip.Create("temp", 1, 2, 1000, true); 49 | 50 | private LogSeverity _minLogSeverity; 51 | 52 | public static void OnLoad() 53 | { 54 | if (Instance != null) return; 55 | new GameObject("Song Loader").AddComponent(); 56 | } 57 | 58 | public static SongLoader Instance; 59 | 60 | private void Awake() 61 | { 62 | Instance = this; 63 | 64 | _minLogSeverity = Environment.CommandLine.Contains("--mute-song-loader") 65 | ? LogSeverity.Error 66 | : LogSeverity.Info; 67 | 68 | CreateCustomLevelCollections(); 69 | SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged; 70 | SceneManagerOnActiveSceneChanged(new Scene(), SceneManager.GetActiveScene()); 71 | _progressBar = ProgressBar.Create(); 72 | 73 | RefreshSongs(); 74 | 75 | DontDestroyOnLoad(gameObject); 76 | } 77 | 78 | private void SceneManagerOnActiveSceneChanged(Scene arg0, Scene scene) 79 | { 80 | if (AreSongsLoading) 81 | { 82 | //Scene changing while songs are loading. Since we are using a separate thread while loading, this is bad and could cause a crash. 83 | //So we have to stop loading. 84 | if (_loadingTask != null) 85 | { 86 | _loadingTask.Cancel(); 87 | _loadingCancelled = true; 88 | AreSongsLoading = false; 89 | LoadingProgress = 0; 90 | StopAllCoroutines(); 91 | _progressBar.ShowMessage("Loading cancelled\nPress Ctrl+R to refresh"); 92 | Log("Loading was cancelled by player since they loaded another scene."); 93 | } 94 | } 95 | 96 | StartCoroutine(WaitRemoveScores()); 97 | 98 | if (scene.buildIndex == 1) 99 | { 100 | _mainFlowCoordinator = Resources.FindObjectsOfTypeAll().FirstOrDefault(); 101 | _mainFlowCoordinator.SetPrivateField("_levelCollectionsForGameplayModes", _customLevelCollectionsForGameplayModes); 102 | 103 | _standardLevelDetailViewController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); 104 | if (_standardLevelDetailViewController == null) return; 105 | _standardLevelDetailViewController.didPressPlayButtonEvent += StandardLevelDetailControllerOnDidPressPlayButtonEvent; 106 | 107 | var standardLevelListViewController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); 108 | if (standardLevelListViewController == null) return; 109 | 110 | standardLevelListViewController.didSelectLevelEvent += StandardLevelListViewControllerOnDidSelectLevelEvent; 111 | } 112 | else if (scene.buildIndex == 5) 113 | { 114 | if (NoteHitVolumeChanger.PrefabFound) return; 115 | var mainGameData = Resources.FindObjectsOfTypeAll().FirstOrDefault(); 116 | if (mainGameData == null) return; 117 | var level = mainGameData.difficultyLevel.level; 118 | var song = CustomLevels.FirstOrDefault(x => x.levelID == level.levelID); 119 | if (song == null) return; 120 | NoteHitVolumeChanger.SetVolume(song.customSongInfo.noteHitVolume, song.customSongInfo.noteMissVolume); 121 | } 122 | } 123 | 124 | private void StandardLevelListViewControllerOnDidSelectLevelEvent(StandardLevelListViewController arg1, IStandardLevel level) 125 | { 126 | var customLevel = level as CustomLevel; 127 | if (customLevel == null) return; 128 | 129 | if (customLevel.audioClip != TemporaryAudioClip || customLevel.AudioClipLoading) return; 130 | 131 | var levels = arg1.GetPrivateField("_levels").ToList(); 132 | 133 | Action callback = delegate 134 | { 135 | arg1.SetPrivateField("_selectedLevel", null); 136 | arg1.HandleLevelSelectionDidChange(levels.IndexOf(customLevel), true); 137 | }; 138 | 139 | customLevel.FixBPMAndGetNoteJumpMovementSpeed(); 140 | StartCoroutine(LoadAudio( 141 | "file:///" + customLevel.customSongInfo.path + "/" + customLevel.customSongInfo.GetAudioPath(), customLevel, 142 | callback)); 143 | } 144 | 145 | public void LoadAudioClipForLevel(CustomLevel customLevel, Action clipReadyCallback) 146 | { 147 | Action callback = delegate { clipReadyCallback(customLevel); }; 148 | 149 | customLevel.FixBPMAndGetNoteJumpMovementSpeed(); 150 | StartCoroutine(LoadAudio( 151 | "file:///" + customLevel.customSongInfo.path + "/" + customLevel.customSongInfo.GetAudioPath(), customLevel, 152 | callback)); 153 | } 154 | 155 | private IEnumerator WaitRemoveScores() 156 | { 157 | yield return new WaitForSecondsRealtime(1f); 158 | RemoveCustomScores(); 159 | } 160 | 161 | private void StandardLevelDetailControllerOnDidPressPlayButtonEvent(StandardLevelDetailViewController songDetailViewController) 162 | { 163 | if (!NoteHitVolumeChanger.PrefabFound) return; 164 | var level = songDetailViewController.difficultyLevel.level; 165 | var song = CustomLevels.FirstOrDefault(x => x.levelID == level.levelID); 166 | if (song == null) return; 167 | NoteHitVolumeChanger.SetVolume(song.customSongInfo.noteHitVolume, song.customSongInfo.noteMissVolume); 168 | } 169 | 170 | private void CreateCustomLevelCollections() 171 | { 172 | var originalCollections = Resources.FindObjectsOfTypeAll().FirstOrDefault(); 173 | 174 | _standardLevelCollection = ScriptableObject.CreateInstance(); 175 | _standardLevelCollection.Init(originalCollections.GetLevels(GameplayMode.SoloStandard)); 176 | 177 | _oneSaberLevelCollection = ScriptableObject.CreateInstance(); 178 | _oneSaberLevelCollection.Init(originalCollections.GetLevels(GameplayMode.SoloOneSaber)); 179 | 180 | _noArrowsLevelCollection = ScriptableObject.CreateInstance(); 181 | _noArrowsLevelCollection.Init(originalCollections.GetLevels(GameplayMode.SoloNoArrows)); 182 | 183 | _partyLevelCollection = ScriptableObject.CreateInstance(); 184 | _partyLevelCollection.Init(originalCollections.GetLevels(GameplayMode.PartyStandard)); 185 | 186 | _customLevelCollectionsForGameplayModes = 187 | ScriptableObject.CreateInstance(); 188 | 189 | var standard = new CustomLevelCollectionForGameplayMode(GameplayMode.SoloStandard, _standardLevelCollection); 190 | var oneSaber = new CustomLevelCollectionForGameplayMode(GameplayMode.SoloOneSaber, _oneSaberLevelCollection); 191 | var noArrows = new CustomLevelCollectionForGameplayMode(GameplayMode.SoloNoArrows, _noArrowsLevelCollection); 192 | var party = new CustomLevelCollectionForGameplayMode(GameplayMode.PartyStandard, _partyLevelCollection); 193 | 194 | _customLevelCollectionsForGameplayModes.SetCollections( 195 | new LevelCollectionsForGameplayModes.LevelCollectionForGameplayMode[] 196 | {standard, oneSaber, noArrows, party}); 197 | } 198 | 199 | public void RefreshSongs(bool fullRefresh = true) 200 | { 201 | if (SceneManager.GetActiveScene().buildIndex != MenuIndex) return; 202 | Log(fullRefresh ? "Starting full song refresh" : "Starting song refresh"); 203 | AreSongsLoaded = false; 204 | AreSongsLoading = true; 205 | LoadingProgress = 0; 206 | _loadingCancelled = false; 207 | 208 | if (LoadingStartedEvent != null) 209 | { 210 | try 211 | { 212 | LoadingStartedEvent(this); 213 | } 214 | catch (Exception e) 215 | { 216 | Log("Some plugin is throwing exception from the LoadingStartedEvent!", LogSeverity.Error); 217 | Log(e.ToString(), LogSeverity.Error); 218 | } 219 | } 220 | 221 | foreach (var customLevel in CustomLevels) 222 | { 223 | _standardLevelCollection.LevelList.Remove(customLevel); 224 | _oneSaberLevelCollection.LevelList.Remove(customLevel); 225 | _noArrowsLevelCollection.LevelList.Remove(customLevel); 226 | _partyLevelCollection.LevelList.Remove(customLevel); 227 | } 228 | 229 | RetrieveAllSongs(fullRefresh); 230 | } 231 | 232 | //Use these methods if your own plugin deletes a song and you want the song loader to remove it from the list. 233 | //This is so you don't have to do a full refresh. 234 | public void RemoveSongWithPath(string path) 235 | { 236 | RemoveSong(CustomLevels.FirstOrDefault(x => x.customSongInfo.path == path)); 237 | } 238 | 239 | public void RemoveSongWithLevelID(string levelID) 240 | { 241 | RemoveSong(CustomLevels.FirstOrDefault(x => x.levelID == levelID)); 242 | } 243 | 244 | public void RemoveSong(IStandardLevel level) 245 | { 246 | if (level == null) return; 247 | RemoveSong(level as CustomLevel); 248 | } 249 | 250 | public void RemoveSong(CustomLevel customLevel) 251 | { 252 | if (customLevel == null) return; 253 | 254 | _standardLevelCollection.LevelList.Remove(customLevel); 255 | _oneSaberLevelCollection.LevelList.Remove(customLevel); 256 | _noArrowsLevelCollection.LevelList.Remove(customLevel); 257 | _partyLevelCollection.LevelList.Remove(customLevel); 258 | 259 | foreach (var difficultyBeatmap in customLevel.difficultyBeatmaps) 260 | { 261 | var customDifficulty = difficultyBeatmap as CustomLevel.CustomDifficultyBeatmap; 262 | if (customDifficulty == null) continue; 263 | _beatmapDataPool.Return(customDifficulty.BeatmapDataSO); 264 | } 265 | 266 | _customLevelPool.Return(customLevel); 267 | } 268 | 269 | private void RemoveCustomScores() 270 | { 271 | if (PlayerPrefs.HasKey("lbPatched")) return; 272 | _leaderboardScoreUploader = FindObjectOfType(); 273 | if (_leaderboardScoreUploader == null) return; 274 | var scores = 275 | _leaderboardScoreUploader.GetPrivateField>("_scoresToUploadForCurrentPlayer"); 276 | 277 | var scoresToRemove = new List(); 278 | foreach (var scoreData in scores) 279 | { 280 | var split = scoreData._leaderboardId.Split('_'); 281 | var levelID = split[0]; 282 | if (CustomLevels.Any(x => x.levelID == levelID)) 283 | { 284 | Log("Removing a custom score here"); 285 | scoresToRemove.Add(scoreData); 286 | } 287 | } 288 | 289 | scores.RemoveAll(x => scoresToRemove.Contains(x)); 290 | } 291 | 292 | private IEnumerator LoadSprite(string spritePath, CustomLevel customLevel) 293 | { 294 | Sprite sprite; 295 | if (!LoadedSprites.ContainsKey(spritePath)) 296 | { 297 | using (var web = UnityWebRequestTexture.GetTexture(EncodePath(spritePath), true)) 298 | { 299 | yield return web.SendWebRequest(); 300 | if (web.isNetworkError || web.isHttpError) 301 | { 302 | Log("Error loading: " + spritePath + ": " + web.error, LogSeverity.Warn); 303 | sprite = null; 304 | } 305 | else 306 | { 307 | var tex = DownloadHandlerTexture.GetContent(web); 308 | sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.one * 0.5f, 100, 1); 309 | LoadedSprites.Add(spritePath, sprite); 310 | } 311 | } 312 | } 313 | else 314 | { 315 | sprite = LoadedSprites[spritePath]; 316 | } 317 | 318 | customLevel.SetCoverImage(sprite); 319 | } 320 | 321 | private IEnumerator LoadAudio(string audioPath, CustomLevel customLevel, Action callback) 322 | { 323 | AudioClip audioClip; 324 | if (!LoadedAudioClips.ContainsKey(audioPath)) 325 | { 326 | using (var www = new WWW(EncodePath(audioPath))) 327 | { 328 | customLevel.AudioClipLoading = true; 329 | yield return www; 330 | 331 | audioClip = www.GetAudioClip(true, true, AudioType.UNKNOWN); 332 | 333 | var timeout = Time.realtimeSinceStartup + 5; 334 | while (audioClip.length == 0) 335 | { 336 | if (Time.realtimeSinceStartup > timeout) 337 | { 338 | Log("Audio clip: " + audioClip.name + " timed out...", LogSeverity.Warn); 339 | break; 340 | } 341 | 342 | yield return null; 343 | } 344 | 345 | LoadedAudioClips.Add(audioPath, audioClip); 346 | } 347 | } 348 | else 349 | { 350 | audioClip = LoadedAudioClips[audioPath]; 351 | } 352 | 353 | customLevel.SetAudioClip(audioClip); 354 | callback.Invoke(); 355 | customLevel.AudioClipLoading = false; 356 | } 357 | 358 | private void RetrieveAllSongs(bool fullRefresh) 359 | { 360 | var stopwatch = new Stopwatch(); 361 | var levelList = new List(); 362 | 363 | if (fullRefresh) 364 | { 365 | _customLevelPool.ReturnAll(); 366 | _beatmapDataPool.ReturnAll(); 367 | CustomLevels.Clear(); 368 | } 369 | 370 | Action job = delegate 371 | { 372 | try 373 | { 374 | stopwatch.Start(); 375 | var path = Environment.CurrentDirectory; 376 | path = path.Replace('\\', '/'); 377 | 378 | var currentHashes = new List(); 379 | var cachedSongs = new string[0]; 380 | if (Directory.Exists(path + "/CustomSongs/.cache")) 381 | { 382 | cachedSongs = Directory.GetDirectories(path + "/CustomSongs/.cache"); 383 | } 384 | else 385 | { 386 | Directory.CreateDirectory(path + "/CustomSongs/.cache"); 387 | } 388 | 389 | var songZips = Directory.GetFiles(path + "/CustomSongs") 390 | .Where(x => x.ToLower().EndsWith(".zip") || x.ToLower().EndsWith(".beat")).ToArray(); 391 | foreach (var songZip in songZips) 392 | { 393 | //Check cache if zip already is extracted 394 | string hash; 395 | if (Utils.CreateMD5FromFile(songZip, out hash)) 396 | { 397 | currentHashes.Add(hash); 398 | if (cachedSongs.Any(x => x.Contains(hash))) continue; 399 | 400 | using (var unzip = new Unzip(songZip)) 401 | { 402 | unzip.ExtractToDirectory(path + "/CustomSongs/.cache/" + hash); 403 | } 404 | } 405 | else 406 | { 407 | Log("Error reading zip " + songZip, LogSeverity.Warn); 408 | } 409 | } 410 | 411 | var songFolders = Directory.GetDirectories(path + "/CustomSongs").ToList(); 412 | var songCaches = Directory.GetDirectories(path + "/CustomSongs/.cache"); 413 | 414 | float i = 0; 415 | foreach (var song in songFolders) 416 | { 417 | i++; 418 | var results = Directory.GetFiles(song, "info.json", SearchOption.AllDirectories); 419 | if (results.Length == 0) 420 | { 421 | Log("Custom song folder '" + song + "' is missing info.json files!", LogSeverity.Warn); 422 | continue; 423 | } 424 | 425 | 426 | foreach (var result in results) 427 | { 428 | var songPath = Path.GetDirectoryName(result).Replace('\\', '/'); 429 | if (!fullRefresh) 430 | { 431 | if (CustomLevels.Any(x => x.customSongInfo.path == songPath)) 432 | { 433 | continue; 434 | } 435 | } 436 | 437 | var customSongInfo = GetCustomSongInfo(songPath); 438 | if (customSongInfo == null) continue; 439 | var id = customSongInfo.GetIdentifier(); 440 | if (CustomLevels.Any(x => x.levelID == id && x.customSongInfo != customSongInfo)) 441 | { 442 | Log("Duplicate song found at " + customSongInfo.path, LogSeverity.Warn); 443 | continue; 444 | } 445 | 446 | var i1 = i; 447 | HMMainThreadDispatcher.instance.Enqueue(delegate 448 | { 449 | if (_loadingCancelled) return; 450 | LoadSong(customSongInfo, levelList); 451 | LoadingProgress = i1 / songFolders.Count; 452 | }); 453 | } 454 | } 455 | 456 | foreach (var song in songCaches) 457 | { 458 | var hash = Path.GetFileName(song); 459 | if (!currentHashes.Contains(hash)) 460 | { 461 | //Old cache 462 | Directory.Delete(song, true); 463 | } 464 | } 465 | 466 | } 467 | catch (Exception e) 468 | { 469 | Log("RetrieveAllSongs failed:", LogSeverity.Error); 470 | Log(e.ToString(), LogSeverity.Error); 471 | throw; 472 | } 473 | }; 474 | 475 | Action finish = delegate 476 | { 477 | stopwatch.Stop(); 478 | Log("Loaded " + levelList.Count + " new songs in " + stopwatch.Elapsed.Seconds + " seconds"); 479 | 480 | CustomLevels.AddRange(levelList); 481 | var orderedList = CustomLevels.OrderBy(x => x.songName); 482 | CustomLevels = orderedList.ToList(); 483 | 484 | foreach (var customLevel in CustomLevels) 485 | { 486 | if (customLevel.customSongInfo.oneSaber) 487 | { 488 | _oneSaberLevelCollection.LevelList.Add(customLevel); 489 | } 490 | else 491 | { 492 | _standardLevelCollection.LevelList.Add(customLevel); 493 | _noArrowsLevelCollection.LevelList.Add(customLevel); 494 | _partyLevelCollection.LevelList.Add(customLevel); 495 | } 496 | } 497 | 498 | AreSongsLoaded = true; 499 | AreSongsLoading = false; 500 | LoadingProgress = 1; 501 | 502 | _loadingTask = null; 503 | 504 | if (SongsLoadedEvent != null) 505 | { 506 | SongsLoadedEvent(this, CustomLevels); 507 | } 508 | }; 509 | 510 | _loadingTask = new HMTask(job, finish); 511 | _loadingTask.Run(); 512 | } 513 | 514 | private void LoadSong(CustomSongInfo song, List levelList) 515 | { 516 | try 517 | { 518 | var newLevel = _customLevelPool.Get(); 519 | newLevel.Init(song); 520 | newLevel.SetAudioClip(TemporaryAudioClip); 521 | 522 | var difficultyBeatmaps = new List(); 523 | foreach (var diffBeatmap in song.difficultyLevels) 524 | { 525 | try 526 | { 527 | var difficulty = diffBeatmap.difficulty.ToEnum(LevelDifficulty.Normal); 528 | 529 | if (string.IsNullOrEmpty(diffBeatmap.json)) 530 | { 531 | Log("Couldn't find or parse difficulty json " + song.path + "/" + diffBeatmap.jsonPath, LogSeverity.Warn); 532 | continue; 533 | } 534 | 535 | var newBeatmapData = _beatmapDataPool.Get(); 536 | newBeatmapData.SetJsonData(diffBeatmap.json); 537 | 538 | var newDiffBeatmap = new CustomLevel.CustomDifficultyBeatmap(newLevel, difficulty, 539 | diffBeatmap.difficultyRank, diffBeatmap.noteJumpMovementSpeed, newBeatmapData); 540 | difficultyBeatmaps.Add(newDiffBeatmap); 541 | } 542 | catch (Exception e) 543 | { 544 | Log("Error parsing difficulty level in song: " + song.path, LogSeverity.Warn); 545 | Log(e.Message, LogSeverity.Warn); 546 | } 547 | } 548 | 549 | if (difficultyBeatmaps.Count == 0) return; 550 | 551 | newLevel.SetDifficultyBeatmaps(difficultyBeatmaps.ToArray()); 552 | newLevel.InitData(); 553 | 554 | StartCoroutine(LoadSprite("file:///" + song.path + "/" + song.coverImagePath, newLevel)); 555 | levelList.Add(newLevel); 556 | } 557 | catch (Exception e) 558 | { 559 | Log("Failed to load song: " + song.path, LogSeverity.Warn); 560 | Log(e.ToString(), LogSeverity.Warn); 561 | } 562 | } 563 | 564 | private CustomSongInfo GetCustomSongInfo(string songPath) 565 | { 566 | var infoText = File.ReadAllText(songPath + "/info.json"); 567 | CustomSongInfo songInfo; 568 | try 569 | { 570 | songInfo = JsonUtility.FromJson(infoText); 571 | } 572 | catch (Exception) 573 | { 574 | Log("Error parsing song: " + songPath, LogSeverity.Warn); 575 | return null; 576 | } 577 | 578 | songInfo.path = songPath; 579 | 580 | //Here comes SimpleJSON to the rescue when JSONUtility can't handle an array. 581 | var diffLevels = new List(); 582 | var n = JSON.Parse(infoText); 583 | var diffs = n["difficultyLevels"]; 584 | for (int i = 0; i < diffs.AsArray.Count; i++) 585 | { 586 | n = diffs[i]; 587 | var difficulty = Utils.ToEnum(n["difficulty"], LevelDifficulty.Normal); 588 | var difficultyRank = (int)difficulty; 589 | 590 | diffLevels.Add(new CustomSongInfo.DifficultyLevel 591 | { 592 | difficulty = n["difficulty"], 593 | difficultyRank = difficultyRank, 594 | audioPath = n["audioPath"], 595 | jsonPath = n["jsonPath"], 596 | noteJumpMovementSpeed = n["noteJumpMovementSpeed"] 597 | }); 598 | } 599 | 600 | songInfo.difficultyLevels = diffLevels.ToArray(); 601 | return songInfo; 602 | } 603 | 604 | private void Log(string message, LogSeverity severity = LogSeverity.Info) 605 | { 606 | if (severity < _minLogSeverity) return; 607 | Console.WriteLine("Song Loader [" + severity.ToString().ToUpper() + "]: " + message); 608 | } 609 | 610 | private void Update() 611 | { 612 | if (Input.GetKeyDown(KeyCode.R)) 613 | { 614 | RefreshSongs(Input.GetKey(KeyCode.LeftControl)); 615 | } 616 | } 617 | 618 | private static string EncodePath(string path) 619 | { 620 | path = Uri.EscapeDataString(path); 621 | path = path.Replace("%2F", "/"); //Forward slash gets encoded, but it shouldn't. 622 | path = path.Replace("%3A", ":"); //Same with semicolon. 623 | return path; 624 | } 625 | } 626 | } -------------------------------------------------------------------------------- /SongLoaderPlugin/SimpleJSON.cs: -------------------------------------------------------------------------------- 1 | /* * * * * 2 | * A simple JSON Parser / builder 3 | * ------------------------------ 4 | * 5 | * It mainly has been written as a simple JSON parser. It can build a JSON string 6 | * from the node-tree, or generate a node tree from any valid JSON string. 7 | * 8 | * If you want to use compression when saving to file / stream / B64 you have to include 9 | * SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) in your project and 10 | * define "USE_SharpZipLib" at the top of the file 11 | * 12 | * Written by Bunny83 13 | * 2012-06-09 14 | * 15 | * [2012-06-09 First Version] 16 | * - provides strongly typed node classes and lists / dictionaries 17 | * - provides easy access to class members / array items / data values 18 | * - the parser now properly identifies types. So generating JSON with this framework should work. 19 | * - only double quotes (") are used for quoting strings. 20 | * - provides "casting" properties to easily convert to / from those types: 21 | * int / float / double / bool 22 | * - provides a common interface for each node so no explicit casting is required. 23 | * - the parser tries to avoid errors, but if malformed JSON is parsed the result is more or less undefined 24 | * - It can serialize/deserialize a node tree into/from an experimental compact binary format. It might 25 | * be handy if you want to store things in a file and don't want it to be easily modifiable 26 | * 27 | * 28 | * [2012-12-17 Update] 29 | * - Added internal JSONLazyCreator class which simplifies the construction of a JSON tree 30 | * Now you can simple reference any item that doesn't exist yet and it will return a JSONLazyCreator 31 | * The class determines the required type by it's further use, creates the type and removes itself. 32 | * - Added binary serialization / deserialization. 33 | * - Added support for BZip2 zipped binary format. Requires the SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) 34 | * The usage of the SharpZipLib library can be disabled by removing or commenting out the USE_SharpZipLib define at the top 35 | * - The serializer uses different types when it comes to store the values. Since my data values 36 | * are all of type string, the serializer will "try" which format fits best. The order is: int, float, double, bool, string. 37 | * It's not the most efficient way but for a moderate amount of data it should work on all platforms. 38 | * 39 | * [2017-03-08 Update] 40 | * - Optimised parsing by using a StringBuilder for token. This prevents performance issues when large 41 | * string data fields are contained in the json data. 42 | * - Finally refactored the badly named JSONClass into JSONObject. 43 | * - Replaced the old JSONData class by distict typed classes ( JSONString, JSONNumber, JSONBool, JSONNull ) this 44 | * allows to propertly convert the node tree back to json without type information loss. The actual value 45 | * parsing now happens at parsing time and not when you actually access one of the casting properties. 46 | * 47 | * [2017-04-11 Update] 48 | * - Fixed parsing bug where empty string values have been ignored. 49 | * - Optimised "ToString" by using a StringBuilder internally. This should heavily improve performance for large files 50 | * - Changed the overload of "ToString(string aIndent)" to "ToString(int aIndent)" 51 | * 52 | * [2017-11-29 Update] 53 | * - Removed the IEnumerator implementations on JSONArray & JSONObject and replaced it with a common 54 | * struct Enumerator in JSONNode that should avoid garbage generation. The enumerator always works 55 | * on KeyValuePair, even for JSONArray. 56 | * - Added two wrapper Enumerators that allows for easy key or value enumeration. A JSONNode now has 57 | * a "Keys" and a "Values" enumerable property. Those are also struct enumerators / enumerables 58 | * - A KeyValuePair can now be implicitly converted into a JSONNode. This allows 59 | * a foreach loop over a JSONNode to directly access the values only. Since KeyValuePair as well as 60 | * all the Enumerators are structs, no garbage is allocated. 61 | * - To add Linq support another "LinqEnumerator" is available through the "Linq" property. This 62 | * enumerator does implement the generic IEnumerable interface so most Linq extensions can be used 63 | * on this enumerable object. This one does allocate memory as it's a wrapper class. 64 | * - The Escape method now escapes all control characters (# < 32) in strings as uncode characters 65 | * (\uXXXX) and if the static bool JSONNode.forceASCII is set to true it will also escape all 66 | * characters # > 127. This might be useful if you require an ASCII output. Though keep in mind 67 | * when your strings contain many non-ascii characters the strings become much longer (x6) and are 68 | * no longer human readable. 69 | * - The node types JSONObject and JSONArray now have an "Inline" boolean switch which will default to 70 | * false. It can be used to serialize this element inline even you serialize with an indented format 71 | * This is useful for arrays containing numbers so it doesn't place every number on a new line 72 | * - Extracted the binary serialization code into a seperate extension file. All classes are now declared 73 | * as "partial" so an extension file can even add a new virtual or abstract method / interface to 74 | * JSONNode and override it in the concrete type classes. It's of course a hacky approach which is 75 | * generally not recommended, but i wanted to keep everything tightly packed. 76 | * - Added a static CreateOrGet method to the JSONNull class. Since this class is immutable it could 77 | * be reused without major problems. If you have a lot null fields in your data it will help reduce 78 | * the memory / garbage overhead. I also added a static setting (reuseSameInstance) to JSONNull 79 | * (default is true) which will change the behaviour of "CreateOrGet". If you set this to false 80 | * CreateOrGet will not reuse the cached instance but instead create a new JSONNull instance each time. 81 | * I made the JSONNull constructor private so if you need to create an instance manually use 82 | * JSONNull.CreateOrGet() 83 | * 84 | * 85 | * The MIT License (MIT) 86 | * 87 | * Copyright (c) 2012-2017 Markus Göbel (Bunny83) 88 | * 89 | * Permission is hereby granted, free of charge, to any person obtaining a copy 90 | * of this software and associated documentation files (the "Software"), to deal 91 | * in the Software without restriction, including without limitation the rights 92 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 93 | * copies of the Software, and to permit persons to whom the Software is 94 | * furnished to do so, subject to the following conditions: 95 | * 96 | * The above copyright notice and this permission notice shall be included in all 97 | * copies or substantial portions of the Software. 98 | * 99 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 100 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 101 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 102 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 103 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 104 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 105 | * SOFTWARE. 106 | * 107 | * * * * */ 108 | using System; 109 | using System.Collections; 110 | using System.Collections.Generic; 111 | using System.Linq; 112 | using System.Text; 113 | 114 | namespace SimpleJSON 115 | { 116 | public enum JSONNodeType 117 | { 118 | Array = 1, 119 | Object = 2, 120 | String = 3, 121 | Number = 4, 122 | NullValue = 5, 123 | Boolean = 6, 124 | None = 7, 125 | Custom = 0xFF, 126 | } 127 | public enum JSONTextMode 128 | { 129 | Compact, 130 | Indent 131 | } 132 | 133 | public abstract partial class JSONNode 134 | { 135 | #region Enumerators 136 | public struct Enumerator 137 | { 138 | private enum Type { None, Array, Object } 139 | private Type type; 140 | private Dictionary.Enumerator m_Object; 141 | private List.Enumerator m_Array; 142 | public bool IsValid { get { return type != Type.None; } } 143 | public Enumerator(List.Enumerator aArrayEnum) 144 | { 145 | type = Type.Array; 146 | m_Object = default(Dictionary.Enumerator); 147 | m_Array = aArrayEnum; 148 | } 149 | public Enumerator(Dictionary.Enumerator aDictEnum) 150 | { 151 | type = Type.Object; 152 | m_Object = aDictEnum; 153 | m_Array = default(List.Enumerator); 154 | } 155 | public KeyValuePair Current 156 | { 157 | get 158 | { 159 | if (type == Type.Array) 160 | return new KeyValuePair(string.Empty, m_Array.Current); 161 | else if (type == Type.Object) 162 | return m_Object.Current; 163 | return new KeyValuePair(string.Empty, null); 164 | } 165 | } 166 | public bool MoveNext() 167 | { 168 | if (type == Type.Array) 169 | return m_Array.MoveNext(); 170 | else if (type == Type.Object) 171 | return m_Object.MoveNext(); 172 | return false; 173 | } 174 | } 175 | public struct ValueEnumerator 176 | { 177 | private Enumerator m_Enumerator; 178 | public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } 179 | public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } 180 | public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } 181 | public JSONNode Current { get { return m_Enumerator.Current.Value; } } 182 | public bool MoveNext() { return m_Enumerator.MoveNext(); } 183 | public ValueEnumerator GetEnumerator() { return this; } 184 | } 185 | public struct KeyEnumerator 186 | { 187 | private Enumerator m_Enumerator; 188 | public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } 189 | public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } 190 | public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } 191 | public JSONNode Current { get { return m_Enumerator.Current.Key; } } 192 | public bool MoveNext() { return m_Enumerator.MoveNext(); } 193 | public KeyEnumerator GetEnumerator() { return this; } 194 | } 195 | 196 | public class LinqEnumerator : IEnumerator>, IEnumerable> 197 | { 198 | private JSONNode m_Node; 199 | private Enumerator m_Enumerator; 200 | internal LinqEnumerator(JSONNode aNode) 201 | { 202 | m_Node = aNode; 203 | if (m_Node != null) 204 | m_Enumerator = m_Node.GetEnumerator(); 205 | } 206 | public KeyValuePair Current { get { return m_Enumerator.Current; } } 207 | object IEnumerator.Current { get { return m_Enumerator.Current; } } 208 | public bool MoveNext() { return m_Enumerator.MoveNext(); } 209 | 210 | public void Dispose() 211 | { 212 | m_Node = null; 213 | m_Enumerator = new Enumerator(); 214 | } 215 | 216 | public IEnumerator> GetEnumerator() 217 | { 218 | return new LinqEnumerator(m_Node); 219 | } 220 | 221 | public void Reset() 222 | { 223 | if (m_Node != null) 224 | m_Enumerator = m_Node.GetEnumerator(); 225 | } 226 | 227 | IEnumerator IEnumerable.GetEnumerator() 228 | { 229 | return new LinqEnumerator(m_Node); 230 | } 231 | } 232 | 233 | #endregion Enumerators 234 | 235 | #region common interface 236 | 237 | public static bool forceASCII = false; // Use Unicode by default 238 | 239 | public abstract JSONNodeType Tag { get; } 240 | 241 | public virtual JSONNode this[int aIndex] { get { return null; } set { } } 242 | 243 | public virtual JSONNode this[string aKey] { get { return null; } set { } } 244 | 245 | public virtual string Value { get { return ""; } set { } } 246 | 247 | public virtual int Count { get { return 0; } } 248 | 249 | public virtual bool IsNumber { get { return false; } } 250 | public virtual bool IsString { get { return false; } } 251 | public virtual bool IsBoolean { get { return false; } } 252 | public virtual bool IsNull { get { return false; } } 253 | public virtual bool IsArray { get { return false; } } 254 | public virtual bool IsObject { get { return false; } } 255 | 256 | public virtual bool Inline { get { return false; } set { } } 257 | 258 | public virtual void Add(string aKey, JSONNode aItem) 259 | { 260 | } 261 | public virtual void Add(JSONNode aItem) 262 | { 263 | Add("", aItem); 264 | } 265 | 266 | public virtual JSONNode Remove(string aKey) 267 | { 268 | return null; 269 | } 270 | 271 | public virtual JSONNode Remove(int aIndex) 272 | { 273 | return null; 274 | } 275 | 276 | public virtual JSONNode Remove(JSONNode aNode) 277 | { 278 | return aNode; 279 | } 280 | 281 | public virtual IEnumerable Children 282 | { 283 | get 284 | { 285 | yield break; 286 | } 287 | } 288 | 289 | public IEnumerable DeepChildren 290 | { 291 | get 292 | { 293 | foreach (var C in Children) 294 | foreach (var D in C.DeepChildren) 295 | yield return D; 296 | } 297 | } 298 | 299 | public override string ToString() 300 | { 301 | StringBuilder sb = new StringBuilder(); 302 | WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact); 303 | return sb.ToString(); 304 | } 305 | 306 | public virtual string ToString(int aIndent) 307 | { 308 | StringBuilder sb = new StringBuilder(); 309 | WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent); 310 | return sb.ToString(); 311 | } 312 | internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode); 313 | 314 | public abstract Enumerator GetEnumerator(); 315 | public IEnumerable> Linq { get { return new LinqEnumerator(this); } } 316 | public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } } 317 | public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } } 318 | 319 | #endregion common interface 320 | 321 | #region typecasting properties 322 | 323 | 324 | public virtual double AsDouble 325 | { 326 | get 327 | { 328 | double v = 0.0; 329 | if (double.TryParse(Value, out v)) 330 | return v; 331 | return 0.0; 332 | } 333 | set 334 | { 335 | Value = value.ToString(); 336 | } 337 | } 338 | 339 | public virtual int AsInt 340 | { 341 | get { return (int)AsDouble; } 342 | set { AsDouble = value; } 343 | } 344 | 345 | public virtual float AsFloat 346 | { 347 | get { return (float)AsDouble; } 348 | set { AsDouble = value; } 349 | } 350 | 351 | public virtual bool AsBool 352 | { 353 | get 354 | { 355 | bool v = false; 356 | if (bool.TryParse(Value, out v)) 357 | return v; 358 | return !string.IsNullOrEmpty(Value); 359 | } 360 | set 361 | { 362 | Value = (value) ? "true" : "false"; 363 | } 364 | } 365 | 366 | public virtual JSONArray AsArray 367 | { 368 | get 369 | { 370 | return this as JSONArray; 371 | } 372 | } 373 | 374 | public virtual JSONObject AsObject 375 | { 376 | get 377 | { 378 | return this as JSONObject; 379 | } 380 | } 381 | 382 | 383 | #endregion typecasting properties 384 | 385 | #region operators 386 | 387 | public static implicit operator JSONNode(string s) 388 | { 389 | return new JSONString(s); 390 | } 391 | public static implicit operator string(JSONNode d) 392 | { 393 | return (d == null) ? null : d.Value; 394 | } 395 | 396 | public static implicit operator JSONNode(double n) 397 | { 398 | return new JSONNumber(n); 399 | } 400 | public static implicit operator double(JSONNode d) 401 | { 402 | return (d == null) ? 0 : d.AsDouble; 403 | } 404 | 405 | public static implicit operator JSONNode(float n) 406 | { 407 | return new JSONNumber(n); 408 | } 409 | public static implicit operator float(JSONNode d) 410 | { 411 | return (d == null) ? 0 : d.AsFloat; 412 | } 413 | 414 | public static implicit operator JSONNode(int n) 415 | { 416 | return new JSONNumber(n); 417 | } 418 | public static implicit operator int(JSONNode d) 419 | { 420 | return (d == null) ? 0 : d.AsInt; 421 | } 422 | 423 | public static implicit operator JSONNode(bool b) 424 | { 425 | return new JSONBool(b); 426 | } 427 | public static implicit operator bool(JSONNode d) 428 | { 429 | return (d == null) ? false : d.AsBool; 430 | } 431 | 432 | public static implicit operator JSONNode(KeyValuePair aKeyValue) 433 | { 434 | return aKeyValue.Value; 435 | } 436 | 437 | public static bool operator ==(JSONNode a, object b) 438 | { 439 | if (ReferenceEquals(a, b)) 440 | return true; 441 | bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator; 442 | bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator; 443 | if (aIsNull && bIsNull) 444 | return true; 445 | return !aIsNull && a.Equals(b); 446 | } 447 | 448 | public static bool operator !=(JSONNode a, object b) 449 | { 450 | return !(a == b); 451 | } 452 | 453 | public override bool Equals(object obj) 454 | { 455 | return ReferenceEquals(this, obj); 456 | } 457 | 458 | public override int GetHashCode() 459 | { 460 | return base.GetHashCode(); 461 | } 462 | 463 | #endregion operators 464 | 465 | [ThreadStatic] 466 | private static StringBuilder m_EscapeBuilder; 467 | internal static StringBuilder EscapeBuilder 468 | { 469 | get 470 | { 471 | if (m_EscapeBuilder == null) 472 | m_EscapeBuilder = new StringBuilder(); 473 | return m_EscapeBuilder; 474 | } 475 | } 476 | internal static string Escape(string aText) 477 | { 478 | var sb = EscapeBuilder; 479 | sb.Length = 0; 480 | if (sb.Capacity < aText.Length + aText.Length / 10) 481 | sb.Capacity = aText.Length + aText.Length / 10; 482 | foreach (char c in aText) 483 | { 484 | switch (c) 485 | { 486 | case '\\': 487 | sb.Append("\\\\"); 488 | break; 489 | case '\"': 490 | sb.Append("\\\""); 491 | break; 492 | case '\n': 493 | sb.Append("\\n"); 494 | break; 495 | case '\r': 496 | sb.Append("\\r"); 497 | break; 498 | case '\t': 499 | sb.Append("\\t"); 500 | break; 501 | case '\b': 502 | sb.Append("\\b"); 503 | break; 504 | case '\f': 505 | sb.Append("\\f"); 506 | break; 507 | default: 508 | if (c < ' ' || (forceASCII && c > 127)) 509 | { 510 | ushort val = c; 511 | sb.Append("\\u").Append(val.ToString("X4")); 512 | } 513 | else 514 | sb.Append(c); 515 | break; 516 | } 517 | } 518 | string result = sb.ToString(); 519 | sb.Length = 0; 520 | return result; 521 | } 522 | 523 | static void ParseElement(JSONNode ctx, string token, string tokenName, bool quoted) 524 | { 525 | if (quoted) 526 | { 527 | ctx.Add(tokenName, token); 528 | return; 529 | } 530 | string tmp = token.ToLower(); 531 | if (tmp == "false" || tmp == "true") 532 | ctx.Add(tokenName, tmp == "true"); 533 | else if (tmp == "null") 534 | ctx.Add(tokenName, null); 535 | else 536 | { 537 | double val; 538 | if (double.TryParse(token, out val)) 539 | ctx.Add(tokenName, val); 540 | else 541 | ctx.Add(tokenName, token); 542 | } 543 | } 544 | 545 | public static JSONNode Parse(string aJSON) 546 | { 547 | Stack stack = new Stack(); 548 | JSONNode ctx = null; 549 | int i = 0; 550 | StringBuilder Token = new StringBuilder(); 551 | string TokenName = ""; 552 | bool QuoteMode = false; 553 | bool TokenIsQuoted = false; 554 | while (i < aJSON.Length) 555 | { 556 | switch (aJSON[i]) 557 | { 558 | case '{': 559 | if (QuoteMode) 560 | { 561 | Token.Append(aJSON[i]); 562 | break; 563 | } 564 | stack.Push(new JSONObject()); 565 | if (ctx != null) 566 | { 567 | ctx.Add(TokenName, stack.Peek()); 568 | } 569 | TokenName = ""; 570 | Token.Length = 0; 571 | ctx = stack.Peek(); 572 | break; 573 | 574 | case '[': 575 | if (QuoteMode) 576 | { 577 | Token.Append(aJSON[i]); 578 | break; 579 | } 580 | 581 | stack.Push(new JSONArray()); 582 | if (ctx != null) 583 | { 584 | ctx.Add(TokenName, stack.Peek()); 585 | } 586 | TokenName = ""; 587 | Token.Length = 0; 588 | ctx = stack.Peek(); 589 | break; 590 | 591 | case '}': 592 | case ']': 593 | if (QuoteMode) 594 | { 595 | 596 | Token.Append(aJSON[i]); 597 | break; 598 | } 599 | if (stack.Count == 0) 600 | throw new Exception("JSON Parse: Too many closing brackets"); 601 | 602 | stack.Pop(); 603 | if (Token.Length > 0 || TokenIsQuoted) 604 | { 605 | ParseElement(ctx, Token.ToString(), TokenName, TokenIsQuoted); 606 | TokenIsQuoted = false; 607 | } 608 | TokenName = ""; 609 | Token.Length = 0; 610 | if (stack.Count > 0) 611 | ctx = stack.Peek(); 612 | break; 613 | 614 | case ':': 615 | if (QuoteMode) 616 | { 617 | Token.Append(aJSON[i]); 618 | break; 619 | } 620 | TokenName = Token.ToString(); 621 | Token.Length = 0; 622 | TokenIsQuoted = false; 623 | break; 624 | 625 | case '"': 626 | QuoteMode ^= true; 627 | TokenIsQuoted |= QuoteMode; 628 | break; 629 | 630 | case ',': 631 | if (QuoteMode) 632 | { 633 | Token.Append(aJSON[i]); 634 | break; 635 | } 636 | if (Token.Length > 0 || TokenIsQuoted) 637 | { 638 | ParseElement(ctx, Token.ToString(), TokenName, TokenIsQuoted); 639 | TokenIsQuoted = false; 640 | } 641 | TokenName = ""; 642 | Token.Length = 0; 643 | TokenIsQuoted = false; 644 | break; 645 | 646 | case '\r': 647 | case '\n': 648 | break; 649 | 650 | case ' ': 651 | case '\t': 652 | if (QuoteMode) 653 | Token.Append(aJSON[i]); 654 | break; 655 | 656 | case '\\': 657 | ++i; 658 | if (QuoteMode) 659 | { 660 | char C = aJSON[i]; 661 | switch (C) 662 | { 663 | case 't': 664 | Token.Append('\t'); 665 | break; 666 | case 'r': 667 | Token.Append('\r'); 668 | break; 669 | case 'n': 670 | Token.Append('\n'); 671 | break; 672 | case 'b': 673 | Token.Append('\b'); 674 | break; 675 | case 'f': 676 | Token.Append('\f'); 677 | break; 678 | case 'u': 679 | { 680 | string s = aJSON.Substring(i + 1, 4); 681 | Token.Append((char)int.Parse( 682 | s, 683 | System.Globalization.NumberStyles.AllowHexSpecifier)); 684 | i += 4; 685 | break; 686 | } 687 | default: 688 | Token.Append(C); 689 | break; 690 | } 691 | } 692 | break; 693 | 694 | default: 695 | Token.Append(aJSON[i]); 696 | break; 697 | } 698 | ++i; 699 | } 700 | if (QuoteMode) 701 | { 702 | throw new Exception("JSON Parse: Quotation marks seems to be messed up."); 703 | } 704 | return ctx; 705 | } 706 | 707 | } 708 | // End of JSONNode 709 | 710 | public partial class JSONArray : JSONNode 711 | { 712 | private List m_List = new List(); 713 | private bool inline = false; 714 | public override bool Inline 715 | { 716 | get { return inline; } 717 | set { inline = value; } 718 | } 719 | 720 | public override JSONNodeType Tag { get { return JSONNodeType.Array; } } 721 | public override bool IsArray { get { return true; } } 722 | public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); } 723 | 724 | public override JSONNode this[int aIndex] 725 | { 726 | get 727 | { 728 | if (aIndex < 0 || aIndex >= m_List.Count) 729 | return new JSONLazyCreator(this); 730 | return m_List[aIndex]; 731 | } 732 | set 733 | { 734 | if (value == null) 735 | value = JSONNull.CreateOrGet(); 736 | if (aIndex < 0 || aIndex >= m_List.Count) 737 | m_List.Add(value); 738 | else 739 | m_List[aIndex] = value; 740 | } 741 | } 742 | 743 | public override JSONNode this[string aKey] 744 | { 745 | get { return new JSONLazyCreator(this); } 746 | set 747 | { 748 | if (value == null) 749 | value = JSONNull.CreateOrGet(); 750 | m_List.Add(value); 751 | } 752 | } 753 | 754 | public override int Count 755 | { 756 | get { return m_List.Count; } 757 | } 758 | 759 | public override void Add(string aKey, JSONNode aItem) 760 | { 761 | if (aItem == null) 762 | aItem = JSONNull.CreateOrGet(); 763 | m_List.Add(aItem); 764 | } 765 | 766 | public override JSONNode Remove(int aIndex) 767 | { 768 | if (aIndex < 0 || aIndex >= m_List.Count) 769 | return null; 770 | JSONNode tmp = m_List[aIndex]; 771 | m_List.RemoveAt(aIndex); 772 | return tmp; 773 | } 774 | 775 | public override JSONNode Remove(JSONNode aNode) 776 | { 777 | m_List.Remove(aNode); 778 | return aNode; 779 | } 780 | 781 | public override IEnumerable Children 782 | { 783 | get 784 | { 785 | foreach (JSONNode N in m_List) 786 | yield return N; 787 | } 788 | } 789 | 790 | 791 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 792 | { 793 | aSB.Append('['); 794 | int count = m_List.Count; 795 | if (inline) 796 | aMode = JSONTextMode.Compact; 797 | for (int i = 0; i < count; i++) 798 | { 799 | if (i > 0) 800 | aSB.Append(','); 801 | if (aMode == JSONTextMode.Indent) 802 | aSB.AppendLine(); 803 | 804 | if (aMode == JSONTextMode.Indent) 805 | aSB.Append(' ', aIndent + aIndentInc); 806 | m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); 807 | } 808 | if (aMode == JSONTextMode.Indent) 809 | aSB.AppendLine().Append(' ', aIndent); 810 | aSB.Append(']'); 811 | } 812 | } 813 | // End of JSONArray 814 | 815 | public partial class JSONObject : JSONNode 816 | { 817 | private Dictionary m_Dict = new Dictionary(); 818 | 819 | private bool inline = false; 820 | public override bool Inline 821 | { 822 | get { return inline; } 823 | set { inline = value; } 824 | } 825 | 826 | public override JSONNodeType Tag { get { return JSONNodeType.Object; } } 827 | public override bool IsObject { get { return true; } } 828 | 829 | public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); } 830 | 831 | 832 | public override JSONNode this[string aKey] 833 | { 834 | get 835 | { 836 | if (m_Dict.ContainsKey(aKey)) 837 | return m_Dict[aKey]; 838 | else 839 | return new JSONLazyCreator(this, aKey); 840 | } 841 | set 842 | { 843 | if (value == null) 844 | value = JSONNull.CreateOrGet(); 845 | if (m_Dict.ContainsKey(aKey)) 846 | m_Dict[aKey] = value; 847 | else 848 | m_Dict.Add(aKey, value); 849 | } 850 | } 851 | 852 | public override JSONNode this[int aIndex] 853 | { 854 | get 855 | { 856 | if (aIndex < 0 || aIndex >= m_Dict.Count) 857 | return null; 858 | return m_Dict.ElementAt(aIndex).Value; 859 | } 860 | set 861 | { 862 | if (value == null) 863 | value = JSONNull.CreateOrGet(); 864 | if (aIndex < 0 || aIndex >= m_Dict.Count) 865 | return; 866 | string key = m_Dict.ElementAt(aIndex).Key; 867 | m_Dict[key] = value; 868 | } 869 | } 870 | 871 | public override int Count 872 | { 873 | get { return m_Dict.Count; } 874 | } 875 | 876 | public override void Add(string aKey, JSONNode aItem) 877 | { 878 | if (aItem == null) 879 | aItem = JSONNull.CreateOrGet(); 880 | 881 | if (!string.IsNullOrEmpty(aKey)) 882 | { 883 | if (m_Dict.ContainsKey(aKey)) 884 | m_Dict[aKey] = aItem; 885 | else 886 | m_Dict.Add(aKey, aItem); 887 | } 888 | else 889 | m_Dict.Add(Guid.NewGuid().ToString(), aItem); 890 | } 891 | 892 | public override JSONNode Remove(string aKey) 893 | { 894 | if (!m_Dict.ContainsKey(aKey)) 895 | return null; 896 | JSONNode tmp = m_Dict[aKey]; 897 | m_Dict.Remove(aKey); 898 | return tmp; 899 | } 900 | 901 | public override JSONNode Remove(int aIndex) 902 | { 903 | if (aIndex < 0 || aIndex >= m_Dict.Count) 904 | return null; 905 | var item = m_Dict.ElementAt(aIndex); 906 | m_Dict.Remove(item.Key); 907 | return item.Value; 908 | } 909 | 910 | public override JSONNode Remove(JSONNode aNode) 911 | { 912 | try 913 | { 914 | var item = m_Dict.Where(k => k.Value == aNode).First(); 915 | m_Dict.Remove(item.Key); 916 | return aNode; 917 | } 918 | catch 919 | { 920 | return null; 921 | } 922 | } 923 | 924 | public override IEnumerable Children 925 | { 926 | get 927 | { 928 | foreach (KeyValuePair N in m_Dict) 929 | yield return N.Value; 930 | } 931 | } 932 | 933 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 934 | { 935 | aSB.Append('{'); 936 | bool first = true; 937 | if (inline) 938 | aMode = JSONTextMode.Compact; 939 | foreach (var k in m_Dict) 940 | { 941 | if (!first) 942 | aSB.Append(','); 943 | first = false; 944 | if (aMode == JSONTextMode.Indent) 945 | aSB.AppendLine(); 946 | if (aMode == JSONTextMode.Indent) 947 | aSB.Append(' ', aIndent + aIndentInc); 948 | aSB.Append('\"').Append(Escape(k.Key)).Append('\"'); 949 | if (aMode == JSONTextMode.Compact) 950 | aSB.Append(':'); 951 | else 952 | aSB.Append(" : "); 953 | k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); 954 | } 955 | if (aMode == JSONTextMode.Indent) 956 | aSB.AppendLine().Append(' ', aIndent); 957 | aSB.Append('}'); 958 | } 959 | 960 | } 961 | // End of JSONObject 962 | 963 | public partial class JSONString : JSONNode 964 | { 965 | private string m_Data; 966 | 967 | public override JSONNodeType Tag { get { return JSONNodeType.String; } } 968 | public override bool IsString { get { return true; } } 969 | 970 | public override Enumerator GetEnumerator() { return new Enumerator(); } 971 | 972 | 973 | public override string Value 974 | { 975 | get { return m_Data; } 976 | set 977 | { 978 | m_Data = value; 979 | } 980 | } 981 | 982 | public JSONString(string aData) 983 | { 984 | m_Data = aData; 985 | } 986 | 987 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 988 | { 989 | aSB.Append('\"').Append(Escape(m_Data)).Append('\"'); 990 | } 991 | public override bool Equals(object obj) 992 | { 993 | if (base.Equals(obj)) 994 | return true; 995 | string s = obj as string; 996 | if (s != null) 997 | return m_Data == s; 998 | JSONString s2 = obj as JSONString; 999 | if (s2 != null) 1000 | return m_Data == s2.m_Data; 1001 | return false; 1002 | } 1003 | public override int GetHashCode() 1004 | { 1005 | return m_Data.GetHashCode(); 1006 | } 1007 | } 1008 | // End of JSONString 1009 | 1010 | public partial class JSONNumber : JSONNode 1011 | { 1012 | private double m_Data; 1013 | 1014 | public override JSONNodeType Tag { get { return JSONNodeType.Number; } } 1015 | public override bool IsNumber { get { return true; } } 1016 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1017 | 1018 | public override string Value 1019 | { 1020 | get { return m_Data.ToString(); } 1021 | set 1022 | { 1023 | double v; 1024 | if (double.TryParse(value, out v)) 1025 | m_Data = v; 1026 | } 1027 | } 1028 | 1029 | public override double AsDouble 1030 | { 1031 | get { return m_Data; } 1032 | set { m_Data = value; } 1033 | } 1034 | 1035 | public JSONNumber(double aData) 1036 | { 1037 | m_Data = aData; 1038 | } 1039 | 1040 | public JSONNumber(string aData) 1041 | { 1042 | Value = aData; 1043 | } 1044 | 1045 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1046 | { 1047 | aSB.Append(m_Data); 1048 | } 1049 | private static bool IsNumeric(object value) 1050 | { 1051 | return value is int || value is uint 1052 | || value is float || value is double 1053 | || value is decimal 1054 | || value is long || value is ulong 1055 | || value is short || value is ushort 1056 | || value is sbyte || value is byte; 1057 | } 1058 | public override bool Equals(object obj) 1059 | { 1060 | if (obj == null) 1061 | return false; 1062 | if (base.Equals(obj)) 1063 | return true; 1064 | JSONNumber s2 = obj as JSONNumber; 1065 | if (s2 != null) 1066 | return m_Data == s2.m_Data; 1067 | if (IsNumeric(obj)) 1068 | return Convert.ToDouble(obj) == m_Data; 1069 | return false; 1070 | } 1071 | public override int GetHashCode() 1072 | { 1073 | return m_Data.GetHashCode(); 1074 | } 1075 | } 1076 | // End of JSONNumber 1077 | 1078 | public partial class JSONBool : JSONNode 1079 | { 1080 | private bool m_Data; 1081 | 1082 | public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } } 1083 | public override bool IsBoolean { get { return true; } } 1084 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1085 | 1086 | public override string Value 1087 | { 1088 | get { return m_Data.ToString(); } 1089 | set 1090 | { 1091 | bool v; 1092 | if (bool.TryParse(value, out v)) 1093 | m_Data = v; 1094 | } 1095 | } 1096 | public override bool AsBool 1097 | { 1098 | get { return m_Data; } 1099 | set { m_Data = value; } 1100 | } 1101 | 1102 | public JSONBool(bool aData) 1103 | { 1104 | m_Data = aData; 1105 | } 1106 | 1107 | public JSONBool(string aData) 1108 | { 1109 | Value = aData; 1110 | } 1111 | 1112 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1113 | { 1114 | aSB.Append((m_Data) ? "true" : "false"); 1115 | } 1116 | public override bool Equals(object obj) 1117 | { 1118 | if (obj == null) 1119 | return false; 1120 | if (obj is bool) 1121 | return m_Data == (bool)obj; 1122 | return false; 1123 | } 1124 | public override int GetHashCode() 1125 | { 1126 | return m_Data.GetHashCode(); 1127 | } 1128 | } 1129 | // End of JSONBool 1130 | 1131 | public partial class JSONNull : JSONNode 1132 | { 1133 | static JSONNull m_StaticInstance = new JSONNull(); 1134 | public static bool reuseSameInstance = true; 1135 | public static JSONNull CreateOrGet() 1136 | { 1137 | if (reuseSameInstance) 1138 | return m_StaticInstance; 1139 | return new JSONNull(); 1140 | } 1141 | private JSONNull() { } 1142 | 1143 | public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } } 1144 | public override bool IsNull { get { return true; } } 1145 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1146 | 1147 | public override string Value 1148 | { 1149 | get { return "null"; } 1150 | set { } 1151 | } 1152 | public override bool AsBool 1153 | { 1154 | get { return false; } 1155 | set { } 1156 | } 1157 | 1158 | public override bool Equals(object obj) 1159 | { 1160 | if (object.ReferenceEquals(this, obj)) 1161 | return true; 1162 | return (obj is JSONNull); 1163 | } 1164 | public override int GetHashCode() 1165 | { 1166 | return 0; 1167 | } 1168 | 1169 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1170 | { 1171 | aSB.Append("null"); 1172 | } 1173 | } 1174 | // End of JSONNull 1175 | 1176 | internal partial class JSONLazyCreator : JSONNode 1177 | { 1178 | private JSONNode m_Node = null; 1179 | private string m_Key = null; 1180 | public override JSONNodeType Tag { get { return JSONNodeType.None; } } 1181 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1182 | 1183 | public JSONLazyCreator(JSONNode aNode) 1184 | { 1185 | m_Node = aNode; 1186 | m_Key = null; 1187 | } 1188 | 1189 | public JSONLazyCreator(JSONNode aNode, string aKey) 1190 | { 1191 | m_Node = aNode; 1192 | m_Key = aKey; 1193 | } 1194 | 1195 | private void Set(JSONNode aVal) 1196 | { 1197 | if (m_Key == null) 1198 | { 1199 | m_Node.Add(aVal); 1200 | } 1201 | else 1202 | { 1203 | m_Node.Add(m_Key, aVal); 1204 | } 1205 | m_Node = null; // Be GC friendly. 1206 | } 1207 | 1208 | public override JSONNode this[int aIndex] 1209 | { 1210 | get 1211 | { 1212 | return new JSONLazyCreator(this); 1213 | } 1214 | set 1215 | { 1216 | var tmp = new JSONArray(); 1217 | tmp.Add(value); 1218 | Set(tmp); 1219 | } 1220 | } 1221 | 1222 | public override JSONNode this[string aKey] 1223 | { 1224 | get 1225 | { 1226 | return new JSONLazyCreator(this, aKey); 1227 | } 1228 | set 1229 | { 1230 | var tmp = new JSONObject(); 1231 | tmp.Add(aKey, value); 1232 | Set(tmp); 1233 | } 1234 | } 1235 | 1236 | public override void Add(JSONNode aItem) 1237 | { 1238 | var tmp = new JSONArray(); 1239 | tmp.Add(aItem); 1240 | Set(tmp); 1241 | } 1242 | 1243 | public override void Add(string aKey, JSONNode aItem) 1244 | { 1245 | var tmp = new JSONObject(); 1246 | tmp.Add(aKey, aItem); 1247 | Set(tmp); 1248 | } 1249 | 1250 | public static bool operator ==(JSONLazyCreator a, object b) 1251 | { 1252 | if (b == null) 1253 | return true; 1254 | return System.Object.ReferenceEquals(a, b); 1255 | } 1256 | 1257 | public static bool operator !=(JSONLazyCreator a, object b) 1258 | { 1259 | return !(a == b); 1260 | } 1261 | 1262 | public override bool Equals(object obj) 1263 | { 1264 | if (obj == null) 1265 | return true; 1266 | return System.Object.ReferenceEquals(this, obj); 1267 | } 1268 | 1269 | public override int GetHashCode() 1270 | { 1271 | return 0; 1272 | } 1273 | 1274 | public override int AsInt 1275 | { 1276 | get 1277 | { 1278 | JSONNumber tmp = new JSONNumber(0); 1279 | Set(tmp); 1280 | return 0; 1281 | } 1282 | set 1283 | { 1284 | JSONNumber tmp = new JSONNumber(value); 1285 | Set(tmp); 1286 | } 1287 | } 1288 | 1289 | public override float AsFloat 1290 | { 1291 | get 1292 | { 1293 | JSONNumber tmp = new JSONNumber(0.0f); 1294 | Set(tmp); 1295 | return 0.0f; 1296 | } 1297 | set 1298 | { 1299 | JSONNumber tmp = new JSONNumber(value); 1300 | Set(tmp); 1301 | } 1302 | } 1303 | 1304 | public override double AsDouble 1305 | { 1306 | get 1307 | { 1308 | JSONNumber tmp = new JSONNumber(0.0); 1309 | Set(tmp); 1310 | return 0.0; 1311 | } 1312 | set 1313 | { 1314 | JSONNumber tmp = new JSONNumber(value); 1315 | Set(tmp); 1316 | } 1317 | } 1318 | 1319 | public override bool AsBool 1320 | { 1321 | get 1322 | { 1323 | JSONBool tmp = new JSONBool(false); 1324 | Set(tmp); 1325 | return false; 1326 | } 1327 | set 1328 | { 1329 | JSONBool tmp = new JSONBool(value); 1330 | Set(tmp); 1331 | } 1332 | } 1333 | 1334 | public override JSONArray AsArray 1335 | { 1336 | get 1337 | { 1338 | JSONArray tmp = new JSONArray(); 1339 | Set(tmp); 1340 | return tmp; 1341 | } 1342 | } 1343 | 1344 | public override JSONObject AsObject 1345 | { 1346 | get 1347 | { 1348 | JSONObject tmp = new JSONObject(); 1349 | Set(tmp); 1350 | return tmp; 1351 | } 1352 | } 1353 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1354 | { 1355 | aSB.Append("null"); 1356 | } 1357 | } 1358 | // End of JSONLazyCreator 1359 | 1360 | public static class JSON 1361 | { 1362 | public static JSONNode Parse(string aJSON) 1363 | { 1364 | return JSONNode.Parse(aJSON); 1365 | } 1366 | } 1367 | } --------------------------------------------------------------------------------