using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text.RegularExpressions; using BepInEx.Logging; using HarmonyLib; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; namespace TaikoMods; /// /// This will allow custom songs to be read in /// [HarmonyPatch] [SuppressMessage("ReSharper", "InconsistentNaming")] public class MusicPatch { public static int SaveDataMax => DataConst.MusicMax; public static string MusicTrackDirectory => Plugin.Instance.ConfigSongDirectory.Value; public static string SaveFilePath => $"{Plugin.Instance.ConfigSaveDirectory.Value}/save.json"; private const string SongDataFileName = "data.json"; public static ManualLogSource Log => Plugin.Log; public static void Setup(Harmony harmony) { CreateDirectoryIfNotExist(Path.GetDirectoryName(SaveFilePath)); CreateDirectoryIfNotExist(MusicTrackDirectory); PatchManual(harmony); void CreateDirectoryIfNotExist(string path) { path = Path.GetFullPath(path); if (!Directory.Exists(path)) { Log.LogInfo($"Creating path at {path}"); Directory.CreateDirectory(path); } } } private static void PatchManual(Harmony harmony) { var original = typeof(FumenLoader).GetNestedType("PlayerData", BindingFlags.NonPublic).GetMethod("Read"); var prefix = typeof(MusicPatch).GetMethod(nameof(Read_Prefix), BindingFlags.Static | BindingFlags.NonPublic); harmony.Patch(original, new HarmonyMethod(prefix)); } #region Custom Save Data private static CustomMusicSaveDataBody _customSaveData; private static readonly DataContractJsonSerializer saveDataSerializer = new(typeof(CustomMusicSaveDataBody)); private static CustomMusicSaveDataBody GetCustomSaveData() { if (_customSaveData != null) return _customSaveData; Log.LogInfo("Loading custom save data"); var savePath = SaveFilePath; try { CustomMusicSaveDataBody data; if (!File.Exists(savePath)) { data = new CustomMusicSaveDataBody(); using var fileStream = File.OpenWrite(savePath); saveDataSerializer.WriteObject(fileStream, data); } else { using var fileStream = File.OpenRead(savePath); data = (CustomMusicSaveDataBody) saveDataSerializer.ReadObject(fileStream); data.CustomTrackToEnsoRecordInfo ??= new Dictionary(); data.CustomTrackToMusicInfoEx ??= new Dictionary(); } _customSaveData = data; return data; } catch (Exception e) { Log.LogError($"Could not load custom data, creating a fresh one\n {e}"); } try { var data = new CustomMusicSaveDataBody(); using var fileStream = File.OpenWrite(savePath); saveDataSerializer.WriteObject(fileStream, data); } catch (Exception e) { Log.LogError($"Cannot save data at path {savePath}\n {e}"); } return new CustomMusicSaveDataBody(); } private static void SaveCustomData() { if (_customSaveData == null) return; Log.LogInfo("Saving custom save data"); try { var data = GetCustomSaveData(); var savePath = SaveFilePath; using var fileStream = File.OpenWrite(savePath); saveDataSerializer.WriteObject(fileStream, data); } catch (Exception e) { Log.LogError($"Could not save custom data \n {e}"); } } #endregion #region Load Custom Songs private static readonly DataContractJsonSerializer customSongSerializer = new(typeof(CustomSong)); private static List customSongsList; private static readonly Dictionary idToSong = new Dictionary(); private static readonly Dictionary uniqueIdToSong = new Dictionary(); public static List GetCustomSongs() { if (customSongsList != null) return customSongsList; if (!Directory.Exists(MusicTrackDirectory)) { Log.LogError($"Cannot find {MusicTrackDirectory}"); customSongsList = new List(); return customSongsList; } customSongsList = new List(); foreach (var musicDirectory in Directory.GetDirectories(MusicTrackDirectory)) { var files = Directory.GetFiles(musicDirectory); var customSongPath = files.FirstOrDefault(x => Path.GetFileName(x) == SongDataFileName); if (string.IsNullOrWhiteSpace(customSongPath)) continue; using var fileStream = File.OpenRead(customSongPath); var song = (CustomSong) customSongSerializer.ReadObject(fileStream); if (song == null) { Log.LogError($"Cannot read {customSongPath}"); continue; } if (idToSong.ContainsKey(song.id)) { Log.LogError($"Cannot load song {song.songName.text} with ID {song.uniqueId} as it clashes with another, skipping it..."); continue; } if (uniqueIdToSong.ContainsKey(song.uniqueId)) { var uniqueIdTest = song.id.GetHashCode(); while (uniqueIdToSong.ContainsKey(uniqueIdTest)) uniqueIdTest++; Log.LogWarning($"Found song {song.songName.text} with an existing ID {song.uniqueId}, changing it to {uniqueIdTest}"); song.uniqueId = uniqueIdTest; } customSongsList.Add(song); idToSong[song.id] = song; uniqueIdToSong[song.uniqueId] = song; Log.LogInfo($"Added Song {song.songName.text}:{song.id}:{song.uniqueId}"); } if (customSongsList.Count == 0) Log.LogInfo($"No tracks found"); return customSongsList; } #endregion #region Loading and Initializing Data /// /// This will handle loading the meta data of tracks /// [HarmonyPatch(typeof(MusicDataInterface))] [HarmonyPatch(MethodType.Constructor)] [HarmonyPatch(new[] {typeof(string)})] [HarmonyPostfix] private static void MusicDataInterface_Postfix(MusicDataInterface __instance, string path) { // This is where the metadata for tracks are read in our attempt to allow custom tracks will be to add additional metadata to the list that is created Log.LogInfo("Injecting custom songs"); var customSongs = GetCustomSongs(); if (customSongs.Count == 0) return; // now that we have loaded this json, inject it into the existing `musicInfoAccessers` var musicInfoAccessors = __instance.musicInfoAccessers; #region Logic from the original constructor for (int i = 0; i < customSongs.Count; i++) { if (!customSongs[i].debug || !(customSongs[i].id != "kakunin")) { musicInfoAccessors.Add(new MusicDataInterface.MusicInfoAccesser(customSongs[i].uniqueId, customSongs[i].id, customSongs[i].songFileName, customSongs[i].order, customSongs[i].genreNo, customSongs[i].isDLC, customSongs[i].debug, customSongs[i].price, customSongs[i].isCap, customSongs[i].rankmatchNeedHasPlayer, new bool[5] { customSongs[i].branchEasy, customSongs[i].branchNormal, customSongs[i].branchHard, customSongs[i].branchMania, customSongs[i].branchUra }, new int[5] { customSongs[i].starEasy, customSongs[i].starNormal, customSongs[i].starHard, customSongs[i].starMania, customSongs[i].starUra }, new int[5] { customSongs[i].shinutiEasy, customSongs[i].shinutiNormal, customSongs[i].shinutiHard, customSongs[i].shinutiMania, customSongs[i].shinutiUra }, new int[5] { customSongs[i].shinutiEasyDuet, customSongs[i].shinutiNormalDuet, customSongs[i].shinutiHardDuet, customSongs[i].shinutiManiaDuet, customSongs[i].shinutiUraDuet }, new int[5] { customSongs[i].scoreEasy, customSongs[i].scoreNormal, customSongs[i].scoreHard, customSongs[i].scoreMania, customSongs[i].scoreUra })); } } #endregion // sort this musicInfoAccessors.Sort((MusicDataInterface.MusicInfoAccesser a, MusicDataInterface.MusicInfoAccesser b) => a.Order - b.Order); } /// /// This will handle loading the preview data of tracks /// [HarmonyPatch(typeof(SongDataInterface))] [HarmonyPatch(MethodType.Constructor)] [HarmonyPatch(new[] {typeof(string)})] [HarmonyPostfix] private static void SongDataInterface_Postfix(SongDataInterface __instance, string path) { // This is where the metadata for tracks are read in our attempt to allow custom tracks will be to add additional metadata to the list that is created Log.LogInfo("Injecting custom song preview data"); var customSongs = GetCustomSongs(); if (customSongs.Count == 0) return; // now that we have loaded this json, inject it into the existing `songInfoAccessers` var musicInfoAccessors = __instance.songInfoAccessers; foreach (var customTrack in customSongs) { musicInfoAccessors.Add(new SongDataInterface.SongInfoAccesser(customTrack.id, customTrack.previewPos, customTrack.fumenOffsetPos)); } } /// /// This will handle loading the localisation of tracks /// [HarmonyPatch(typeof(WordDataInterface))] [HarmonyPatch(MethodType.Constructor)] [HarmonyPatch(new[] {typeof(string), typeof(string)})] [HarmonyPostfix] private static void WordDataInterface_Postfix(WordDataInterface __instance, string path, string language) { // This is where the metadata for tracks are read in our attempt to allow custom tracks will be to add additional metadata to the list that is created var customSongs = GetCustomSongs(); if (customSongs.Count == 0) return; // now that we have loaded this json, inject it into the existing `songInfoAccessers` var musicInfoAccessors = __instance.wordListInfoAccessers; foreach (var customTrack in customSongs) { musicInfoAccessors.Add(new WordDataInterface.WordListInfoAccesser($"song_{customTrack.id}", customTrack.songName.text, customTrack.songName.font)); musicInfoAccessors.Add(new WordDataInterface.WordListInfoAccesser($"song_sub_{customTrack.id}", customTrack.songSubtitle.text, customTrack.songSubtitle.font)); musicInfoAccessors.Add(new WordDataInterface.WordListInfoAccesser($"song_detail_{customTrack.id}", customTrack.songDetail.text, customTrack.songDetail.font)); } } #endregion #region Loading / Save Custom Save Data /// /// When loading, make sure to ignore custom tracks, as their IDs will be different /// [HarmonyPatch(typeof(SongSelectManager), "LoadSongList")] [HarmonyPrefix] private static bool LoadSongList_Prefix(SongSelectManager __instance) { #region Edited Code Log.LogInfo("Loading custom save"); var customData = GetCustomSaveData(); #endregion #region Setup instanced variables / methods var playDataMgr = (PlayDataManager) typeof(SongSelectManager).GetField("playDataMgr", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(__instance); var musicInfoAccess = (MusicDataInterface.MusicInfoAccesser[]) typeof(SongSelectManager).GetField("musicInfoAccess", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(__instance); var enableKakuninSong = (bool) (typeof(SongSelectManager).GetField("enableKakuninSong", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(__instance) ?? false); var getLocalizedTextMethodInfo = typeof(SongSelectManager).GetMethod("GetLocalizedText", BindingFlags.NonPublic | BindingFlags.Instance); var getLocalizedText = (string x) => (string) getLocalizedTextMethodInfo?.Invoke(__instance, new object[] {x, string.Empty}); var updateSortCategoryInfoMethodInfo = typeof(SongSelectManager).GetMethod("UpdateSortCategoryInfo", BindingFlags.NonPublic | BindingFlags.Instance); var updateSortCategoryInfo = (DataConst.SongSortType x) => updateSortCategoryInfoMethodInfo?.Invoke(__instance, new object[] {x}); #endregion if (playDataMgr == null) { Log.LogError("Could not find playDataMgr"); return true; } __instance.UnsortedSongList.Clear(); playDataMgr.GetMusicInfoExAll(0, out var dst); playDataMgr.GetPlayerInfo(0, out var _); _ = TaikoSingletonMonoBehaviour.Instance.newFriends.Count; for (int i = 0; i < 8; i++) { for (int j = 0; j < musicInfoAccess.Length; j++) { bool flag = false; playDataMgr.GetUnlockInfo(0, DataConst.ItemType.Music, musicInfoAccess[j].UniqueId, out var dst3); if (!dst3.isUnlock && musicInfoAccess[j].Price != 0) { flag = true; } if (!enableKakuninSong && musicInfoAccess[j].IsKakuninSong()) { flag = true; } if (flag || musicInfoAccess[j].GenreNo != i) { continue; } SongSelectManager.Song song2 = new SongSelectManager.Song(); song2.PreviewIndex = j; song2.Id = musicInfoAccess[j].Id; song2.TitleKey = "song_" + musicInfoAccess[j].Id; song2.SubKey = "song_sub_" + musicInfoAccess[j].Id; song2.RubyKey = "song_detail_" + musicInfoAccess[j].Id; song2.UniqueId = musicInfoAccess[j].UniqueId; song2.SongGenre = musicInfoAccess[j].GenreNo; song2.ListGenre = i; song2.Order = musicInfoAccess[j].Order; song2.TitleText = getLocalizedText("song_" + song2.Id); song2.SubText = getLocalizedText("song_sub_" + song2.Id); song2.DetailText = getLocalizedText("song_detail_" + song2.Id); song2.Stars = musicInfoAccess[j].Stars; song2.Branches = musicInfoAccess[j].Branches; song2.HighScores = new SongSelectManager.Score[5]; song2.HighScores2P = new SongSelectManager.Score[5]; song2.DLC = musicInfoAccess[j].IsDLC; song2.Price = musicInfoAccess[j].Price; song2.IsCap = musicInfoAccess[j].IsCap; if (TaikoSingletonMonoBehaviour.Instance.MyDataManager.SongData.GetInfo(song2.Id) != null) { song2.AudioStartMS = TaikoSingletonMonoBehaviour.Instance.MyDataManager.SongData.GetInfo(song2.Id).PreviewPos; } else { song2.AudioStartMS = 0; } if (dst != null) { #region Edited Code MusicInfoEx data; if (musicInfoAccess[j].UniqueId >= SaveDataMax) customData.CustomTrackToMusicInfoEx.TryGetValue(musicInfoAccess[j].UniqueId, out data); else data = dst[musicInfoAccess[j].UniqueId]; song2.Favorite = data.favorite; song2.NotPlayed = new bool[5]; song2.NotCleared = new bool[5]; song2.NotFullCombo = new bool[5]; song2.NotDondaFullCombo = new bool[5]; song2.NotPlayed2P = new bool[5]; song2.NotCleared2P = new bool[5]; song2.NotFullCombo2P = new bool[5]; song2.NotDondaFullCombo2P = new bool[5]; bool isNew = data.isNew; #endregion for (int k = 0; k < 5; k++) { playDataMgr.GetPlayerRecordInfo(0, musicInfoAccess[j].UniqueId, (EnsoData.EnsoLevelType) k, out var dst4); song2.NotPlayed[k] = dst4.playCount <= 0; song2.NotCleared[k] = dst4.crown < DataConst.CrownType.Silver; song2.NotFullCombo[k] = dst4.crown < DataConst.CrownType.Gold; song2.NotDondaFullCombo[k] = dst4.crown < DataConst.CrownType.Rainbow; song2.HighScores[k].hiScoreRecordInfos = dst4.normalHiScore; song2.HighScores[k].crown = dst4.crown; playDataMgr.GetPlayerRecordInfo(1, musicInfoAccess[j].UniqueId, (EnsoData.EnsoLevelType) k, out var dst5); song2.NotPlayed2P[k] = dst5.playCount <= 0; song2.NotCleared2P[k] = dst4.crown < DataConst.CrownType.Silver; song2.NotFullCombo2P[k] = dst5.crown < DataConst.CrownType.Gold; song2.NotDondaFullCombo2P[k] = dst5.crown < DataConst.CrownType.Rainbow; song2.HighScores2P[k].hiScoreRecordInfos = dst5.normalHiScore; song2.HighScores2P[k].crown = dst5.crown; } song2.NewSong = isNew && (song2.DLC || song2.Price > 0); } __instance.UnsortedSongList.Add(song2); } } var unsortedSongList = (from song in __instance.UnsortedSongList orderby song.SongGenre, song.Order select song).ToList(); typeof(SongSelectManager).GetProperty(nameof(SongSelectManager.UnsortedSongList), BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance)?.SetValue(__instance, unsortedSongList); var songList = new List(__instance.UnsortedSongList); typeof(SongSelectManager).GetProperty(nameof(SongSelectManager.SongList), BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance)?.SetValue(__instance, songList); updateSortCategoryInfo(DataConst.SongSortType.Genre); return false; } /// /// When saving favourite tracks, save the custom ones too /// [HarmonyPatch(typeof(SongSelectManager), "SaveFavotiteSongs")] [HarmonyPrefix] private static bool SaveFavotiteSongs_Prefix(SongSelectManager __instance) { var playDataMgr = (PlayDataManager) typeof(SongSelectManager).GetField("playDataMgr", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(__instance); playDataMgr.GetMusicInfoExAll(0, out var dst); var customSaveData = GetCustomSaveData(); bool saveCustomData = false; int num = 0; foreach (SongSelectManager.Song unsortedSong in __instance.UnsortedSongList) { num++; if (unsortedSong.UniqueId < SaveDataMax) { dst[unsortedSong.UniqueId].favorite = unsortedSong.Favorite; playDataMgr.SetMusicInfoEx(0, unsortedSong.UniqueId, ref dst[unsortedSong.UniqueId], num >= __instance.UnsortedSongList.Count); } else { customSaveData.CustomTrackToMusicInfoEx.TryGetValue(unsortedSong.UniqueId, out var data); saveCustomData |= data.favorite != unsortedSong.Favorite; data.favorite = unsortedSong.Favorite; customSaveData.CustomTrackToMusicInfoEx[unsortedSong.UniqueId] = data; } } if (saveCustomData) SaveCustomData(); return false; } /// /// When loading the song, mark the custom song as not new /// [HarmonyPatch(typeof(CourseSelect), "EnsoConfigSubmit")] [HarmonyPrefix] private static bool EnsoConfigSubmit_Prefix(CourseSelect __instance) { var songInfoType = typeof(CourseSelect).GetNestedType("SongInfo", BindingFlags.NonPublic); var scoreType = typeof(CourseSelect).GetNestedType("Score", BindingFlags.NonPublic); var playerTypeEnumType = typeof(CourseSelect).GetNestedType("PlayerType", BindingFlags.NonPublic); var settings = (EnsoData.Settings) typeof(CourseSelect).GetField("settings", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var playDataManager = (PlayDataManager) typeof(CourseSelect).GetField("playDataManager", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var ensoDataManager = (EnsoDataManager) typeof(CourseSelect).GetField("ensoDataManager", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var selectedSongInfo = typeof(CourseSelect).GetField("selectedSongInfo", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var ensoMode = (EnsoMode) typeof(CourseSelect).GetField("ensoMode", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var ensoMode2P = (EnsoMode) typeof(CourseSelect).GetField("ensoMode2P", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var selectedCourse = (int) typeof(CourseSelect).GetField("selectedCourse", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var selectedCourse2P = (int) typeof(CourseSelect).GetField("selectedCourse2P", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var status = (SongSelectStatus) typeof(CourseSelect).GetField("status", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var SetSaveDataEnsoModeMethodInfo = typeof(CourseSelect).GetMethod("SetSaveDataEnsoMode", BindingFlags.NonPublic | BindingFlags.Instance); var SetSaveDataEnsoMode = (object x) => (string) SetSaveDataEnsoModeMethodInfo?.Invoke(__instance, new object[] {x}); var songUniqueId = (int) songInfoType.GetField("UniqueId").GetValue(selectedSongInfo); void SetSettings() => typeof(CourseSelect).GetField("settings", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, settings); settings.ensoType = EnsoData.EnsoType.Normal; settings.rankMatchType = EnsoData.RankMatchType.None; settings.musicuid = (string) songInfoType.GetField("Id").GetValue(selectedSongInfo); settings.musicUniqueId = songUniqueId; settings.genre = (EnsoData.SongGenre) songInfoType.GetField("SongGenre").GetValue(selectedSongInfo); settings.playerNum = 1; settings.ensoPlayerSettings[0].neiroId = ensoMode.neiro; settings.ensoPlayerSettings[0].courseType = (EnsoData.EnsoLevelType) selectedCourse; settings.ensoPlayerSettings[0].speed = ensoMode.speed; settings.ensoPlayerSettings[0].dron = ensoMode.dron; settings.ensoPlayerSettings[0].reverse = ensoMode.reverse; settings.ensoPlayerSettings[0].randomlv = ensoMode.randomlv; settings.ensoPlayerSettings[0].special = ensoMode.special; var array = (Array) songInfoType.GetField("HighScores").GetValue(selectedSongInfo); settings.ensoPlayerSettings[0].hiScore = ((HiScoreRecordInfo) scoreType.GetField("hiScoreRecordInfos").GetValue(array.GetValue(selectedCourse))).score; SetSettings(); if (status.Is2PActive) { settings.ensoPlayerSettings[1].neiroId = ensoMode2P.neiro; settings.ensoPlayerSettings[1].courseType = (EnsoData.EnsoLevelType) selectedCourse2P; settings.ensoPlayerSettings[1].speed = ensoMode2P.speed; settings.ensoPlayerSettings[1].dron = ensoMode2P.dron; settings.ensoPlayerSettings[1].reverse = ensoMode2P.reverse; settings.ensoPlayerSettings[1].randomlv = ensoMode2P.randomlv; settings.ensoPlayerSettings[1].special = ensoMode2P.special; TaikoSingletonMonoBehaviour.Instance.MyDataManager.PlayData.GetPlayerRecordInfo(1, songUniqueId, (EnsoData.EnsoLevelType) selectedCourse2P, out var dst); settings.ensoPlayerSettings[1].hiScore = dst.normalHiScore.score; settings.playerNum = 2; } settings.debugSettings.isTestMenu = false; settings.rankMatchType = EnsoData.RankMatchType.None; settings.isRandomSelect = (bool) songInfoType.GetField("IsRandomSelect").GetValue(selectedSongInfo); settings.isDailyBonus = (bool) songInfoType.GetField("IsDailyBonus").GetValue(selectedSongInfo); ensoMode.songUniqueId = settings.musicUniqueId; ensoMode.level = (EnsoData.EnsoLevelType) selectedCourse; SetSettings(); typeof(CourseSelect).GetField("ensoMode", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, ensoMode); SetSaveDataEnsoMode(Enum.Parse(playerTypeEnumType, "Player1")); ensoMode2P.songUniqueId = settings.musicUniqueId; ensoMode2P.level = (EnsoData.EnsoLevelType) selectedCourse2P; typeof(CourseSelect).GetField("ensoMode2P", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, ensoMode2P); SetSaveDataEnsoMode(Enum.Parse(playerTypeEnumType, "Player2")); playDataManager.GetSystemOption(out var dst2); int deviceTypeIndex = EnsoDataManager.GetDeviceTypeIndex(settings.ensoPlayerSettings[0].inputDevice); settings.noteDispOffset = dst2.onpuDispLevels[deviceTypeIndex]; settings.noteDelay = dst2.onpuHitLevels[deviceTypeIndex]; settings.songVolume = TaikoSingletonMonoBehaviour.Instance.MySoundManager.GetVolume(SoundManager.SoundType.InGameSong); settings.seVolume = TaikoSingletonMonoBehaviour.Instance.MySoundManager.GetVolume(SoundManager.SoundType.Se); settings.voiceVolume = TaikoSingletonMonoBehaviour.Instance.MySoundManager.GetVolume(SoundManager.SoundType.Voice); settings.bgmVolume = TaikoSingletonMonoBehaviour.Instance.MySoundManager.GetVolume(SoundManager.SoundType.Bgm); settings.neiroVolume = TaikoSingletonMonoBehaviour.Instance.MySoundManager.GetVolume(SoundManager.SoundType.InGameNeiro); settings.effectLevel = (EnsoData.EffectLevel) dst2.qualityLevel; SetSettings(); ensoDataManager.SetSettings(ref settings); ensoDataManager.DecideSetting(); if (status.Is2PActive) { dst2.controlType[1] = dst2.controlType[0]; playDataManager.SetSystemOption(ref dst2); } var customSaveData = GetCustomSaveData(); if (songUniqueId < SaveDataMax) { playDataManager.GetMusicInfoExAll(0, out var dst3); dst3[songUniqueId].isNew = false; playDataManager.SetMusicInfoEx(0, songUniqueId, ref dst3[songUniqueId]); } else { customSaveData.CustomTrackToMusicInfoEx.TryGetValue(songUniqueId, out var data); data.isNew = false; customSaveData.CustomTrackToMusicInfoEx[songUniqueId] = data; SaveCustomData(); } UnityEngine.Debug.Log($"p1 is {ensoMode}"); return false; } /// /// When loading the song obtain isfavourite correctly /// [HarmonyPatch(typeof(KpiListCommon.MusicKpiInfo), "GetEnsoSettings")] [HarmonyPrefix] private static bool GetEnsoSettings_Prefix(KpiListCommon.MusicKpiInfo __instance) { TaikoSingletonMonoBehaviour.Instance.MyDataManager.EnsoData.CopySettings(out var dst); __instance.music_id = dst.musicuid; __instance.genre = (int) dst.genre; __instance.course_type = (int) dst.ensoPlayerSettings[0].courseType; __instance.neiro_id = dst.ensoPlayerSettings[0].neiroId; __instance.speed = (int) dst.ensoPlayerSettings[0].speed; __instance.dron = (int) dst.ensoPlayerSettings[0].dron; __instance.reverse = (int) dst.ensoPlayerSettings[0].reverse; __instance.randomlv = (int) dst.ensoPlayerSettings[0].randomlv; __instance.special = (int) dst.ensoPlayerSettings[0].special; PlayDataManager playData = TaikoSingletonMonoBehaviour.Instance.MyDataManager.PlayData; playData.GetEnsoMode(out var dst2); __instance.sort_course = (int) dst2.songSortCourse; __instance.sort_type = (int) dst2.songSortType; __instance.sort_filter = (int) dst2.songFilterType; __instance.sort_favorite = (int) dst2.songFilterTypeFavorite; MusicDataInterface.MusicInfoAccesser[] array = TaikoSingletonMonoBehaviour.Instance.MyDataManager.MusicData.musicInfoAccessers.ToArray(); playData.GetMusicInfoExAll(0, out var dst3); #region edited code for (int i = 0; i < array.Length; i++) { var id = array[i].UniqueId; if (id == dst.musicUniqueId && dst3 != null) { if (id < SaveDataMax) { __instance.is_favorite = dst3[id].favorite; } else { GetCustomSaveData().CustomTrackToMusicInfoEx.TryGetValue(id, out var data); __instance.is_favorite = data.favorite; } } } #endregion playData.GetPlayerInfo(0, out var dst4); __instance.current_coins_num = dst4.donCoin; __instance.total_coins_num = dst4.getCoinsInTotal; playData.GetRankMatchSeasonRecordInfo(0, 0, out var dst5); __instance.rank_point = dst5.rankPointMax; return false; } /// /// Load scores from custom save data /// [HarmonyPatch(typeof(PlayDataManager), "GetPlayerRecordInfo")] [HarmonyPrefix] public static bool GetPlayerRecordInfo_Prefix(int playerId, int uniqueId, EnsoData.EnsoLevelType levelType, out EnsoRecordInfo dst, PlayDataManager __instance) { if (uniqueId < SaveDataMax) { dst = new EnsoRecordInfo(); return true; } int num = (int) levelType; if (num is < 0 or >= 5) num = 0; // load our custom save, this will combine the scores of player1 and player2 var saveData = GetCustomSaveData().CustomTrackToEnsoRecordInfo; if (!saveData.TryGetValue(uniqueId, out var ensoData)) { ensoData = new EnsoRecordInfo[(int) EnsoData.EnsoLevelType.Num]; saveData[uniqueId] = ensoData; } dst = ensoData[num]; return false; } /// /// Save scores to custom save data /// [HarmonyPatch(typeof(PlayDataManager), "UpdatePlayerScoreRecordInfo", new Type[] {typeof(int), typeof(int), typeof(int), typeof(EnsoData.EnsoLevelType), typeof(bool), typeof(DataConst.SpecialTypes), typeof(HiScoreRecordInfo), typeof(DataConst.ResultType), typeof(bool), typeof(DataConst.CrownType)})] [HarmonyPrefix] public static bool UpdatePlayerScoreRecordInfo(PlayDataManager __instance, int playerId, int charaIndex, int uniqueId, EnsoData.EnsoLevelType levelType, bool isSinuchi, DataConst.SpecialTypes spTypes, HiScoreRecordInfo record, DataConst.ResultType resultType, bool savemode, DataConst.CrownType crownType) { if (uniqueId < SaveDataMax) return true; int num = (int) levelType; if (num is < 0 or >= 5) num = 0; var saveData = GetCustomSaveData().CustomTrackToEnsoRecordInfo; if (!saveData.TryGetValue(uniqueId, out var ensoData)) { ensoData = new EnsoRecordInfo[(int) EnsoData.EnsoLevelType.Num]; saveData[uniqueId] = ensoData; } EnsoRecordInfo ensoRecordInfo = ensoData[(int) levelType]; #pragma warning disable Harmony003 if (ensoRecordInfo.normalHiScore.score <= record.score) { ensoRecordInfo.normalHiScore.score = record.score; ensoRecordInfo.normalHiScore.combo = record.combo; ensoRecordInfo.normalHiScore.excellent = record.excellent; ensoRecordInfo.normalHiScore.good = record.good; ensoRecordInfo.normalHiScore.bad = record.bad; ensoRecordInfo.normalHiScore.renda = record.renda; } #pragma warning restore Harmony003 if (crownType != DataConst.CrownType.Off) { if (IsValueInRange((int) crownType, 0, 5) && ensoRecordInfo.crown <= crownType) { ensoRecordInfo.crown = crownType; ensoRecordInfo.cleared = crownType >= DataConst.CrownType.Silver; } } ensoData[(int) levelType] = ensoRecordInfo; if (savemode && playerId == 0) SaveCustomData(); return false; bool IsValueInRange(int myValue, int minValue, int maxValue) { if (myValue >= minValue && myValue < maxValue) return true; return false; } } /// /// Allow for a song id > 400 /// [HarmonyPatch(typeof(EnsoMode), "IsValid")] [HarmonyPrefix] public static bool IsValid_Prefix(ref bool __result, EnsoMode __instance) { #pragma warning disable Harmony003 __result = Validate(); return false; bool Validate() { // commented out this code // if (songUniqueId < DataConst.InvalidId || songUniqueId > DataConst.MusicMax) // { // return false; // } if (!Enum.IsDefined(typeof(EnsoData.SongGenre), __instance.listGenre)) { return false; } if (__instance.neiro < 0 || __instance.neiro > DataConst.NeiroMax) { return false; } if (!Enum.IsDefined(typeof(EnsoData.EnsoLevelType), __instance.level)) { return false; } if (!Enum.IsDefined(typeof(DataConst.SpeedTypes), __instance.speed)) { return false; } if (!Enum.IsDefined(typeof(DataConst.OptionOnOff), __instance.dron)) { return false; } if (!Enum.IsDefined(typeof(DataConst.OptionOnOff), __instance.reverse)) { return false; } if (!Enum.IsDefined(typeof(DataConst.RandomLevel), __instance.randomlv)) { return false; } if (!Enum.IsDefined(typeof(DataConst.SpecialTypes), __instance.special)) { return false; } if (!Enum.IsDefined(typeof(DataConst.SongSortType), __instance.songSortType)) { return false; } if (!Enum.IsDefined(typeof(DataConst.SongSortCourse), __instance.songSortCourse)) { return false; } if (!Enum.IsDefined(typeof(DataConst.SongFilterType), __instance.songFilterType)) { return false; } if (!Enum.IsDefined(typeof(DataConst.SongFilterTypeFavorite), __instance.songFilterTypeFavorite)) { return false; } return true; } #pragma warning restore Harmony003 } #endregion #region Read Fumen private static readonly Regex fumenFilePathRegex = new Regex("(?.*?)_(?[ehmn])(?_[12])?.bin"); private static readonly Dictionary playerToFumenData = new Dictionary(); /// /// Read unencrypted Fumen files, save them to /// todo dispose old fumens? /// /// private static unsafe bool Read_Prefix(string filePath, ref bool __result, object __instance) { var type = typeof(FumenLoader).GetNestedType("PlayerData", BindingFlags.NonPublic); if (File.Exists(filePath)) return true; // if the file doesn't exist, perhaps it's a custom song? var fileName = Path.GetFileName(filePath); var match = fumenFilePathRegex.Match(fileName); if (!match.Success) { Log.LogError($"Cannot interpret {fileName}"); return true; } // get song id var songId = match.Groups["songID"]; var difficulty = match.Groups["difficulty"]; var songIndex = match.Groups["songIndex"]; var newPath = Path.Combine(MusicTrackDirectory, $"{songId}\\{fileName}"); Log.LogInfo($"Redirecting file from {filePath} to {newPath}"); type.GetMethod("Dispose").Invoke(__instance, new object[] { }); type.GetField("fumenPath").SetValue(__instance, newPath); byte[] array = File.ReadAllBytes(newPath); var fumenSize = array.Length; type.GetField("fumenSize").SetValue(__instance, fumenSize); var fumenData = UnsafeUtility.Malloc(fumenSize, 16, Allocator.Persistent); type.GetField("fumenData").SetValue(__instance, (IntPtr) fumenData); Marshal.Copy(array, 0, (IntPtr) fumenData, fumenSize); type.GetField("isReadEnd").SetValue(__instance, true); type.GetField("isReadSucceed").SetValue(__instance, true); __result = true; playerToFumenData[__instance] = (IntPtr) fumenData; return false; } /// /// When asking to get a Fumen, used the ones we stored above /// [HarmonyPatch(typeof(FumenLoader), "GetFumenData")] [HarmonyPrefix] public static unsafe bool GetFumenData_Prefix(int player, ref void* __result, FumenLoader __instance) { var settings = (EnsoData.Settings) typeof(FumenLoader).GetField("settings", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var playerData = (Array) typeof(FumenLoader).GetField("playerData", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); if (player >= 0 && player < settings.playerNum) { if (playerToFumenData.TryGetValue(playerData.GetValue(player), out var data)) { __result = (void*) data; return false; } } // try loading the actual data return true; } #endregion #region Read Song private static readonly Regex musicFilePathRegex = new Regex("^song_(?.*?)$"); /// /// Read an unencrypted song "asynchronously" (it does it instantly, we should have fast enough PCs right?) /// /// [HarmonyPatch(typeof(CriPlayer), "LoadAsync")] [HarmonyPostfix] public static void LoadAsync_Postfix(CriPlayer __instance) { // Run this on the next frame Plugin.Instance.StartCustomCoroutine(LoadAsync()); IEnumerator LoadAsync() { yield return null; var sheetName = __instance.CueSheetName; var path = Application.streamingAssetsPath + "/sound/" + sheetName + ".bin"; if (File.Exists(path)) yield break; typeof(CriPlayer).GetField("isLoadingAsync", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, true); typeof(CriPlayer).GetField("isCancelLoadingAsync", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, false); typeof(CriPlayer).GetProperty("IsPrepared").SetValue(__instance, false); typeof(CriPlayer).GetProperty("IsLoadSucceed").SetValue(__instance, false); typeof(CriPlayer).GetProperty("LoadingState").SetValue(__instance, CriPlayer.LoadingStates.Loading); typeof(CriPlayer).GetProperty("LoadTime").SetValue(__instance, -1f); typeof(CriPlayer).GetField("loadStartTime", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, Time.time); var match = musicFilePathRegex.Match(sheetName); if (!match.Success) { Log.LogError($"Cannot interpret {sheetName}"); yield break; } var songName = match.Groups["songName"]; var newPath = Path.Combine(MusicTrackDirectory, $"{songName}\\{sheetName}.bin"); Log.LogInfo($"Redirecting file from {path} to {newPath}"); var cueSheet = CriAtom.AddCueSheetAsync(sheetName, File.ReadAllBytes(newPath), null); typeof(CriPlayer).GetProperty("CueSheet").SetValue(__instance, cueSheet); if (cueSheet != null) { typeof(CriPlayer).GetField("isLoadingAsync", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, false); typeof(CriPlayer).GetProperty("IsLoadSucceed").SetValue(__instance, true); typeof(CriPlayer).GetProperty("LoadingState").SetValue(__instance, CriPlayer.LoadingStates.Finished); typeof(CriPlayer).GetProperty("LoadTime").SetValue(__instance, 0); yield break; } Log.LogError($"Could not load music"); typeof(CriPlayer).GetProperty("LoadingState").SetValue(__instance, CriPlayer.LoadingStates.Finished); typeof(CriPlayer).GetField("isLoadingAsync", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, false); } } /// /// Read an unencrypted song /// [HarmonyPatch(typeof(CriPlayer), "Load")] [HarmonyPrefix] private static bool Load_Prefix(ref bool __result, CriPlayer __instance) { var sheetName = __instance.CueSheetName; var path = Application.streamingAssetsPath + "/sound/" + sheetName + ".bin"; if (File.Exists(path)) return true; var match = musicFilePathRegex.Match(sheetName); if (!match.Success) { Log.LogError($"Cannot interpret {sheetName}"); return true; } var songName = match.Groups["songName"]; var newPath = Path.Combine(MusicTrackDirectory, $"{songName}\\{sheetName}.bin"); Log.LogInfo($"Redirecting file from {path} to {newPath}"); // load custom song typeof(CriPlayer).GetProperty("IsPrepared").SetValue(__instance, false); typeof(CriPlayer).GetProperty("LoadingState").SetValue(__instance, CriPlayer.LoadingStates.Loading); typeof(CriPlayer).GetProperty("IsLoadSucceed").SetValue(__instance, false); typeof(CriPlayer).GetProperty("LoadTime").SetValue(__instance, -1f); typeof(CriPlayer).GetField("loadStartTime", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, Time.time); if (sheetName == "") { typeof(CriPlayer).GetProperty("LoadingState").SetValue(__instance, CriPlayer.LoadingStates.Finished); __result = false; return false; } // var cueSheet = CriAtom.AddCueSheetAsync(sheetName, File.ReadAllBytes(newPath), null); typeof(CriPlayer).GetProperty("CueSheet").SetValue(__instance, cueSheet); if (cueSheet != null) { __result = true; return false; } typeof(CriPlayer).GetProperty("LoadingState").SetValue(__instance, CriPlayer.LoadingStates.Finished); __result = false; return false; } #endregion #region Data Structures [Serializable] [DataContract(Name = "CustomSaveData")] public class CustomMusicSaveDataBody { [DataMember] public Dictionary CustomTrackToMusicInfoEx = new(); [DataMember] public Dictionary CustomTrackToEnsoRecordInfo = new(); } // ReSharper disable once ClassNeverInstantiated.Global [DataContract(Name = "CustomSong")] [Serializable] public class CustomSong { // Song Details [DataMember] public int uniqueId; [DataMember] public string id; [DataMember] public string songFileName; [DataMember] public int order; [DataMember] public int genreNo; [DataMember] public bool isDLC; [DataMember] public bool debug; [DataMember] public int price; [DataMember] public bool isCap; [DataMember] public int rankmatchNeedHasPlayer; [DataMember] public bool branchEasy; [DataMember] public bool branchNormal; [DataMember] public bool branchHard; [DataMember] public bool branchMania; [DataMember] public bool branchUra; [DataMember] public int starEasy; [DataMember] public int starNormal; [DataMember] public int starHard; [DataMember] public int starMania; [DataMember] public int starUra; [DataMember] public int shinutiEasy; [DataMember] public int shinutiNormal; [DataMember] public int shinutiHard; [DataMember] public int shinutiMania; [DataMember] public int shinutiUra; [DataMember] public int shinutiEasyDuet; [DataMember] public int shinutiNormalDuet; [DataMember] public int shinutiHardDuet; [DataMember] public int shinutiManiaDuet; [DataMember] public int shinutiUraDuet; [DataMember] public int scoreEasy; [DataMember] public int scoreNormal; [DataMember] public int scoreHard; [DataMember] public int scoreMania; [DataMember] public int scoreUra; // Preview Details [DataMember] public int previewPos; [DataMember] public int fumenOffsetPos; // LocalisationDetails /// /// Song Title /// /// A Cruel Angel's Thesis /// /// [DataMember] public TextEntry songName; /// /// Origin of the song /// /// From \" Neon Genesis EVANGELION \" /// /// [DataMember] public TextEntry songSubtitle; /// /// Extra details for the track, sometimes used to say it's Japanese name /// /// 残酷な天使のテーゼ /// /// [DataMember] public TextEntry songDetail; /// /// What font to use for this titles font /// [DataMember] public int songFontType; } [Serializable] public class TextEntry { /// /// The text to display /// public string text; /// /// 0 == Japanese /// 1 == English /// 2 == Traditional Chinese /// 3 == Simplified Chinese /// 4 == Korean /// public int font; } #endregion }