diff --git a/.idea/.idea.TaikoMods.dir/.idea/.name b/.idea/.idea.TaikoMods.dir/.idea/.name deleted file mode 100644 index 8dafd9f..0000000 --- a/.idea/.idea.TaikoMods.dir/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -TaikoMods \ No newline at end of file diff --git a/.idea/.idea.TaikoMods.dir/.idea/projectSettingsUpdater.xml b/.idea/.idea.TaikoMods.dir/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000..4bb9f4d --- /dev/null +++ b/.idea/.idea.TaikoMods.dir/.idea/projectSettingsUpdater.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.TaikoMods.dir/.idea/workspace.xml b/.idea/.idea.TaikoMods.dir/.idea/workspace.xml new file mode 100644 index 0000000..7d16c69 --- /dev/null +++ b/.idea/.idea.TaikoMods.dir/.idea/workspace.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1643955770420 + + + + + + + + + + \ No newline at end of file diff --git a/Folder.DotSettings.user b/Folder.DotSettings.user new file mode 100644 index 0000000..806e514 --- /dev/null +++ b/Folder.DotSettings.user @@ -0,0 +1,5 @@ + + True + <AssemblyExplorer> + <Assembly Path="C:\git\public-git\taiko-mods\Taiko-Mod\bin\Debug\net48\Assembly-CSharp-firstpass.dll" /> +</AssemblyExplorer> \ No newline at end of file diff --git a/MusicPatch.cs b/MusicPatch.cs deleted file mode 100644 index bbe2f1a..0000000 --- a/MusicPatch.cs +++ /dev/null @@ -1,1137 +0,0 @@ -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++) - { - musicInfoAccessors.Add(new MusicDataInterface.MusicInfoAccesser(customSongs[i].uniqueId, customSongs[i].id, $"song_{customSongs[i].id}", customSongs[i].order, customSongs[i].genreNo, - true, false, 0, false, 0, 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("(?.*?)_(?[ehmnx])(?_[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 int order; - [DataMember] public int genreNo; - [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; - } - - [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 -} \ No newline at end of file diff --git a/README.md b/README.md index 1422b3f..47407b5 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,52 @@ -# Taiko no Tatsujin: The Drum Master - Mods +# TakoTako -Hey this modifies version 1.1.0.0 of Taiko no Tatsujin: The Drum Master +Hey this is a mod for Taiko no Tatsujin: The Drum Master Currently has the features: -- Fixes sign in screen +- Fixes sign in screen for version 1.1.0.0 - Skips splash screen -- Disable fullscreen on application focus -- Allows custom tracks to be loaded into the game +- Disable fullscreen on application focus (worked when in windowed mode) +- Allows custom official tracks or TJAs to be loaded into the game +- Override songs names to a certain language than the default one ---- ## Installation -1. Download ![BepInEx](https://github.com/BepInEx/BepInEx/releases) `BepInEx_x64_XXXXX.zip`, as of writing the latest version is 5.4.18. This is a mod to patch Unity Games -2. Find your game's installation directiory, yours might be under `C:\Program Files\ModifiableWindowsApps\Taiko no Tatsujin\` -3. Paste all of the files from the .zip from step 1 into this folder -(It will look something like this, my directory is different because I installed it to a different folder)\ -![](https://github.com/Fluto/Taiko-no-Tatsujin-The-Drum-Master-Patch/blob/main/3.png) -4. Run Taiko no Tatusjin The Drum Master once, then close it -5. ![Download my patch](https://github.com/Fluto/TaikoMods/releases) -6. Look in your game's folder again, new files will have been generated under `.\BepInEx\plugins` -7. Paste the .DLL from step 4 into this folder\ -![](https://github.com/Fluto/Taiko-no-Tatsujin-The-Drum-Master-Patch/blob/main/4.png) -8. And you're done! +To install mods is a bit tricky this time around as we have to take a few more steps to be able to inject files into the game. !(Swigs did a quick video on it here)[https://youtu.be/WDsWDVbtbbI] but if you want to follow along in text read on ahead. + +1. Become an Xbox Insider, to do this open the `Xbox Insider Hub` which you can get from the Microsoft Store if you don't already have it installed. Go to Previews > Windows Gaming, and join it. There should be an update ready for you for the Xbox app, so go ahead and update and relaunch it +2. In the Xbox App go to Settings > General and enable `Use advanced installation and management features`. Feel free to change your installation directory +3. If the game is already installed uninstall it, and reinstall it +4. Download ![BepInEx](https://github.com/BepInEx/BepInEx/releases) `BepInEx_x64_XXXXX.zip`, as of writing the latest version is 5.4.18. This is a mod to patch Unity Games +5. Go to where you installed your game, for example `C:\XboxGames\T Tablet\Content` +6. Paste all of the files from the .zip from step 5 into this folder +(It will look something like this)\ +![](https://github.com/Fluto/TakoTako/blob/main/readme-image-0.png) +7. We now need to give special permissions to the `BepInEx` folder. To do this, right click it, click on `Properties`, go to the `Security` tab, Click on the `Advanced` button, Click Change at the top, Under `Enter the object name to select` field type in your username and click `Check Names`. If the text doesn't become underscored that means you have entered the incorrect username. Then press `Ok` on that window to dismiss it. Going back to the `Advanced Security Settings Window` tick `Replace owner on subcontainers and objects` then finally press Apply. +![](https://github.com/Fluto/TakoTako/blob/main/readme-image-1.png) +8. Run Taiko no Tatusjin The Drum Master once, then close it. This will generate some files +9. Look in your game's folder again, new files will have been generated under `.\BepInEx\plugins` +10. ![Download my patch](https://github.com/Fluto/TaikoMods/releases) +11. Paste the .DLL from step 4 into this folder\ +![](https://github.com/Fluto/TakoTako/blob/main/readme-image-2.png) +12. And you're done! ## Configuration -After installing the mod, and running the game it will generate files in `.\BepInEx\config`. Open `com.fluto.taikomods.cfg` to configure this mod +After installing the mod, and running the game it will generate files in `.\BepInEx\config`. Open `com.fluto.takotako.cfg` to configure this mod Here you can enable each individual feature or redirect where custom songs will be loaded from ## Custom Songs With this feature you can inject custom songs into the game! -To begin place custom songs in `SongDirectory` specified in your configuration file, by default this is `%userprofile%/Documents/TaikoTheDrumMasterMods/customSongs` -Each song must have it's own directory with a unique name. The folder must have this structure +To begin place custom songs in `SongDirectory` specified in your configuration file, by default this is `%userprofile%/Documents/TakoTako/customSongs` +Each song must have it's own directory with a unique name. +These songs can be nested within folders. + +The folder must have this structure: ``` -CustomSongs +Offical Songs -- [MUSIC_ID] ---- data.json (this contains the metadata for the track) ---- song_[MUSIC_ID].bin (this is a raw .acb music file, this is a CRIWARE format) @@ -54,8 +65,20 @@ CustomSongs ---- [MUSIC_ID]_x.bin ---- [MUSIC_ID]_x_1.bin ---- [MUSIC_ID]_x_2.bin + +TJA +-- [MUSIC_ID] +---- [MUSIC_ID].tja +---- song_[MUSIC_ID].ogg or .wav + +Genre override +e.g. this will override the songs to pop +-- 01 Pop +---- [MUSIC_ID] +------ [MUSIC_ID].tja +------ song_[MUSIC_ID].ogg or .wav ``` -This format will be updated in the future to remove redundantancy + ``` data.json Format { @@ -95,22 +118,53 @@ data.json Format int fumenOffsetPos; // Text Info - TextEntry songName (Song Title - e.g. A Cruel Angel's Thesis) - { - string text; - int font; (0 == Japanese, 1 == English, 2 == Traditional Chinese, 3 == Simplified Chinese, 4 == Korean) - } + TextEntry songName; (Song Title - e.g. A Cruel Angel's Thesis) - TextEntry songSubtitle (Origin of the song - e.g. From \" Neon Genesis EVANGELION \") - { - string text; - int font; (0 == Japanese, 1 == English, 2 == Traditional Chinese, 3 == Simplified Chinese, 4 == Korean) - } + TextEntry songSubtitle; (Origin of the song - e.g. From \" Neon Genesis EVANGELION \") + + TextEntry songDetail; (Extra details for the track, sometimes used to say it's Japanese name - e.g. 残酷な天使のテーゼ) +} + +TextEntry { - TextEntry songDetail (Extra details for the track, sometimes used to say it's Japanese name - e.g. 残酷な天使のテーゼ) - { string text; int font; (0 == Japanese, 1 == English, 2 == Traditional Chinese, 3 == Simplified Chinese, 4 == Korean) - } + + // Langauge overrides + string jpText; (langauge override for 日本語 text) + int jpFont; (langauge override for 日本語 text) + string enText; (langauge override for English text) + int enFont; (langauge override for English text) + string frText; (langauge override for Français text) + int frFont; (langauge override for Français text) + string itText; (langauge override for Italiano text) + int itFont; (langauge override for Italiano text) + string deText; (langauge override for Deutsch text) + int deFont; (langauge override for Deutsch text) + string esText; (langauge override for Español text) + int esFont; (langauge override for Español text) + string tcText; (langauge override for 繁體中文 text) + int tcFont; (langauge override for 繁體中文 text) + string scText; (langauge override for 简体中文 text) + int scFont; (langauge override for 简体中文 text) + string krText; (langauge override for 영어 text) + int krFont; (langauge override for 영어 text) } ``` +--- +## Supported Versions +
+Supported Versions +- 1.1.0.0 +- 1.2.2.0 +
+ +--- +## Contributers +(to add!) + +--- +## Credits +- !(SuperSonicAudio)[https://github.com/blueskythlikesclouds/SonicAudioTools] +- !(VGAudio)[https://github.com/Thealexbarney/VGAudio] +- Pulsar#5356 for the TJA2BIN.exe \ No newline at end of file diff --git a/TaikoMods.csproj b/TaikoMods.csproj deleted file mode 100644 index 4a3109a..0000000 --- a/TaikoMods.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - net48 - com.fluto.taikomods - Fixes Taiko issues and allows custom songs - 0.0.1 - true - latest - TaikoMods - com.fluto.taikomods - 1.0.1 - - - - - - - - - - - - - - - - D:\XboxGames\T Tablet\Content\Taiko no Tatsujin_Data\Managed\Assembly-CSharp.dll - - - D:\XboxGames\T Tablet\Content\Taiko no Tatsujin_Data\Managed\Assembly-CSharp-firstpass.dll - - - - - - - diff --git a/.gitignore b/TakoTako/.gitignore similarity index 100% rename from .gitignore rename to TakoTako/.gitignore diff --git a/.idea/.idea.TaikoMods.dir/.idea/.gitignore b/TakoTako/.idea/.idea.TakoTako.dir/.idea/.gitignore similarity index 92% rename from .idea/.idea.TaikoMods.dir/.idea/.gitignore rename to TakoTako/.idea/.idea.TakoTako.dir/.idea/.gitignore index c23595d..30ccfec 100644 --- a/.idea/.idea.TaikoMods.dir/.idea/.gitignore +++ b/TakoTako/.idea/.idea.TakoTako.dir/.idea/.gitignore @@ -5,7 +5,7 @@ /contentModel.xml /modules.xml /projectSettingsUpdater.xml -/.idea.TaikoMods.iml +/.idea.TakoTako.iml # Editor-based HTTP Client requests /httpRequests/ # Datasource local storage ignored files diff --git a/.idea/.idea.TaikoMods.dir/.idea/encodings.xml b/TakoTako/.idea/.idea.TakoTako.dir/.idea/encodings.xml similarity index 100% rename from .idea/.idea.TaikoMods.dir/.idea/encodings.xml rename to TakoTako/.idea/.idea.TakoTako.dir/.idea/encodings.xml diff --git a/.idea/.idea.TaikoMods.dir/.idea/indexLayout.xml b/TakoTako/.idea/.idea.TakoTako.dir/.idea/indexLayout.xml similarity index 100% rename from .idea/.idea.TaikoMods.dir/.idea/indexLayout.xml rename to TakoTako/.idea/.idea.TakoTako.dir/.idea/indexLayout.xml diff --git a/.idea/.idea.TaikoMods.dir/.idea/vcs.xml b/TakoTako/.idea/.idea.TakoTako.dir/.idea/vcs.xml similarity index 69% rename from .idea/.idea.TaikoMods.dir/.idea/vcs.xml rename to TakoTako/.idea/.idea.TakoTako.dir/.idea/vcs.xml index 94a25f7..6c0b863 100644 --- a/.idea/.idea.TaikoMods.dir/.idea/vcs.xml +++ b/TakoTako/.idea/.idea.TakoTako.dir/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/DisableScreenChangeOnFocus.cs b/TakoTako/DisableScreenChangeOnFocus.cs similarity index 95% rename from DisableScreenChangeOnFocus.cs rename to TakoTako/DisableScreenChangeOnFocus.cs index 2634bc4..84cd4c4 100644 --- a/DisableScreenChangeOnFocus.cs +++ b/TakoTako/DisableScreenChangeOnFocus.cs @@ -1,6 +1,6 @@ using HarmonyLib; -namespace TaikoMods; +namespace TakoTako; [HarmonyPatch] public class DisableScreenChangeOnFocus diff --git a/TakoTako/Executables/tja2bin.exe b/TakoTako/Executables/tja2bin.exe new file mode 100644 index 0000000..f47a623 Binary files /dev/null and b/TakoTako/Executables/tja2bin.exe differ diff --git a/TakoTako/FodyWeavers.xml b/TakoTako/FodyWeavers.xml new file mode 100644 index 0000000..8412ac5 --- /dev/null +++ b/TakoTako/FodyWeavers.xml @@ -0,0 +1,8 @@ + + + + TakoTako* + Newtonsoft.Json* + + + diff --git a/TakoTako/FodyWeavers.xsd b/TakoTako/FodyWeavers.xsd new file mode 100644 index 0000000..05e92c1 --- /dev/null +++ b/TakoTako/FodyWeavers.xsd @@ -0,0 +1,141 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Folder.DotSettings b/TakoTako/Folder.DotSettings similarity index 86% rename from Folder.DotSettings rename to TakoTako/Folder.DotSettings index 31b9bd1..2bc44b1 100644 --- a/Folder.DotSettings +++ b/TakoTako/Folder.DotSettings @@ -1,4 +1,5 @@  True True + True True \ No newline at end of file diff --git a/TakoTako/MusicPatch.cs b/TakoTako/MusicPatch.cs new file mode 100644 index 0000000..1b5e985 --- /dev/null +++ b/TakoTako/MusicPatch.cs @@ -0,0 +1,1953 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using BepInEx.Logging; +using HarmonyLib; +using Newtonsoft.Json; +using TakoTako.Common; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace TakoTako; + +/// +/// 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 CustomMusicSaveDataBody GetCustomSaveData() + { + if (_customSaveData != null) + return _customSaveData; + + var savePath = SaveFilePath; + CustomMusicSaveDataBody data; + try + { + if (!File.Exists(savePath)) + { + data = new CustomMusicSaveDataBody(); + SaveCustomData(); + } + else + { + using var fileStream = File.OpenRead(savePath); + data = (CustomMusicSaveDataBody) JsonConvert.DeserializeObject(File.ReadAllText(savePath)); + 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}"); + } + + data = new CustomMusicSaveDataBody(); + SaveCustomData(); + return data; + } + + private static int saveMutex = 0; + + private static void SaveCustomData() + { + if (!Plugin.Instance.ConfigSaveEnabled.Value) + return; + + if (_customSaveData == null) + return; + + saveMutex++; + if (saveMutex > 1) + return; + + SaveData(); + + async void SaveData() + { + while (saveMutex > 0) + { + saveMutex = 0; + Log.LogInfo("Saving custom data"); + try + { + var data = GetCustomSaveData(); + var savePath = SaveFilePath; + var json = JsonConvert.SerializeObject(data); + + using Stream fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough); + using StreamWriter streamWriter = new StreamWriter(fs); + await streamWriter.WriteAsync(json); + } + catch (Exception e) + { + Log.LogError($"Could not save custom data \n {e}"); + } + } + } + } + + #endregion + + #region Load Custom Songs + + private static ConcurrentBag customSongsList; + private static readonly ConcurrentDictionary idToSong = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary uniqueIdToSong = new ConcurrentDictionary(); + + public static ConcurrentBag GetCustomSongs() + { + if (customSongsList != null) + return customSongsList; + + customSongsList = new ConcurrentBag(); + if (!Directory.Exists(MusicTrackDirectory)) + { + Log.LogError($"Cannot find {MusicTrackDirectory}"); + return customSongsList; + } + + try + { + // add songs + var songPaths = Directory.GetFiles(MusicTrackDirectory, "song*.bin", SearchOption.AllDirectories).Select(Path.GetDirectoryName).Distinct().ToList(); + Parallel.ForEach(songPaths, musicDirectory => + { + try + { + var directory = musicDirectory; + + var isGenerated = musicDirectory.EndsWith("[GENERATED]"); + if (isGenerated) + return; + + SubmitDirectory(directory, false); + } + catch (Exception e) + { + Log.LogError(e); + } + }); + + var tjaPaths = Directory.GetFiles(MusicTrackDirectory, "*.tja", SearchOption.AllDirectories).Select(Path.GetDirectoryName).Distinct().ToList(); + // convert / add TJA songs + Parallel.ForEach(tjaPaths, new ParallelOptions() {MaxDegreeOfParallelism = 4}, musicDirectory => + { + try + { + if (IsTjaConverted(musicDirectory, out var conversionStatus) && conversionStatus != null) + { + foreach (var item in conversionStatus.Items.Where(item => item.Successful)) + SubmitDirectory(Path.Combine(musicDirectory, item.FolderName), true); + return; + } + + conversionStatus ??= new ConversionStatus(); + + if (conversionStatus.Items.Count > 0 && conversionStatus.Items.Any(x => !x.Successful && x.Attempts > ConversionStatus.ConversionItem.MaxAttempts)) + { + Log.LogWarning($"Ignoring {musicDirectory}"); + return; + } + + try + { + var pathName = Path.GetFileName(musicDirectory); + var pluginDirectory = @$"{Environment.CurrentDirectory}\BepInEx\plugins\{PluginInfo.PLUGIN_GUID}"; + var tjaConvertPath = @$"{pluginDirectory}\TJAConvert.exe"; + var tja2BinConvertPath = @$"{pluginDirectory}\tja2bin.exe"; + if (!File.Exists(tjaConvertPath) || !File.Exists(tja2BinConvertPath)) + throw new Exception("Cannot find .exes in plugin folder"); + + Log.LogInfo($"Converting {pathName}"); + var info = new ProcessStartInfo() + { + FileName = tjaConvertPath, + Arguments = $"\"{musicDirectory}\"", + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + RedirectStandardOutput = true, + WorkingDirectory = pluginDirectory, + StandardOutputEncoding = Encoding.Unicode, + }; + + var process = new Process(); + process.StartInfo = info; + process.Start(); + var result = process.StandardOutput.ReadToEnd(); + var resultLines = result.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries); + + foreach (var line in resultLines) + { + var match = ConversionStatus.ConversionResultRegex.Match(line); + if (!match.Success) + continue; + + var resultInt = int.Parse(match.Groups["ID"].Value); + var folderPath = match.Groups["PATH"].Value; + + folderPath = Path.GetFullPath(folderPath).Replace(Path.GetFullPath(musicDirectory), "."); + + var existingEntry = conversionStatus.Items.FirstOrDefault(x => x.FolderName == folderPath); + var asciiFolderPath = Regex.Replace(folderPath, @"[^\u0000-\u007F]+", string.Empty); + if (resultInt >= 0) + Log.LogInfo($"Converted {asciiFolderPath} successfully"); + else + Log.LogError($"Could not convert {asciiFolderPath}"); + + if (existingEntry == null) + { + conversionStatus.Items.Add(new ConversionStatus.ConversionItem() + { + Attempts = 1, + FolderName = folderPath, + Successful = resultInt >= 0, + }); + } + else + { + existingEntry.Attempts++; + existingEntry.Successful = resultInt >= 0; + } + } + + File.WriteAllText(Path.Combine(musicDirectory, "conversion.json"), JsonConvert.SerializeObject(conversionStatus), Encoding.Unicode); + } + catch (Exception e) + { + Log.LogError(e); + return; + } + + if (!IsTjaConverted(musicDirectory, out conversionStatus)) + return; + + // if the files are converted, let's gzip those .bins to save space... because they can add up + foreach (var item in conversionStatus.Items) + { + var directory = Path.Combine(musicDirectory, item.FolderName); + var dataPath = Path.Combine(directory, "data.json"); + if (!File.Exists(dataPath)) + { + Log.LogError($"Cannot find {dataPath}"); + return; + } + + var song = JsonConvert.DeserializeObject(File.ReadAllText(dataPath)); + if (song == null) + { + Log.LogError($"Cannot read {dataPath}"); + return; + } + + foreach (var filePath in Directory.EnumerateFiles(directory, "*.bin")) + { + using MemoryStream compressedMemoryStream = new MemoryStream(); + using (FileStream originalFileStream = File.Open(filePath, FileMode.Open)) + { + using var compressor = new GZipStream(compressedMemoryStream, CompressionMode.Compress); + originalFileStream.CopyTo(compressor); + } + + File.WriteAllBytes(filePath, compressedMemoryStream.ToArray()); + } + + song.AreFilesGZipped = true; + File.WriteAllText(dataPath, JsonConvert.SerializeObject(song)); + + SubmitDirectory(directory, true); + } + } + catch (Exception e) + { + Log.LogError(e); + } + }); + + if (customSongsList.Count == 0) + Log.LogInfo($"No tracks found"); + } + catch (Exception e) + { + Log.LogError(e); + } + + return customSongsList; + + void SubmitDirectory(string directory, bool isTjaSong) + { + var dataPath = Path.Combine(directory, "data.json"); + if (!File.Exists(dataPath)) + { + Log.LogError($"Cannot find {dataPath}"); + return; + } + + var song = JsonConvert.DeserializeObject(File.ReadAllText(dataPath)); + if (song == null) + { + Log.LogError($"Cannot read {dataPath}"); + return; + } + + if (Plugin.Instance.ConfigApplyGenreOverride.Value) + { + // if this directory has a genre then override it + var fullPath = Path.GetFullPath(directory); + fullPath = fullPath.Replace(Path.GetFullPath(Plugin.Instance.ConfigSongDirectory.Value), ""); + var directories = fullPath.Split('\\'); + if (directories.Any(x => x.Equals("01 Pop", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 0; + if (directories.Any(x => x.Equals("02 Anime", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 1; + if (directories.Any(x => x.Equals("03 Vocaloid", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 2; + if (directories.Any(x => x.Equals("04 Children and Folk", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 4; + if (directories.Any(x => x.Equals("05 Variety", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 3; + if (directories.Any(x => x.Equals("06 Classical", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 5; + if (directories.Any(x => x.Equals("07 Game Music", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 6; + if (directories.Any(x => x.Equals("08 Live Festival Mode", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 3; + if (directories.Any(x => x.Equals("08 Namco Original", StringComparison.InvariantCultureIgnoreCase))) + song.genreNo = 7; + } + + var instanceId = Guid.NewGuid().ToString(); + song.SongName = song.id; + song.FolderPath = directory; + song.id = instanceId; + + if (uniqueIdToSong.ContainsKey(song.uniqueId) || (song.uniqueId >= 0 && song.uniqueId <= SaveDataMax)) + { + var uniqueIdTest = unchecked(song.id.GetHashCode() + song.previewPos + song.fumenOffsetPos); + while (uniqueIdToSong.ContainsKey(uniqueIdTest) || (uniqueIdTest >= 0 && uniqueIdTest <= SaveDataMax)) + uniqueIdTest = unchecked((uniqueIdTest + 1) * (uniqueIdTest + 1)); + + song.uniqueId = uniqueIdTest; + } + + customSongsList.Add(song); + idToSong[song.id] = song; + uniqueIdToSong[song.uniqueId] = song; + Log.LogInfo($"Added {(isTjaSong ? "TJA" : "")} Song {song.songName.text}"); + } + } + + private static bool IsTjaConverted(string directory, out ConversionStatus conversionStatus) + { + conversionStatus = null; + if (!Directory.Exists(directory)) + return false; + + var conversionFile = Path.Combine(directory, "conversion.json"); + if (!File.Exists(conversionFile)) + return false; + + var json = File.ReadAllText(conversionFile, Encoding.Unicode); + try + { + conversionStatus = JsonConvert.DeserializeObject(json); + if (conversionStatus == null) + return false; + + return conversionStatus.Items.Count != 0 && conversionStatus.Items.All(x => x.Successful); + } + catch + { + return false; + } + } + + #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) + { + try + { + // 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 + + foreach (var song in customSongs) + { + if (song == null) + continue; + + musicInfoAccessors.Add(new MusicDataInterface.MusicInfoAccesser( + song.uniqueId, + song.id, + $"song_{song.id}", + song.order, + song.genreNo, + !Plugin.Instance.ConfigDisableCustomDLCSongs.Value, + false, + 0, false, + 0, + new[] + { + song.branchEasy, + song.branchNormal, + song.branchHard, + song.branchMania, + song.branchUra + }, new[] + { + song.starEasy, + song.starNormal, + song.starHard, + song.starMania, + song.starUra + }, new[] + { + song.shinutiEasy, + song.shinutiNormal, + song.shinutiHard, + song.shinutiMania, + song.shinutiUra + }, new[] + { + song.shinutiEasyDuet, + song.shinutiNormalDuet, + song.shinutiHardDuet, + song.shinutiManiaDuet, + song.shinutiUraDuet + }, new[] + { + song.scoreEasy, + song.scoreNormal, + song.scoreHard, + song.scoreMania, + song.scoreUra + })); + } + + #endregion + + // sort this + musicInfoAccessors.Sort((a, b) => a.Order - b.Order); + } + catch (Exception e) + { + Log.LogError(e); + } + } + + + /// + /// 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; + if (musicInfoAccessors == null) + return; + + foreach (var customTrack in customSongs) + { + if (customTrack == null) + continue; + + 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; + + var customLanguage = Plugin.Instance.ConfigOverrideDefaultSongLanguage.Value; + var languageValue = language; + if (customLanguage is "Japanese" or "English" or "French" or "Italian" or "German" or "Spanish" or "ChineseTraditional" or "ChineseSimplified" or "Korean") + languageValue = customLanguage; + + // now that we have loaded this json, inject it into the existing `songInfoAccessers` + var musicInfoAccessors = __instance.wordListInfoAccessers; + + // override the existing songs if we're using a custom language + if (languageValue != language) + { + var wordListInfoRead = (ReadData) typeof(WordDataInterface).GetField("wordListInfoRead", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); + var dictionary = wordListInfoRead.InfomationDatas.ToList(); + + for (int i = 0; i < musicInfoAccessors.Count; i++) + { + const string songDetailPrefix = "song_detail_"; + var entry = musicInfoAccessors[i]; + var index = entry.Key.IndexOf(songDetailPrefix, StringComparison.Ordinal); + if (index < 0) + continue; + + var songTitle = entry.Key.Substring(songDetailPrefix.Length); + if (string.IsNullOrWhiteSpace(songTitle)) + continue; + + var songKey = $"song_{songTitle}"; + var subtitleKey = $"song_sub_{songTitle}"; + var detailKey = $"song_detail_{songTitle}"; + + var songEntry = dictionary.Find(x => x.key == songKey); + var subtitleEntry = dictionary.Find(x => x.key == subtitleKey); + var detailEntry = dictionary.Find(x => x.key == detailKey); + + if (songEntry == null || subtitleEntry == null || detailEntry == null) + continue; + + musicInfoAccessors.RemoveAll(x => x.Key == songKey || x.Key == subtitleKey || x.Key == detailKey); + + var songValues = GetValuesWordList(songEntry); + var subtitleValues = GetValuesWordList(songEntry); + var detailValues = GetValuesWordList(songEntry); + + musicInfoAccessors.Insert(0, new WordDataInterface.WordListInfoAccesser(songKey, songValues.text, songValues.font)); + musicInfoAccessors.Insert(0, new WordDataInterface.WordListInfoAccesser(subtitleKey, subtitleValues.text, subtitleValues.font)); + musicInfoAccessors.Insert(0, new WordDataInterface.WordListInfoAccesser(detailKey, detailValues.text, detailValues.font)); + } + } + + foreach (var customTrack in customSongs) + { + Add($"song_{customTrack.id}", customTrack.songName); + Add($"song_sub_{customTrack.id}", customTrack.songSubtitle); + Add($"song_detail_{customTrack.id}", customTrack.songDetail); + + void Add(string key, TextEntry textEntry) + { + var (text, font) = GetValuesTextEntry(textEntry); + musicInfoAccessors.Add(new WordDataInterface.WordListInfoAccesser(key, text, font)); + } + } + + (string text, int font) GetValuesWordList(WordListInfo wordListInfo) + { + string text; + int font; + switch (languageValue) + { + case "Japanese": + text = wordListInfo.jpText; + font = wordListInfo.jpFontType; + break; + case "English": + text = wordListInfo.enText; + font = wordListInfo.enFontType; + break; + case "French": + text = wordListInfo.frText; + font = wordListInfo.frFontType; + break; + case "Italian": + text = wordListInfo.itText; + font = wordListInfo.itFontType; + break; + case "German": + text = wordListInfo.deText; + font = wordListInfo.deFontType; + break; + case "Spanish": + text = wordListInfo.esText; + font = wordListInfo.esFontType; + break; + case "Chinese": + case "ChineseT": + case "ChineseTraditional": + text = wordListInfo.tcText; + font = wordListInfo.tcFontType; + break; + case "ChineseSimplified": + case "ChineseS": + text = wordListInfo.scText; + font = wordListInfo.scFontType; + break; + case "Korean": + text = wordListInfo.krText; + font = wordListInfo.krFontType; + break; + default: + text = wordListInfo.enText; + font = wordListInfo.enFontType; + break; + } + + return (text, font); + } + + (string text, int font) GetValuesTextEntry(TextEntry textEntry) + { + string text; + int font; + switch (languageValue) + { + case "Japanese": + text = textEntry.jpText; + font = textEntry.jpFont; + break; + case "English": + text = textEntry.enText; + font = textEntry.enFont; + break; + case "French": + text = textEntry.frText; + font = textEntry.frFont; + break; + case "Italian": + text = textEntry.itText; + font = textEntry.itFont; + break; + case "German": + text = textEntry.deText; + font = textEntry.deFont; + break; + case "Spanish": + text = textEntry.esText; + font = textEntry.esFont; + break; + case "Chinese": + case "ChineseT": + case "ChineseTraditional": + text = textEntry.tcText; + font = textEntry.tcFont; + break; + case "ChineseSimplified": + case "ChineseS": + text = textEntry.scText; + font = textEntry.scFont; + break; + case "Korean": + text = textEntry.krText; + font = textEntry.krFont; + break; + default: + text = textEntry.enText; + font = textEntry.enFont; + break; + } + + if (!string.IsNullOrEmpty(text)) return (text, font); + text = textEntry.text; + font = textEntry.font; + + return (text, 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 (uniqueIdToSong.ContainsKey(musicInfoAccess[j].UniqueId)) + { + customData.CustomTrackToMusicInfoEx.TryGetValue(musicInfoAccess[j].UniqueId, out var objectData); + data = objectData; + } + 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 (uniqueIdToSong.ContainsKey(unsortedSong.UniqueId)) + { + customSaveData.CustomTrackToMusicInfoEx.TryGetValue(unsortedSong.UniqueId, out var data); + saveCustomData |= data.favorite != unsortedSong.Favorite; + data.favorite = unsortedSong.Favorite; + customSaveData.CustomTrackToMusicInfoEx[unsortedSong.UniqueId] = data; + } + else + { + dst[unsortedSong.UniqueId].favorite = unsortedSong.Favorite; + playDataMgr.SetMusicInfoEx(0, unsortedSong.UniqueId, ref dst[unsortedSong.UniqueId], num >= __instance.UnsortedSongList.Count); + } + } + + 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 (uniqueIdToSong.ContainsKey(songUniqueId)) + { + customSaveData.CustomTrackToMusicInfoEx.TryGetValue(songUniqueId, out var data); + data.isNew = false; + customSaveData.CustomTrackToMusicInfoEx[songUniqueId] = data; + SaveCustomData(); + } + else + { + playDataManager.GetMusicInfoExAll(0, out var dst3); + dst3[songUniqueId].isNew = false; + playDataManager.SetMusicInfoEx(0, songUniqueId, ref dst3[songUniqueId]); + } + + 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 (uniqueIdToSong.ContainsKey(id)) + { + GetCustomSaveData().CustomTrackToMusicInfoEx.TryGetValue(id, out var data); + __instance.is_favorite = data.favorite; + } + else + { + __instance.is_favorite = dst3[id].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 (!uniqueIdToSong.ContainsKey(uniqueId)) + { + 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 (!uniqueIdToSong.ContainsKey(uniqueId)) + 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; + } + } + + [HarmonyPatch(typeof(SongSelectManager), "Start")] + [HarmonyPostfix] + public static void Start_Postfix(SongSelectManager __instance) + { + Plugin.Instance.StartCustomCoroutine(SetSelectedSongAsync()); + + IEnumerator SetSelectedSongAsync() + { + while (__instance.SongList.Count == 0 || (bool) typeof(SongSelectManager).GetField("isAsyncLoading", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance)) + yield return null; + + // if the song id is < 0 then fix the selected song index + var ensoMode = (EnsoMode) typeof(SongSelectManager).GetField("ensoMode", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); + var lastPlaySongId = ensoMode.songUniqueId; + + var songIndex = __instance.SongList.IndexOf(__instance.SongList.FirstOrDefault(song => song.UniqueId == lastPlaySongId)); + if (songIndex < 0) + yield break; + + typeof(SongSelectManager).GetProperty("SelectedSongIndex").SetValue(__instance, songIndex); + __instance.SortSongList(ensoMode.songSortCourse, ensoMode.songSortType, ensoMode.songFilterType, ensoMode.songFilterTypeFavorite); + } + } + + /// + /// 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 + } + + /// + /// Allow for a song id less than 0 + /// + [HarmonyPatch(typeof(EnsoDataManager), "DecideSetting")] + [HarmonyPrefix] + public static bool DecideSetting_Prefix(EnsoDataManager __instance) + { + var ensoSettings = (EnsoData.Settings) typeof(EnsoDataManager).GetField("ensoSettings", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); + // if (ensoSettings.musicuid.Length <= 0) + // { + // MusicDataInterface.MusicInfoAccesser infoByUniqueId = TaikoSingletonMonoBehaviour.Instance.MyDataManager.MusicData.GetInfoByUniqueId(ensoSettings.musicUniqueId); + // if (infoByUniqueId != null) + // { + // ensoSettings.musicuid = infoByUniqueId.Id; + // } + // } + // else if (ensoSettings.musicUniqueId <= DataConst.InvalidId) + // { + // MusicDataInterface.MusicInfoAccesser infoById = TaikoSingletonMonoBehaviour.Instance.MyDataManager.MusicData.GetInfoById(ensoSettings.musicuid); + // if (infoById != null) + // { + // ensoSettings.musicUniqueId = infoById.UniqueId; + // } + // } + if (ensoSettings.musicuid.Length <= 0 /* || ensoSettings.musicUniqueId <= DataConst.InvalidId*/) + { + List musicInfoAccessers = TaikoSingletonMonoBehaviour.Instance.MyDataManager.MusicData.musicInfoAccessers; + for (int i = 0; i < musicInfoAccessers.Count; i++) + { + if (!musicInfoAccessers[i].Debug) + { + ensoSettings.musicuid = musicInfoAccessers[i].Id; + ensoSettings.musicUniqueId = musicInfoAccessers[i].UniqueId; + } + } + } + + MusicDataInterface.MusicInfoAccesser infoByUniqueId2 = TaikoSingletonMonoBehaviour.Instance.MyDataManager.MusicData.GetInfoByUniqueId(ensoSettings.musicUniqueId); + if (infoByUniqueId2 != null) + { + ensoSettings.songFilePath = infoByUniqueId2.SongFileName; + } + + __instance.DecidePartsSetting(); + if (ensoSettings.ensoType == EnsoData.EnsoType.Normal) + { + int num = 0; + int dlcType = 2; + if (ensoSettings.rankMatchType == EnsoData.RankMatchType.None) + { + num = ((ensoSettings.playerNum != 1) ? 1 : 0); + } + else if (ensoSettings.rankMatchType == EnsoData.RankMatchType.RankMatch) + { + num = 2; + ensoSettings.isRandomSelect = false; + ensoSettings.isDailyBonus = false; + } + else + { + num = 3; + ensoSettings.isRandomSelect = false; + ensoSettings.isDailyBonus = false; + } + + TaikoSingletonMonoBehaviour.Instance.CosmosLib._kpiListCommon._musicKpiInfo.SetMusicSortSettings(num, dlcType, ensoSettings.isRandomSelect, ensoSettings.isDailyBonus); + } + else + { + ensoSettings.isRandomSelect = false; + ensoSettings.isDailyBonus = false; + } + + typeof(EnsoDataManager).GetField("ensoSettings", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, ensoSettings); + return false; + } + + #endregion + + #region Read Fumen + + private static readonly Regex fumenFilePathRegex = new Regex("(?.*?)_(?[ehmnx])(_(?[12]))?.bin"); + + private static readonly Dictionary playerToFumenData = new Dictionary(); + + /// + /// Read unencrypted Fumen files, save them to + /// + /// + 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"].Value; + var difficulty = match.Groups["difficulty"].Value; + var songIndex = match.Groups["songIndex"].Value; + + if (!idToSong.TryGetValue(songId, out var songInstance)) + { + Log.LogError($"Cannot find song with id: {songId}"); + return true; + } + + var path = songInstance.FolderPath; + var songName = songInstance.SongName; + + var files = Directory.GetFiles(path, "*.bin"); + if (files.Length == 0) + { + Log.LogError($"Cannot find fumen at {path}"); + return true; + } + + var newPath = GetPathOfBestFumen(); + if (!File.Exists(newPath)) + { + Log.LogError($"Cannot find fumen for {newPath}"); + return true; + } + + type.GetMethod("Dispose").Invoke(__instance, new object[] { }); + type.GetField("fumenPath").SetValue(__instance, newPath); + + byte[] array = File.ReadAllBytes(newPath); + if (songInstance.AreFilesGZipped) + { + using var memoryStream = new MemoryStream(array); + using var destination = new MemoryStream(); + using var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress); + gzipStream.CopyTo(destination); + array = destination.ToArray(); + } + + 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; + + string GetPathOfBestFumen() + { + var baseSongPath = Path.Combine(path, $"{songName}"); + var withDifficulty = baseSongPath + $"_{difficulty}"; + var withSongIndex = withDifficulty + (string.IsNullOrWhiteSpace(songIndex) ? "" : $"_{songIndex}"); + + var testPath = withSongIndex + ".bin"; + if (File.Exists(testPath)) + return testPath; + + testPath = withDifficulty + ".bin"; + if (File.Exists(testPath)) + return testPath; + + // add every difficulty below this one + Difficulty difficultyEnum = (Difficulty) Enum.Parse(typeof(Difficulty), difficulty); + int difficultyInt = (int) difficultyEnum; + + var checkDifficulties = new List(); + + for (int i = 1; i < (int) Difficulty.Count; i++) + { + AddIfInRange(difficultyInt - i); + AddIfInRange(difficultyInt + i); + + void AddIfInRange(int checkDifficulty) + { + if (checkDifficulty is >= 0 and < (int) Difficulty.Count) + checkDifficulties.Add((Difficulty) checkDifficulty); + } + } + + foreach (var testDifficulty in checkDifficulties) + { + withDifficulty = baseSongPath + $"_{testDifficulty.ToString()}"; + testPath = withDifficulty + ".bin"; + if (File.Exists(testPath)) + return testPath; + testPath = withDifficulty + "_1.bin"; + if (File.Exists(testPath)) + return testPath; + testPath = withDifficulty + "_2.bin"; + if (File.Exists(testPath)) + return testPath; + } + + // uh... can't find it? + return string.Empty; + } + } + + private enum Difficulty + { + e, + h, + m, + n, + x, + Count, + } + + private static Difficulty[] AllDifficulties = (Difficulty[]) Enum.GetValues(typeof(Difficulty)); + + /// + /// 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"].Value; + if (!idToSong.TryGetValue(songName, out var songInstance)) + { + Log.LogError($"Cannot find song : {songName}"); + yield break; + } + + var newPath = Path.Combine(songInstance.FolderPath, $"{sheetName.Replace(songName, songInstance.SongName)}.bin"); + + var bytes = File.ReadAllBytes(newPath); + if (songInstance.AreFilesGZipped) + { + using var memoryStream = new MemoryStream(bytes); + using var destination = new MemoryStream(); + using var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress); + gzipStream.CopyTo(destination); + bytes = destination.ToArray(); + } + + var cueSheet = CriAtom.AddCueSheetAsync(sheetName, bytes, 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"].Value; + + if (!idToSong.TryGetValue(songName, out var songInstance)) + { + Log.LogError($"Cannot find song : {songName}"); + return true; + } + + var newPath = Path.Combine(songInstance.FolderPath, $"{sheetName.Replace(songName, songInstance.SongName)}.bin"); + + // 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 bytes = File.ReadAllBytes(newPath); + if (songInstance.AreFilesGZipped) + { + using var memoryStream = new MemoryStream(bytes); + using var destination = new MemoryStream(); + using var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress); + gzipStream.CopyTo(destination); + bytes = destination.ToArray(); + } + + var cueSheet = CriAtom.AddCueSheetAsync(sheetName, bytes, 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] + public class CustomMusicSaveDataBody + { + public Dictionary CustomTrackToMusicInfoEx = new(); + public Dictionary CustomTrackToEnsoRecordInfo = new(); + } + + /// + /// This acts as a wrapper for the taiko save data formatting to decrease file size + /// + [Serializable] + public class CustomMusicSaveDataBodySerializable + { + [JsonProperty("m")] public Dictionary CustomTrackToMusicInfoEx = new(); + [JsonProperty("r")] public Dictionary CustomTrackToEnsoRecordInfo = new(); + + + public static explicit operator CustomMusicSaveDataBodySerializable(CustomMusicSaveDataBody m) + { + var result = new CustomMusicSaveDataBodySerializable(); + foreach (var musicInfoEx in m.CustomTrackToMusicInfoEx) + result.CustomTrackToMusicInfoEx[musicInfoEx.Key] = musicInfoEx.Value; + foreach (var ensoRecord in m.CustomTrackToEnsoRecordInfo) + { + var array = new EnsoRecordInfoSerializable[ensoRecord.Value.Length]; + for (var i = 0; i < ensoRecord.Value.Length; i++) + array[i] = ensoRecord.Value[i]; + result.CustomTrackToEnsoRecordInfo[ensoRecord.Key] = array; + } + + return result; + } + + public static explicit operator CustomMusicSaveDataBody(CustomMusicSaveDataBodySerializable m) + { + var result = new CustomMusicSaveDataBody(); + foreach (var musicInfoEx in m.CustomTrackToMusicInfoEx) + result.CustomTrackToMusicInfoEx[musicInfoEx.Key] = musicInfoEx.Value; + foreach (var ensoRecord in m.CustomTrackToEnsoRecordInfo) + { + var array = new EnsoRecordInfo[ensoRecord.Value.Length]; + for (var i = 0; i < ensoRecord.Value.Length; i++) + array[i] = ensoRecord.Value[i]; + result.CustomTrackToEnsoRecordInfo[ensoRecord.Key] = array; + } + + return result; + } + + + [Serializable] + public class MusicInfoExSerializable + { + [JsonProperty("f", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool favorite; + + [JsonProperty("favorite")] + private bool favorite_v0 + { + set => favorite = value; + } + + [JsonProperty("n", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool isNew; + + [JsonProperty("isNew")] + private bool isNew_v0 + { + set => favorite = value; + } + + public static implicit operator MusicInfoEx(MusicInfoExSerializable m) => new() + { + favorite = m.favorite, + isNew = m.isNew, + }; + + public static implicit operator MusicInfoExSerializable(MusicInfoEx m) => new() + { + favorite = m.favorite, + isNew = m.isNew, + }; + } + + [Serializable] + public class EnsoRecordInfoSerializable + { + [JsonProperty("h", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public HiScoreRecordInfoSerializable normalHiScore; + + [JsonProperty("normalHiScore", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + private HiScoreRecordInfoSerializable normalHiScore_v0 + { + set => normalHiScore = value; + } + + [JsonProperty("c", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public DataConst.CrownType crown; + + [JsonProperty("crown", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + private DataConst.CrownType crown_v0 + { + set => crown = value; + } + + [JsonProperty("p", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int playCount; + + [JsonProperty("playCount", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + private int playCount_v0 + { + set => playCount = value; + } + + [JsonProperty("l", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool cleared; + + [JsonProperty("cleared", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + private bool cleared_v0 + { + set => cleared = value; + } + + [JsonProperty("g", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool allGood; + + [JsonProperty("allGood", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + private bool allGood_v0 + { + set => allGood = value; + } + + public static implicit operator EnsoRecordInfo(EnsoRecordInfoSerializable e) => new() + { + normalHiScore = e.normalHiScore, + crown = e.crown, + playCount = e.playCount, + cleared = e.cleared, + allGood = e.allGood, + }; + + public static implicit operator EnsoRecordInfoSerializable(EnsoRecordInfo e) => new() + { + normalHiScore = e.normalHiScore, + crown = e.crown, + playCount = e.playCount, + cleared = e.cleared, + allGood = e.allGood, + }; + + [Serializable] + public struct HiScoreRecordInfoSerializable + { + [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int score; + + [JsonProperty("score", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int score_v0 + { + set => score = value; + } + + [JsonProperty("e", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short excellent; + + [JsonProperty("excellent", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short excellent_v0 + { + set => excellent = value; + } + + [JsonProperty("g", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short good; + + [JsonProperty("good", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short good_v0 + { + set => good = value; + } + + [JsonProperty("b", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short bad; + + [JsonProperty("bad", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short bad_v0 + { + set => bad = value; + } + + [JsonProperty("c", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short combo; + + [JsonProperty("combo", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short combo_v0 + { + set => combo = value; + } + + [JsonProperty("r", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short renda; + + [JsonProperty("renda", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public short renda_v0 + { + set => renda = value; + } + + public static implicit operator HiScoreRecordInfo(HiScoreRecordInfoSerializable h) => new() + { + score = h.score, + excellent = h.excellent, + good = h.good, + bad = h.bad, + combo = h.combo, + renda = h.renda, + }; + + public static implicit operator HiScoreRecordInfoSerializable(HiScoreRecordInfo h) => new() + { + score = h.score, + excellent = h.excellent, + good = h.good, + bad = h.bad, + combo = h.combo, + renda = h.renda, + }; + } + } + } + + #endregion + + private class ConversionStatus + { + public static Regex ConversionResultRegex = new("(?-?\\d*)\\:(?.*?)$"); + + [JsonProperty("i")] public List Items = new(); + + public override string ToString() + { + return $"{nameof(Items)}: {string.Join(",", Items)}"; + } + + public class ConversionItem + { + [JsonIgnore] public const int CurrentVersion = 1; + [JsonIgnore] public const int MaxAttempts = 3; + + [JsonProperty("f")] public string FolderName; + [JsonProperty("a")] public int Attempts; + [JsonProperty("s")] public bool Successful; + [JsonProperty("v")] public int Version = CurrentVersion; + + public override string ToString() + { + return $"{nameof(FolderName)}: {FolderName}, {nameof(Attempts)}: {Attempts}, {nameof(Successful)}: {Successful}, {nameof(Version)}: {Version}"; + } + } + } + + public class SongInstance : CustomSong + { + public string FolderPath; + public string SongName; + } +} diff --git a/NuGet.Config b/TakoTako/NuGet.Config similarity index 100% rename from NuGet.Config rename to TakoTako/NuGet.Config diff --git a/Plugin.cs b/TakoTako/Plugin.cs similarity index 62% rename from Plugin.cs rename to TakoTako/Plugin.cs index e43b8fe..04a3099 100644 --- a/Plugin.cs +++ b/TakoTako/Plugin.cs @@ -3,11 +3,11 @@ using System.Collections; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; -using FlutoTaikoMods; using HarmonyLib; using HarmonyLib.Tools; +using UnityEngine; -namespace TaikoMods +namespace TakoTako { [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] public class Plugin : BaseUnityPlugin @@ -16,60 +16,88 @@ namespace TaikoMods public ConfigEntry ConfigDisableScreenChangeOnFocus; public ConfigEntry ConfigFixSignInScreen; public ConfigEntry ConfigEnableCustomSongs; + public ConfigEntry ConfigSongDirectory; + public ConfigEntry ConfigSaveEnabled; public ConfigEntry ConfigSaveDirectory; - + public ConfigEntry ConfigDisableCustomDLCSongs; + public ConfigEntry ConfigOverrideDefaultSongLanguage; + public ConfigEntry ConfigApplyGenreOverride; + public static Plugin Instance; private Harmony _harmony; public static ManualLogSource Log; - + private void Awake() { Instance = this; Log = Logger; - + Logger.LogInfo($"Plugin {PluginInfo.PLUGIN_GUID} is loaded!"); - + SetupConfig(); - + SetupHarmony(); } private void SetupConfig() { var userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - - ConfigFixSignInScreen = Config.Bind("General", - "FixSignInScreen", - true, - "When true this will apply the patch to fix signing into Xbox Live"); - - ConfigSkipSplashScreen = Config.Bind("General", - "SkipSplashScreen", - true, - "When true this will skip the intro"); - - ConfigDisableScreenChangeOnFocus = Config.Bind("General", - "DisableScreenChangeOnFocus", - false, - "When focusing this wont do anything jank, I thnk"); - + ConfigEnableCustomSongs = Config.Bind("CustomSongs", "EnableCustomSongs", true, "When true this will load custom mods"); - + ConfigSongDirectory = Config.Bind("CustomSongs", "SongDirectory", - $"{userFolder}/Documents/TaikoTheDrumMasterMods/customSongs", + $"{userFolder}/Documents/{typeof(Plugin).Namespace}/customSongs", "The directory where custom tracks are stored"); + ConfigSaveEnabled = Config.Bind("CustomSongs", + "SaveEnabled", + true, + "Should there be local saves? Disable this if you want to wipe modded saves with every load"); + ConfigSaveDirectory = Config.Bind("CustomSongs", "SaveDirectory", $"{userFolder}/Documents/TaikoTheDrumMasterMods/saves", "The directory where saves are stored"); + + ConfigDisableCustomDLCSongs = Config.Bind("CustomSongs", + "DisableCustomDLCSongs", + false, + "By default, DLC is enabled for custom songs, this is to reduce any hiccups when playing online with other people. " + + "Set this to true if you want DLC to be marked as false, be aware that the fact you're playing a custom song will be sent over the internet"); + + ConfigOverrideDefaultSongLanguage = Config.Bind("CustomSongs", + "ConfigOverrideDefaultSongLanguage", + string.Empty, + "Set this value to {Japanese, English, French, Italian, German, Spanish, ChineseTraditional, ChineseSimplified, Korean} " + + "to override all music tracks to a certain language, regardless of your applications language"); + + ConfigApplyGenreOverride = Config.Bind("CustomSongs", + "ConfigApplyGenreOverride", + true, + "Set this value to {01 Pop, 02 Anime, 03 Vocaloid, 04 Children and Folk, 05 Variety, 06 Classical, 07 Game Music, 08 Live Festival Mode, 08 Namco Original} " + + "to override all track's genre in a certain folder. This is useful for TJA files that do not have a genre"); + + ConfigFixSignInScreen = Config.Bind("General", + "FixSignInScreen", + true, + "When true this will apply the patch to fix signing into Xbox Live"); + + ConfigSkipSplashScreen = Config.Bind("General", + "SkipSplashScreen", + true, + "When true this will skip the intro"); + + ConfigDisableScreenChangeOnFocus = Config.Bind("General", + "DisableScreenChangeOnFocus", + false, + "When focusing this wont do anything jank, I thnk"); } - + private void SetupHarmony() { // Patch methods @@ -77,13 +105,13 @@ namespace TaikoMods if (ConfigSkipSplashScreen.Value) _harmony.PatchAll(typeof(SkipSplashScreen)); - + if (ConfigFixSignInScreen.Value) _harmony.PatchAll(typeof(SignInPatch)); - + if (ConfigDisableScreenChangeOnFocus.Value) _harmony.PatchAll(typeof(DisableScreenChangeOnFocus)); - + if (ConfigEnableCustomSongs.Value) { _harmony.PatchAll(typeof(MusicPatch)); diff --git a/TakoTako/References/Newtonsoft.Json.dll b/TakoTako/References/Newtonsoft.Json.dll new file mode 100644 index 0000000..0657dba Binary files /dev/null and b/TakoTako/References/Newtonsoft.Json.dll differ diff --git a/SignInPatch.cs b/TakoTako/SignInPatch.cs similarity index 80% rename from SignInPatch.cs rename to TakoTako/SignInPatch.cs index 415857e..c6f3439 100644 --- a/SignInPatch.cs +++ b/TakoTako/SignInPatch.cs @@ -1,10 +1,8 @@ using System.Reflection; using HarmonyLib; using Microsoft.Xbox; -using TaikoMods; -using UnityEngine; -namespace FlutoTaikoMods; +namespace TakoTako; /// /// This patch will address the issue where signing with GDK is done correctly @@ -13,14 +11,10 @@ namespace FlutoTaikoMods; [HarmonyPatch("SignIn")] public static class SignInPatch { - + // ReSharper disable once InconsistentNaming private static bool Prefix(GdkHelpers __instance) { - // Only apply this patch if we're on version 1.0.0 - if (Application.version != "1.0.0") - return false; - Plugin.Log.LogInfo("Patching sign in to force the user to be prompted to sign in"); var methodInfo = typeof(GdkHelpers).GetMethod("SignInImpl", BindingFlags.NonPublic | BindingFlags.Instance); if (methodInfo == null) @@ -32,4 +26,4 @@ public static class SignInPatch methodInfo.Invoke(__instance, new object[] {true}); return false; } -} \ No newline at end of file +} diff --git a/SkipSplashScreen.cs b/TakoTako/SkipSplashScreen.cs similarity index 96% rename from SkipSplashScreen.cs rename to TakoTako/SkipSplashScreen.cs index 66eed61..d8d93f4 100644 --- a/SkipSplashScreen.cs +++ b/TakoTako/SkipSplashScreen.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using HarmonyLib; -namespace TaikoMods; +namespace TakoTako; [HarmonyPatch] [SuppressMessage("ReSharper", "InconsistentNaming")] diff --git a/TakoTako/TakoTako.csproj b/TakoTako/TakoTako.csproj new file mode 100644 index 0000000..8233088 --- /dev/null +++ b/TakoTako/TakoTako.csproj @@ -0,0 +1,72 @@ + + + + net48 + com.fluto.takotako + Fixes Taiko issues and allows custom songs + 0.0.1 + true + latest + TakoTako + com.fluto.takotako + 2.0.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + D:\XboxGames\T Tablet\Content\Taiko no Tatsujin_Data\Managed\Assembly-CSharp.dll + True + + + D:\XboxGames\T Tablet\Content\Taiko no Tatsujin_Data\Managed\Assembly-CSharp-firstpass.dll + True + + + References\Newtonsoft.Json.dll + + + ..\TakoTakoScripts\TakoTako.Common\bin\Debug\net48\TakoTako.Common.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TakoTakoScripts/.gitignore b/TakoTakoScripts/.gitignore new file mode 100644 index 0000000..9d3e8b0 --- /dev/null +++ b/TakoTakoScripts/.gitignore @@ -0,0 +1,35 @@ +# Common IntelliJ Platform excludes + +# User specific +**/.idea/**/workspace.xml +**/.idea/**/tasks.xml +**/.idea/shelf/* +**/.idea/dictionaries +**/.idea/httpRequests/ + +# Sensitive or high-churn files +**/.idea/**/dataSources/ +**/.idea/**/dataSources.ids +**/.idea/**/dataSources.xml +**/.idea/**/dataSources.local.xml +**/.idea/**/sqlDataSources.xml +**/.idea/**/dynamic.xml + +# Rider +# Rider auto-generates .iml files, and contentModel.xml +**/.idea/**/*.iml +**/.idea/**/contentModel.xml +**/.idea/**/modules.xml + +*.suo +*.user +.vs/ +[Bb]in/ +[Oo]bj/ +_UpgradeReport_Files/ +[Pp]ackages/ + +Thumbs.db +Desktop.ini +.DS_Store +ShortcutFolder.lnk diff --git a/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/.gitignore b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/.gitignore new file mode 100644 index 0000000..bd4c181 --- /dev/null +++ b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/.idea.TakoTakoScripts.iml +/projectSettingsUpdater.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/encodings.xml b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/indexLayout.xml b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/vcs.xml b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/TakoTakoScripts/.idea/.idea.TakoTakoScripts/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TakoTakoScripts/TJAConvert/Files.cs b/TakoTakoScripts/TJAConvert/Files.cs new file mode 100644 index 0000000..6a49f39 --- /dev/null +++ b/TakoTakoScripts/TJAConvert/Files.cs @@ -0,0 +1,133 @@ +namespace TJAConvert +{ + public class Files + { + public static byte[] TemplateACBData = + { + 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0xED, 0x58, 0x7D, 0x6C, 0x14, 0xC7, + 0x15, 0x7F, 0x7B, 0xE7, 0xE0, 0x60, 0xC0, 0x04, 0xDA, 0x40, 0x2A, 0x6A, 0x62, 0x92, 0x26, 0x10, + 0x04, 0xC4, 0x76, 0x02, 0x05, 0xDB, 0x15, 0x3E, 0xFC, 0x85, 0x8D, 0x4D, 0x4E, 0x77, 0xC6, 0x04, + 0x68, 0x80, 0xF5, 0xEE, 0xD8, 0xB7, 0x61, 0x6F, 0xF7, 0xBA, 0xBB, 0x67, 0xB8, 0x04, 0x52, 0x5A, + 0xB5, 0x46, 0x6D, 0x49, 0xA9, 0xA2, 0x82, 0xF2, 0x17, 0x41, 0x6A, 0x50, 0xDA, 0x24, 0x82, 0xA4, + 0x0D, 0x52, 0x93, 0xB4, 0xA5, 0x2D, 0x85, 0xAA, 0x1F, 0x81, 0xAA, 0x9F, 0x91, 0x50, 0x52, 0x50, + 0x2A, 0x25, 0x52, 0x55, 0x3E, 0xDA, 0x2A, 0x4A, 0x93, 0x34, 0xD7, 0xF7, 0x9B, 0xDD, 0xBB, 0x5B, + 0x9F, 0xCD, 0x91, 0x56, 0xEA, 0x3F, 0x55, 0x47, 0xBA, 0xDF, 0xBC, 0x99, 0x37, 0xF3, 0x66, 0xE6, + 0xBD, 0x37, 0xEF, 0xCD, 0x5E, 0xDB, 0x86, 0x81, 0x2E, 0xA2, 0xD9, 0xEF, 0x10, 0x29, 0xF7, 0x13, + 0x45, 0x4E, 0x13, 0x4D, 0xD9, 0x45, 0x28, 0x6D, 0x4A, 0x9A, 0x51, 0x19, 0x60, 0xA8, 0x06, 0xCC, + 0x01, 0xCC, 0x8B, 0x33, 0xDC, 0x0E, 0x58, 0xB4, 0x85, 0xE1, 0x6E, 0x50, 0xCD, 0xA0, 0xFA, 0x00, + 0x1B, 0x00, 0x43, 0x80, 0x0C, 0xE0, 0x61, 0xC0, 0x18, 0xE0, 0x08, 0xE0, 0x38, 0xE0, 0x25, 0xC0, + 0xCF, 0x01, 0xBF, 0x03, 0xBC, 0x01, 0xF8, 0x07, 0x83, 0x32, 0x03, 0xB0, 0x60, 0x33, 0xC3, 0x5D, + 0xA0, 0x3E, 0x99, 0x60, 0xE8, 0x01, 0x6C, 0x45, 0xD3, 0x05, 0x40, 0x9E, 0xF2, 0x25, 0xDE, 0xA5, + 0x72, 0x18, 0xD4, 0x37, 0x01, 0x2F, 0x02, 0x20, 0x4F, 0x79, 0x0D, 0x73, 0xDF, 0xE2, 0x0D, 0x29, + 0x7F, 0xE1, 0x66, 0x24, 0x0A, 0xA8, 0x05, 0xDC, 0x0A, 0x68, 0x04, 0xF4, 0x31, 0x37, 0xB2, 0x95, + 0x85, 0x46, 0x2C, 0x34, 0x1F, 0x02, 0xEC, 0x07, 0x40, 0x5E, 0xE4, 0x18, 0xE0, 0x0C, 0xE0, 0x02, + 0xE0, 0x5D, 0x1E, 0x1C, 0xAD, 0x05, 0xCC, 0x06, 0xDC, 0xCC, 0x7D, 0xD1, 0x5B, 0x40, 0xE1, 0xF8, + 0xD1, 0x3B, 0x01, 0x77, 0x01, 0x96, 0x00, 0xA0, 0x88, 0x68, 0x13, 0x60, 0x39, 0xC6, 0xAD, 0x04, + 0x74, 0x00, 0x36, 0x50, 0x50, 0x94, 0x26, 0x85, 0x8A, 0xE5, 0x26, 0x40, 0x3D, 0xFF, 0x2E, 0x30, + 0x03, 0x34, 0xEB, 0x56, 0xD9, 0xCE, 0xF5, 0x5B, 0x74, 0xDD, 0x12, 0xE1, 0x71, 0x4A, 0x8A, 0x45, + 0xF3, 0x5C, 0xE2, 0xBA, 0x0A, 0xED, 0x4F, 0x10, 0xDD, 0x70, 0x84, 0x6B, 0xF3, 0xDA, 0xF3, 0xAA, + 0x79, 0xBD, 0xE8, 0x62, 0xFE, 0x19, 0xE3, 0xFB, 0x6B, 0xB6, 0xFB, 0x1B, 0xAA, 0xD9, 0x5B, 0xD8, + 0x18, 0xCA, 0xEA, 0xE5, 0x3F, 0x3E, 0xC0, 0x7D, 0x47, 0x60, 0xDA, 0x6B, 0xCB, 0x8C, 0xEE, 0x9F, + 0xA4, 0x73, 0x1A, 0xE0, 0x0B, 0xE5, 0xBD, 0xF9, 0x71, 0x43, 0x20, 0x77, 0x2D, 0xD1, 0x74, 0xAA, + 0x2C, 0x3F, 0x54, 0xA6, 0x6F, 0x0F, 0x6F, 0xEF, 0x1A, 0x65, 0xAD, 0x50, 0x75, 0xE1, 0x50, 0x97, + 0x61, 0x8A, 0x1E, 0x5D, 0x58, 0x9E, 0x31, 0x6C, 0x70, 0x33, 0x69, 0x3C, 0x24, 0x68, 0x50, 0x38, + 0xAE, 0x61, 0x5B, 0x34, 0x90, 0xCB, 0x08, 0x1A, 0x50, 0x9D, 0x11, 0xE1, 0x51, 0x4C, 0x1B, 0xEE, + 0xD7, 0x97, 0xAF, 0x55, 0xDD, 0x14, 0xB5, 0xAB, 0x9E, 0x18, 0xB1, 0x9D, 0x5C, 0xE7, 0x2E, 0x4F, + 0x58, 0x72, 0x60, 0x7B, 0x56, 0x0C, 0xA8, 0x43, 0xA6, 0x00, 0xB1, 0x5E, 0x4D, 0x07, 0x8D, 0x8D, + 0xEA, 0xA8, 0x18, 0xB6, 0x9D, 0xB4, 0xDF, 0x8A, 0x19, 0xAE, 0xAA, 0xF9, 0x64, 0xB7, 0xA3, 0x66, + 0x52, 0x01, 0x69, 0xDA, 0x43, 0xAA, 0x29, 0x79, 0x09, 0x31, 0x2C, 0x1C, 0x61, 0x69, 0x22, 0x34, + 0xBE, 0x24, 0x2C, 0x99, 0xB3, 0xBC, 0x60, 0x4E, 0x52, 0x7C, 0xA6, 0xDD, 0x4E, 0xA7, 0x55, 0x4B, + 0xF7, 0xDB, 0x03, 0x8E, 0xAA, 0xED, 0x28, 0xB2, 0xB2, 0x65, 0x22, 0xDA, 0x6D, 0xCB, 0x73, 0x6C, + 0xB3, 0x24, 0x29, 0x96, 0xF5, 0xEC, 0x7E, 0x5B, 0xCF, 0x9A, 0xAA, 0xC7, 0x7B, 0x0F, 0xE6, 0x79, + 0x8E, 0x50, 0xD3, 0xB1, 0x9D, 0x43, 0x03, 0xB6, 0xB6, 0xD1, 0x76, 0x76, 0xDC, 0x67, 0xEA, 0xC4, + 0x2D, 0x28, 0xA7, 0xA0, 0x0D, 0x1E, 0x62, 0x58, 0x23, 0x38, 0x61, 0x9F, 0x91, 0x36, 0x3C, 0x8C, + 0xF2, 0xE7, 0xAE, 0xCF, 0xA6, 0x0B, 0x9D, 0x7D, 0x86, 0x2B, 0x19, 0x6E, 0xB8, 0x73, 0xBD, 0xAD, + 0x0B, 0xBF, 0x33, 0xA6, 0x0D, 0x75, 0x67, 0x0D, 0xBD, 0xB4, 0x9A, 0x54, 0x67, 0xF9, 0xDA, 0xDB, + 0xE4, 0xE2, 0xDA, 0xD0, 0xA0, 0x6D, 0x66, 0xD3, 0x72, 0x6B, 0xBC, 0xEE, 0xA0, 0x6A, 0x16, 0x74, + 0x7C, 0x5F, 0xD6, 0x73, 0x0D, 0x9D, 0x65, 0x5B, 0xC1, 0x06, 0xD6, 0x98, 0xB6, 0xB6, 0x63, 0xFC, + 0xC9, 0x65, 0x57, 0xB0, 0x3D, 0x3E, 0x38, 0xB5, 0xA7, 0x54, 0x56, 0x92, 0x27, 0x9C, 0x4E, 0x4B, + 0xB3, 0x75, 0x96, 0x27, 0x4D, 0xDB, 0x39, 0xCA, 0x76, 0x0F, 0x94, 0xA2, 0x49, 0x65, 0x94, 0x34, + 0xC9, 0xF6, 0x2E, 0x33, 0x48, 0xC1, 0x9C, 0x45, 0xB3, 0x77, 0xA8, 0x9E, 0x1A, 0x2C, 0x27, 0x54, + 0x8F, 0x0D, 0xA4, 0xF5, 0x58, 0xC3, 0x76, 0xD1, 0x11, 0xE2, 0x8E, 0x61, 0x3B, 0x86, 0x97, 0x93, + 0x4B, 0x85, 0xF4, 0xE1, 0xDB, 0x6B, 0x9C, 0x05, 0xA5, 0x71, 0x27, 0xDA, 0x34, 0xB4, 0x3F, 0x3E, + 0x5E, 0x9C, 0x8F, 0x90, 0x16, 0x7C, 0x86, 0xB8, 0x6A, 0x9A, 0xC2, 0x0B, 0x8D, 0x9B, 0x94, 0x23, + 0x65, 0x4E, 0xCE, 0xB1, 0xB3, 0x96, 0xDE, 0x2D, 0x2C, 0xE1, 0xA8, 0x9E, 0xED, 0xF8, 0x7D, 0x89, + 0xC6, 0x06, 0x4A, 0xAC, 0xA2, 0xC4, 0x4A, 0x8A, 0x3B, 0xF6, 0x83, 0x42, 0xF3, 0xD6, 0x89, 0x1C, + 0x25, 0x56, 0x50, 0x62, 0x39, 0x25, 0xEE, 0xA5, 0xC4, 0x3D, 0x94, 0x68, 0xE2, 0x31, 0x94, 0x68, + 0xA0, 0xB8, 0xAA, 0x43, 0x81, 0x31, 0x36, 0xDA, 0x04, 0xD3, 0x95, 0x3A, 0x62, 0xC3, 0x6E, 0x53, + 0x70, 0xC3, 0x6A, 0x62, 0xED, 0x6B, 0xEA, 0xBB, 0x58, 0x71, 0xAA, 0x77, 0x77, 0xBC, 0xBD, 0x9E, + 0xDD, 0x69, 0x59, 0xE3, 0xB2, 0x7B, 0x9A, 0x96, 0x35, 0x34, 0xD6, 0xAF, 0xC9, 0x1A, 0xA6, 0xDE, + 0x5C, 0x43, 0xAE, 0x6D, 0x8D, 0x6C, 0xF3, 0x44, 0x3A, 0xD3, 0x50, 0xF1, 0xCA, 0xEE, 0xE9, 0x58, + 0x73, 0xFB, 0xE3, 0x8F, 0x9E, 0x18, 0x7B, 0xF1, 0x58, 0xBA, 0xF9, 0xE2, 0x2F, 0xBC, 0xB1, 0x72, + 0x7E, 0x9B, 0xCC, 0x4F, 0xF4, 0x2A, 0xC7, 0xC9, 0x4E, 0xAE, 0x11, 0x05, 0x7E, 0x2F, 0x19, 0x53, + 0x69, 0x1E, 0x05, 0xF9, 0xA9, 0x0A, 0x69, 0xA8, 0x86, 0xE3, 0x3A, 0xDD, 0xC2, 0x49, 0x80, 0x16, + 0x82, 0x6A, 0x40, 0x66, 0x59, 0x05, 0x6E, 0x0F, 0xB8, 0x71, 0x80, 0x8E, 0x79, 0x51, 0x39, 0x3B, + 0x1B, 0x5A, 0xA2, 0xF5, 0x4F, 0x16, 0x29, 0x6C, 0x49, 0x18, 0xB8, 0x47, 0xA7, 0x92, 0x8B, 0xC0, + 0xC8, 0xC5, 0x56, 0x8F, 0xA5, 0x8B, 0x5D, 0xB4, 0xC1, 0x15, 0x0E, 0x7C, 0x84, 0xA4, 0xF7, 0x23, + 0xB0, 0x84, 0x2F, 0x64, 0xBF, 0x9A, 0xA1, 0x3E, 0x61, 0x8D, 0x78, 0x29, 0x38, 0x47, 0x19, 0xC7, + 0x0D, 0xC2, 0xD3, 0xA0, 0xE1, 0x1A, 0x43, 0x86, 0xC9, 0x5E, 0x14, 0x3A, 0x5F, 0x1F, 0x9F, 0xEF, + 0x36, 0xAE, 0x17, 0x51, 0x21, 0x20, 0x46, 0x68, 0x0A, 0xCE, 0x87, 0x03, 0xDD, 0x98, 0x08, 0x62, + 0xDF, 0xC7, 0xA8, 0x10, 0x8D, 0xC2, 0xB5, 0xBF, 0xB3, 0x6B, 0xE8, 0x3B, 0x90, 0x7F, 0x81, 0xE5, + 0xF7, 0x72, 0xFD, 0x69, 0xFE, 0xBD, 0x26, 0x19, 0x35, 0x34, 0x1B, 0xF2, 0x21, 0x7A, 0x2A, 0x94, + 0x73, 0x33, 0xA0, 0x1E, 0xB0, 0x18, 0xB0, 0x02, 0x8C, 0xD5, 0xD0, 0x5F, 0x1F, 0xA8, 0x8D, 0x00, + 0x91, 0xF0, 0x35, 0x17, 0xA1, 0x88, 0xF2, 0x6C, 0x07, 0xF4, 0x96, 0x97, 0xA5, 0x70, 0x8B, 0xA8, + 0x5F, 0xA4, 0x39, 0x86, 0xB2, 0xAB, 0xB0, 0x1A, 0xE5, 0xAD, 0xF4, 0x75, 0xE8, 0x7B, 0x10, 0x22, + 0x0D, 0xAE, 0x4C, 0x4A, 0xB5, 0x2C, 0x61, 0xBA, 0xD4, 0x67, 0xDB, 0x99, 0x2E, 0x53, 0x1D, 0xA1, + 0xA4, 0x9A, 0xCE, 0x98, 0xCC, 0x4D, 0x70, 0x0C, 0xC6, 0x08, 0xD9, 0x16, 0x2E, 0x8D, 0xBB, 0x94, + 0x25, 0x37, 0x8C, 0xDB, 0x0E, 0xC7, 0x9F, 0x52, 0xBB, 0x47, 0xAF, 0xE4, 0x5F, 0xFE, 0xF9, 0x39, + 0xB9, 0x2A, 0xB4, 0x89, 0x9B, 0xEC, 0x5F, 0xCA, 0x16, 0xC9, 0x98, 0x41, 0x4B, 0x71, 0x7E, 0x9C, + 0x75, 0x0A, 0x94, 0x3C, 0x0D, 0x87, 0xC3, 0x5B, 0x81, 0x96, 0x00, 0x5A, 0xD0, 0xEC, 0x06, 0x3C, + 0x00, 0xC8, 0x00, 0x3E, 0x07, 0xC6, 0x63, 0xA0, 0x0E, 0x03, 0x8E, 0x01, 0x7E, 0x02, 0x59, 0xBF, + 0x2D, 0x2E, 0x57, 0x55, 0x20, 0xF2, 0xE3, 0x12, 0x1E, 0xDA, 0xE8, 0x91, 0x17, 0xD8, 0xCF, 0x41, + 0x83, 0xB6, 0xA1, 0xF9, 0xC1, 0xA3, 0xDB, 0xB1, 0xB3, 0x19, 0xDF, 0x98, 0x7E, 0xB8, 0xF0, 0x0D, + 0x5A, 0xF2, 0x3C, 0xB6, 0x2A, 0xD4, 0xA5, 0x05, 0x49, 0xC5, 0x0D, 0x67, 0x98, 0xA4, 0xA7, 0x3A, + 0x9E, 0x3F, 0x21, 0xD4, 0xCB, 0x5A, 0xE4, 0xE9, 0x2E, 0x05, 0xDE, 0x07, 0x67, 0xC5, 0xE5, 0x6E, + 0x2C, 0xEF, 0x68, 0xF2, 0x83, 0x8D, 0x8C, 0xC4, 0x2E, 0x95, 0x45, 0x96, 0x70, 0x00, 0x0D, 0x2D, + 0x03, 0xCF, 0x2E, 0x31, 0x5C, 0x0A, 0x9E, 0x30, 0x15, 0xF5, 0x4F, 0xD0, 0x7F, 0x1D, 0xD7, 0x77, + 0xF0, 0xEF, 0x53, 0x92, 0xA1, 0xD0, 0x8D, 0x14, 0x18, 0xA3, 0x90, 0xD6, 0x17, 0x14, 0x02, 0x7E, + 0xA0, 0x87, 0x82, 0x3E, 0x88, 0x62, 0xBE, 0x5E, 0x37, 0x46, 0xF8, 0xA5, 0xD4, 0x51, 0xB5, 0x7A, + 0x2F, 0xD3, 0x9D, 0x55, 0xED, 0xEF, 0x72, 0x65, 0x33, 0x67, 0x61, 0x85, 0x77, 0x41, 0x60, 0xFF, + 0x8F, 0xF3, 0x7A, 0x70, 0xE6, 0x51, 0x49, 0xA3, 0x4C, 0x97, 0xF7, 0x4D, 0xFA, 0xFF, 0x14, 0xC0, + 0x2C, 0xEC, 0x64, 0x3E, 0xA8, 0xC5, 0x80, 0x36, 0x40, 0x12, 0xDE, 0xA1, 0xC1, 0x3B, 0x2C, 0x4C, + 0xCE, 0x81, 0xFA, 0x3C, 0xFA, 0xBE, 0x06, 0xEE, 0xA1, 0xB0, 0x95, 0x41, 0xF9, 0xAD, 0x27, 0x7D, + 0x53, 0xD3, 0x51, 0x6E, 0x4B, 0x15, 0xF9, 0x39, 0xC9, 0x57, 0xDE, 0x38, 0x0B, 0xFF, 0x47, 0x26, + 0x2D, 0x37, 0x92, 0xFF, 0x96, 0x09, 0x3D, 0x6B, 0xA4, 0x27, 0xF9, 0x24, 0x5F, 0x0D, 0x9F, 0xE0, + 0xD4, 0x2B, 0xBB, 0x93, 0x9A, 0x5D, 0x1C, 0x27, 0xF7, 0xC6, 0x17, 0xA9, 0x72, 0x09, 0xF4, 0xA7, + 0xB3, 0xFE, 0xB6, 0x72, 0xF3, 0x2B, 0x4C, 0x0F, 0x49, 0x46, 0xAD, 0x7F, 0x7F, 0x64, 0xFC, 0x00, + 0xCC, 0x85, 0xFE, 0x16, 0x80, 0x92, 0xF7, 0x67, 0x15, 0xA8, 0x4E, 0xC0, 0x16, 0x80, 0x05, 0x78, + 0x04, 0xF0, 0x75, 0x70, 0x9F, 0x84, 0x12, 0x9F, 0x43, 0xF3, 0x04, 0xE0, 0x0C, 0xE9, 0x45, 0x27, + 0x8A, 0x94, 0xD6, 0xF6, 0x75, 0x5A, 0x7E, 0x93, 0x8A, 0x0F, 0x22, 0x8A, 0x9B, 0x6A, 0x6E, 0x88, + 0x0F, 0x91, 0xC0, 0xAB, 0x07, 0x7E, 0x19, 0x78, 0xA4, 0xAC, 0xFE, 0x5B, 0xFA, 0xFE, 0x70, 0x97, + 0x22, 0x7C, 0xAD, 0xA4, 0x6D, 0xAE, 0x7F, 0x13, 0x27, 0x2F, 0xB1, 0xAE, 0x64, 0x93, 0x12, 0x89, + 0x48, 0xF5, 0xD4, 0xCB, 0x9E, 0x39, 0xB4, 0x38, 0x5A, 0xE2, 0xAF, 0x6D, 0x8F, 0x41, 0x65, 0xDB, + 0x87, 0xD3, 0x1E, 0xD7, 0x1C, 0x95, 0x71, 0xB3, 0xF6, 0x46, 0xF7, 0x68, 0x76, 0x3A, 0x13, 0x79, + 0x5A, 0x99, 0xC9, 0x8D, 0xBD, 0x18, 0xA7, 0x19, 0x19, 0xFE, 0x58, 0xC8, 0xA8, 0x15, 0x83, 0xE5, + 0x84, 0x32, 0xEB, 0x4C, 0x3E, 0x5F, 0x37, 0xF7, 0xA6, 0xEA, 0x5C, 0x6E, 0xF3, 0xE6, 0xD6, 0xF9, + 0x57, 0x5A, 0x0F, 0x44, 0x7F, 0x76, 0x65, 0xF7, 0x63, 0xA7, 0x4E, 0x1C, 0x3A, 0x78, 0x68, 0x57, + 0x2A, 0x65, 0xF4, 0xB6, 0x2C, 0xBD, 0xDA, 0x7A, 0x2E, 0xFA, 0xCB, 0x2B, 0xEF, 0xFF, 0xE8, 0x37, + 0x2F, 0x1C, 0x3D, 0x78, 0x68, 0x6C, 0x1F, 0xF7, 0xF5, 0xB6, 0xD4, 0x5D, 0x6D, 0xFD, 0x63, 0xF4, + 0x95, 0xDA, 0x25, 0x2D, 0xBD, 0x9B, 0x52, 0xA9, 0xD1, 0xB1, 0x2F, 0x1E, 0xFC, 0xC6, 0xB7, 0x7F, + 0x78, 0xB9, 0xFA, 0xEC, 0xD5, 0xDD, 0x17, 0x7F, 0x7D, 0xEA, 0x05, 0x1E, 0xF4, 0xDE, 0xC9, 0x4B, + 0xFF, 0x7C, 0xF5, 0xD2, 0xBC, 0xCF, 0xFE, 0xF9, 0xE4, 0x13, 0xDA, 0xAD, 0x17, 0x8F, 0xBF, 0x71, + 0xA9, 0xE5, 0x83, 0x7B, 0x2F, 0x3F, 0xF2, 0xDE, 0x5F, 0xDF, 0x7E, 0x34, 0xBF, 0xCF, 0x3D, 0xB7, + 0xD4, 0x3D, 0xB7, 0xEF, 0xFC, 0xCE, 0x57, 0xDE, 0xAE, 0xDB, 0x33, 0xFA, 0x87, 0x99, 0x9B, 0x9E, + 0x3F, 0xFF, 0xD5, 0x07, 0x2E, 0xBF, 0xFE, 0xFD, 0xFC, 0xD9, 0x87, 0xF3, 0x07, 0x5E, 0xFA, 0xDB, + 0xA9, 0x15, 0x3F, 0xF8, 0xEE, 0xF3, 0x3B, 0xCF, 0x3F, 0xF3, 0xD4, 0x49, 0xE7, 0xF4, 0xB1, 0xDD, + 0x0F, 0x9E, 0x7D, 0x79, 0x24, 0xFD, 0xFA, 0x71, 0x23, 0xBF, 0x70, 0xDB, 0xDF, 0xDF, 0xB9, 0x7A, + 0x72, 0xD1, 0xBF, 0x75, 0xB4, 0xFF, 0x97, 0xFF, 0xA9, 0xF2, 0x91, 0xFA, 0xC9, 0xFB, 0xEF, 0x3F, + 0xFB, 0x3D, 0xEF, 0xCD, 0xF7, 0x8F, 0xAE, 0x7B, 0xF6, 0x3B, 0x8F, 0xD7, 0x4E, 0xDD, 0xFF, 0xF4, + 0x87, 0xF8, 0xC0, 0x1E, 0x5F, 0x82, 0xFC, 0xD5, 0x11, 0xCA, 0x5F, 0x1D, 0x92, 0xA1, 0xC8, 0x5C, + 0x1F, 0x91, 0xEF, 0x33, 0xFE, 0x7D, 0x94, 0x7F, 0xF3, 0xFD, 0xEF, 0x1A, 0x37, 0xFC, 0x7D, 0x43, + 0xFD, 0xAA, 0xCB, 0xC1, 0x83, 0x3F, 0x70, 0x2A, 0xCA, 0xDF, 0xCD, 0xF2, 0xF0, 0x70, 0x5B, 0x27, + 0x69, 0x94, 0x2A, 0x7E, 0xE1, 0xF2, 0x6B, 0x15, 0x81, 0x72, 0x06, 0x16, 0x99, 0xDD, 0xCC, 0x30, + 0x97, 0x7F, 0x75, 0x48, 0x45, 0xF3, 0x11, 0x02, 0x16, 0x60, 0x1C, 0x46, 0xDD, 0x89, 0x28, 0x09, + 0xA2, 0x01, 0x44, 0xF8, 0x33, 0xC7, 0x0F, 0x3E, 0x32, 0xF6, 0x03, 0x38, 0xCA, 0x70, 0x4E, 0x48, + 0xF2, 0x53, 0xF1, 0x7A, 0xFB, 0x2A, 0xDB, 0x5F, 0x5B, 0xE8, 0xFC, 0xCD, 0xC1, 0xF9, 0x8B, 0xF9, + 0x7B, 0x46, 0x30, 0xF6, 0x86, 0xF0, 0x37, 0x4F, 0x29, 0x79, 0xDF, 0x16, 0xA1, 0xF2, 0x10, 0x5E, + 0x51, 0xBF, 0x2B, 0xCB, 0xE5, 0x4F, 0x0B, 0xC6, 0xD6, 0x94, 0xBE, 0xA0, 0x8A, 0xD2, 0xAB, 0x7F, + 0x55, 0x15, 0x4E, 0x19, 0x13, 0xCA, 0xE9, 0x2F, 0xCF, 0x9A, 0xF9, 0xC1, 0x9C, 0x37, 0x7B, 0x0F, + 0x7F, 0x6B, 0x5B, 0xF7, 0xCB, 0x3F, 0x7D, 0x62, 0xC2, 0xFF, 0x12, 0xFF, 0x02, 0x1E, 0x7A, 0xF5, + 0x14, 0x00, 0x13, 0x00, 0x00 + }; + } +} \ No newline at end of file diff --git a/TakoTakoScripts/TJAConvert/FodyWeavers.xml b/TakoTakoScripts/TJAConvert/FodyWeavers.xml new file mode 100644 index 0000000..5029e70 --- /dev/null +++ b/TakoTakoScripts/TJAConvert/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/TakoTakoScripts/TJAConvert/FodyWeavers.xsd b/TakoTakoScripts/TJAConvert/FodyWeavers.xsd new file mode 100644 index 0000000..05e92c1 --- /dev/null +++ b/TakoTakoScripts/TJAConvert/FodyWeavers.xsd @@ -0,0 +1,141 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/TakoTakoScripts/TJAConvert/Program.cs b/TakoTakoScripts/TJAConvert/Program.cs new file mode 100644 index 0000000..4338f0d --- /dev/null +++ b/TakoTakoScripts/TJAConvert/Program.cs @@ -0,0 +1,1141 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using NAudio.Vorbis; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using Newtonsoft.Json; +using SonicAudioLib.Archives; +using SonicAudioLib.CriMw; +using TakoTako.Common; +using VGAudio.Containers.Hca; +using VGAudio.Containers.Wave; + +namespace TJAConvert +{ + public static class Program + { + public static async Task Main(string[] args) + { + if (args.Length != 1) + { + Console.WriteLine("Pass in a tja directory"); + return; + } + + var directory = args[0]; + if (!Directory.Exists(directory)) + { + Console.WriteLine("This is not a valid tja directory"); + return; + } + + + var result = await Run(directory); + Console.OutputEncoding = Encoding.Unicode; + Console.WriteLine(result); + } + + /// 0 if pass, -1 if failed unexpectedly, -2 if invalid tja, -3 Timeout + public static async Task Run(string directory) + { + StringBuilder result = new StringBuilder(); + foreach (var tjaPath in Directory.EnumerateFiles(directory, "*.tja")) + { + var fileName = Path.GetFileNameWithoutExtension(tjaPath); + var truncatedName = fileName.Substring(0, Math.Min(fileName.Length, 10)); + if (truncatedName.Length != fileName.Length) + fileName = truncatedName + "..."; + + string realDirectory; + int namingAttempts = 0; + do + { + if (namingAttempts == 0) + realDirectory = Path.Combine(directory, $"{fileName} [GENERATED]"); + else + realDirectory = Path.Combine(directory, $"{fileName} {namingAttempts}[GENERATED]"); + namingAttempts++; + } while (File.Exists(realDirectory)); + + int intResult = -1; + try + { + intResult = await RunConversion(); + } + catch + { + } + + result.AppendLine($"{intResult}:{realDirectory}"); + + async Task RunConversion() + { + TJAMetadata metadata; + try + { + metadata = new TJAMetadata(tjaPath); + } + catch + { + return -2; + } + + var originalAudioPath = $"{directory}/{metadata.AudioPath}"; + + if (!File.Exists(originalAudioPath)) + return -2; + + var newDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var tempOutDirectory = Path.Combine(newDirectory, Guid.NewGuid().ToString()); + if (Directory.Exists(tempOutDirectory)) + Directory.Delete(tempOutDirectory, true); + Directory.CreateDirectory(tempOutDirectory); + + var passed = await TJAToFumens(metadata, tjaPath, tempOutDirectory); + + if (passed >= 0) passed = CreateMusicFile(metadata, tempOutDirectory) ? 0 : -1; + + var copyFilePath = Path.Combine(newDirectory, Path.GetFileName(originalAudioPath)); + File.Copy(originalAudioPath, copyFilePath); + + var audioExtension = Path.GetExtension(copyFilePath).TrimStart('.'); + switch (audioExtension.ToLowerInvariant()) + { + case "wav": + if (passed >= 0) passed = WavToACB(copyFilePath, tempOutDirectory) ? 0 : -1; + break; + case "ogg": + if (passed >= 0) passed = OGGToACB(copyFilePath, tempOutDirectory) ? 0 : -1; + break; + default: + Console.WriteLine($"Do not support {audioExtension} audio files"); + passed = -4; + break; + } + + if (passed >= 0) + { + if (Directory.Exists(realDirectory)) + Directory.Delete(realDirectory, true); + Directory.CreateDirectory(realDirectory); + foreach (var filePath in Directory.EnumerateFiles(tempOutDirectory)) + { + var extension = Path.GetExtension(filePath).Trim('.'); + if (extension != "bin" && extension != "json") + continue; + var copyFileName = Path.GetFileName(filePath); + File.Copy(filePath, Path.Combine(realDirectory, copyFileName)); + } + } + + if (Directory.Exists(tempOutDirectory)) + Directory.Delete(tempOutDirectory, true); + + return passed; + } + } + + return result.ToString(); + } + + private static void Pack(string path) + { + const int bufferSize = 4096; + string acbPath = path + ".acb"; + + if (!File.Exists(acbPath)) + throw new FileNotFoundException("Unable to locate the corresponding ACB file. Please ensure that it's in the same directory."); + + CriTable acbFile = new CriTable(); + acbFile.Load(acbPath, bufferSize); + + CriAfs2Archive afs2Archive = new CriAfs2Archive(); + + CriCpkArchive cpkArchive = new CriCpkArchive(); + CriCpkArchive extCpkArchive = new CriCpkArchive(); + cpkArchive.Mode = extCpkArchive.Mode = CriCpkMode.Id; + + using (CriTableReader reader = CriTableReader.Create((byte[]) acbFile.Rows[0]["WaveformTable"])) + { + while (reader.Read()) + { + ushort id = reader.ContainsField("MemoryAwbId") ? reader.GetUInt16("MemoryAwbId") : reader.GetUInt16("Id"); + + string inputName = id.ToString("D5"); + + inputName += ".hca"; + inputName = Path.Combine(path, inputName); + + if (!File.Exists(inputName)) + throw new FileNotFoundException($"Unable to locate {inputName}"); + + CriAfs2Entry entry = new CriAfs2Entry + { + FilePath = new FileInfo(inputName), + Id = id + }; + afs2Archive.Add(entry); + } + } + + acbFile.Rows[0]["AwbFile"] = null; + acbFile.Rows[0]["StreamAwbAfs2Header"] = null; + + if (afs2Archive.Count > 0 || cpkArchive.Count > 0) + acbFile.Rows[0]["AwbFile"] = afs2Archive.Save(); + + acbFile.WriterSettings = CriTableWriterSettings.Adx2Settings; + acbFile.Save(acbPath, bufferSize); + } + + private static bool CreateMusicFile(TJAMetadata metadata, string outputPath) + { + try + { + var musicInfo = new CustomSong + { + uniqueId = metadata.Title.GetHashCode(), + id = metadata.Id, + order = 0, + genreNo = (int) metadata.Genre, + branchEasy = false, + branchNormal = false, + branchHard = false, + branchMania = false, + branchUra = false, + previewPos = (int) (metadata.PreviewTime * 1000), + fumenOffsetPos = (int) (metadata.Offset * 10), + songName = new TextEntry() + { + text = metadata.Title, + font = GetFontForText(metadata.Title), + jpText = metadata.TitleJA, + jpFont = string.IsNullOrWhiteSpace(metadata.TitleJA) ? 0 : GetFontForText(metadata.TitleJA), + enText = metadata.TitleEN, + enFont = string.IsNullOrWhiteSpace(metadata.TitleEN) ? 0 : GetFontForText(metadata.TitleEN), + scText = metadata.TitleCN, + scFont = string.IsNullOrWhiteSpace(metadata.TitleCN) ? 0 : GetFontForText(metadata.TitleCN), + tcText = metadata.TitleTW, + tcFont = string.IsNullOrWhiteSpace(metadata.TitleTW) ? 0 : GetFontForText(metadata.TitleTW), + krText = metadata.TitleKO, + krFont = string.IsNullOrWhiteSpace(metadata.TitleKO) ? 0 : GetFontForText(metadata.TitleKO), + }, + songSubtitle = new TextEntry() + { + text = metadata.Subtitle, + font = GetFontForText(metadata.Subtitle), + jpText = metadata.SubtitleJA, + jpFont = string.IsNullOrWhiteSpace(metadata.SubtitleJA) ? 0 : GetFontForText(metadata.SubtitleJA), + enText = metadata.SubtitleEN, + enFont = string.IsNullOrWhiteSpace(metadata.SubtitleEN) ? 0 : GetFontForText(metadata.SubtitleEN), + scText = metadata.SubtitleCN, + scFont = string.IsNullOrWhiteSpace(metadata.SubtitleCN) ? 0 : GetFontForText(metadata.SubtitleCN), + tcText = metadata.SubtitleTW, + tcFont = string.IsNullOrWhiteSpace(metadata.SubtitleTW) ? 0 : GetFontForText(metadata.SubtitleTW), + krText = metadata.SubtitleKO, + krFont = string.IsNullOrWhiteSpace(metadata.SubtitleKO) ? 0 : GetFontForText(metadata.SubtitleKO), + }, + songDetail = new TextEntry() + { + text = metadata.Detail, + font = GetFontForText(metadata.Detail), + jpText = metadata.DetailJA, + jpFont = string.IsNullOrWhiteSpace(metadata.DetailJA) ? 0 : GetFontForText(metadata.DetailJA), + enText = metadata.DetailEN, + enFont = string.IsNullOrWhiteSpace(metadata.DetailEN) ? 0 : GetFontForText(metadata.DetailEN), + scText = metadata.DetailCN, + scFont = string.IsNullOrWhiteSpace(metadata.DetailCN) ? 0 : GetFontForText(metadata.DetailCN), + tcText = metadata.DetailTW, + tcFont = string.IsNullOrWhiteSpace(metadata.DetailTW) ? 0 : GetFontForText(metadata.DetailTW), + krText = metadata.DetailKO, + krFont = string.IsNullOrWhiteSpace(metadata.DetailKO) ? 0 : GetFontForText(metadata.DetailKO), + }, + }; + + foreach (var course in metadata.Courses) + { + //todo figure out the best score? + switch (course.CourseType) + { + case CourseType.Easy: + musicInfo.starEasy = course.Level; + musicInfo.shinutiEasy = 10170; + musicInfo.shinutiEasyDuet = 10170; + musicInfo.scoreEasy = 360090; + musicInfo.branchEasy = course.IsBranching; + break; + case CourseType.Normal: + musicInfo.starNormal = course.Level; + musicInfo.shinutiNormal = 6010; + musicInfo.shinutiNormalDuet = 6010; + musicInfo.scoreNormal = 650150; + musicInfo.branchNormal = course.IsBranching; + break; + case CourseType.Hard: + musicInfo.starHard = course.Level; + musicInfo.shinutiHard = 3010; + musicInfo.shinutiHardDuet = 3010; + musicInfo.scoreHard = 800210; + musicInfo.branchHard = course.IsBranching; + break; + case CourseType.Oni: + musicInfo.starMania = course.Level; + musicInfo.shinutiMania = 1000; + musicInfo.shinutiManiaDuet = 1000; + musicInfo.scoreMania = 10000; + musicInfo.branchMania = course.IsBranching; + break; + case CourseType.UraOni: + musicInfo.starUra = course.Level; + musicInfo.shinutiUra = 1000; + musicInfo.shinutiUraDuet = 1000; + musicInfo.scoreUra = 10000; + musicInfo.branchUra = course.IsBranching; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var json = JsonConvert.SerializeObject(musicInfo, Formatting.Indented); + File.WriteAllText($"{outputPath}/data.json", json); + return true; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + private static async Task TJAToFumens(TJAMetadata metadata, string tjaPath, string outputPath) + { + var fileName = Path.GetFileName(tjaPath); + var newPath = Path.Combine(outputPath, fileName); + + // copy the file here in case we need to make any edits to it + if (File.Exists(newPath)) + File.Delete(newPath); + + File.Copy(tjaPath, newPath); + + int passed = 0; + // If this TJA has Ura Oni + if (metadata.Courses.Any(x => x.CourseType == CourseType.UraOni)) + { + // tja2bin doesn't support Ura Oni, so rip it out and change the course type to oni, then rename the final file + passed = await ConvertUraOni(metadata, newPath); + if (passed < 0) + return passed; + // for every .bin in this directory, we can now add the prefix _x + + foreach (var filePath in Directory.EnumerateFiles(outputPath, "*.bin")) + { + var binFileName = Path.GetFileName(filePath); + var binDirectory = Path.GetDirectoryName(filePath); + + binFileName = binFileName + .Replace("_m_1.bin", "_x_1_.bin") + .Replace("_m_2.bin", "_x_2_.bin") + .Replace("_m.bin", "_x.bin"); + + File.Move(filePath, $"{binDirectory}/{binFileName}"); + } + + try + { + metadata = new TJAMetadata(newPath); + } + catch + { + return -2; + } + } + + // If this TJA has doubles, then rip them out out + if (metadata.Courses.Any(x => x.PlayStyle == TJAMetadata.PlayStyle.Double)) + { + // will need to create additional files to splice them out + + passed = await SpliceDoubles(metadata, newPath); + if (passed < 0) + return passed; + + try + { + metadata = new TJAMetadata(newPath); + } + catch + { + return -2; + } + } + + if (metadata.Courses.All(x => x.PlayStyle != TJAMetadata.PlayStyle.Double)) + passed = await Convert(newPath, outputPath); + + if (passed < 0) + return passed; + + var fumenFiles = Directory.EnumerateFiles(outputPath, "*.bin").ToList().Where(x => !x.StartsWith("song_")).ToList(); + if (fumenFiles.Count == 0) + { + Console.WriteLine($"Failed to create Fumens for {fileName}"); + return -1; + } + + return passed; + } + + private static async Task ConvertUraOni(TJAMetadata metadata, string newPath) + { + var directory = Path.GetDirectoryName(newPath); + var fileName = Path.GetFileNameWithoutExtension(newPath); + var lines = File.ReadAllLines(newPath).ToList(); + + int courseStartIndex = lines.FindLastIndex(x => + { + var match = TJAMetadata.TJAKeyValueRegex.Match(x); + if (!match.Success) + return false; + + var type = match.Groups["KEY"].Value; + return TJAMetadata.MainMetadataKeys.Contains(type.ToUpperInvariant()); + }); + + var metaDataLines = lines.Take(courseStartIndex + 1).ToList(); + var courses = metadata.Courses.Where(x => x.CourseType == CourseType.UraOni).Reverse(); + + foreach (var course in courses) + { + if (course.PlayStyle == TJAMetadata.PlayStyle.Single) + { + var file = new List(metaDataLines); + file.Add(""); + file.AddRange(course.MetadataToTJA(courseTypeOverride: CourseType.Oni)); + file.Add(""); + file.AddRange(lines.GetRange(course.SongDataIndexStart, course.SongDataIndexEnd - course.SongDataIndexStart + 1)); + + var path = $"{directory}/{fileName}.tja"; + File.WriteAllLines(path, file); + + var passed = await Convert(path, directory); + if (passed < 0) + return passed; + + lines.RemoveRange(course.CourseDataIndexStart, course.SongDataIndexEnd - course.CourseDataIndexStart + 1); + } + else + { + var passed = await SplitP1P2(lines, course, directory, fileName, CourseType.Oni); + if (passed < 0) + return passed; + } + } + + File.WriteAllLines(newPath, lines); + + return 0; + } + + /// + /// This aims to separate P1 and P2 tracks for TJA2BIN to read + /// + private static async Task SpliceDoubles(TJAMetadata metadata, string newPath) + { + var directory = Path.GetDirectoryName(newPath); + var fileName = Path.GetFileNameWithoutExtension(newPath); + var lines = File.ReadAllLines(newPath).ToList(); + + // first thing to do is inject missing metadata + for (int i = metadata.Courses.Count - 1; i >= 0; i--) + { + var course = metadata.Courses[i]; + lines.RemoveRange(course.CourseDataIndexStart, course.CourseDataIndexEnd - course.CourseDataIndexStart); + lines.Insert(course.CourseDataIndexStart, ""); + var courseData = course.MetadataToTJA(); + lines.InsertRange(course.CourseDataIndexStart + 1, courseData); + lines.Insert(course.CourseDataIndexStart + 1 + courseData.Count, ""); + } + + File.WriteAllLines(newPath, lines); + + try + { + metadata = new TJAMetadata(newPath); + } + catch + { + return -2; + } + + var doubleCourses = metadata.Courses.Where(x => x.PlayStyle == TJAMetadata.PlayStyle.Double).Reverse(); + + // remove doubles section + foreach (var course in doubleCourses) + { + var passed = await SplitP1P2(lines, course, directory, fileName); + if (passed < 0) + return passed; + } + + File.WriteAllLines(newPath, lines); + return 0; + } + + private static async Task SplitP1P2(List lines, TJAMetadata.Course course, string directory, string fileName, CourseType? courseTypeOverride = null) + { + // metadata end + int courseStartIndex = lines.FindLastIndex(x => + { + var match = TJAMetadata.TJAKeyValueRegex.Match(x); + if (!match.Success) + return false; + + var type = match.Groups["KEY"].Value; + return TJAMetadata.MainMetadataKeys.Contains(type.ToUpperInvariant()); + }); + var metaDataLines = lines.Take(courseStartIndex + 1).ToList(); + + var startSongP1Index = lines.FindIndex(course.CourseDataIndexEnd, x => x.StartsWith("#START P1", StringComparison.InvariantCultureIgnoreCase)); + if (startSongP1Index < 0) + return -1; + var endP1Index = lines.FindIndex(startSongP1Index, x => x.StartsWith("#END", StringComparison.InvariantCultureIgnoreCase)); + if (endP1Index < 0) + return -1; + + var startSongP2Index = lines.FindIndex(endP1Index, x => x.StartsWith("#START P2", StringComparison.InvariantCultureIgnoreCase)); + if (startSongP2Index < 0) + return -1; + var endP2Index = lines.FindIndex(startSongP2Index, x => x.StartsWith("#END", StringComparison.InvariantCultureIgnoreCase)); + if (endP2Index < 0) + return -1; + + // otherwise create new files + var p1File = new List(metaDataLines); + p1File.AddRange(course.MetadataToTJA(TJAMetadata.PlayStyle.Single, courseTypeOverride)); + p1File.AddRange(lines.GetRange(startSongP1Index, endP1Index - startSongP1Index + 1)); + RemoveP1P2(p1File); + + var path = $"{directory}/{fileName}_1.tja"; + File.WriteAllLines(path, p1File); + + var passed = await Convert(path, directory); + if (passed < 0) + return passed; + + var p2File = new List(metaDataLines); + p2File.AddRange(course.MetadataToTJA(TJAMetadata.PlayStyle.Single, courseTypeOverride)); + p2File.AddRange(lines.GetRange(startSongP2Index, endP2Index - startSongP2Index + 1)); + RemoveP1P2(p2File); + + path = $"{directory}/{fileName}_2.tja"; + File.WriteAllLines(path, p2File); + + passed = await Convert(path, directory); + if (passed < 0) + return passed; + + lines.RemoveRange(course.CourseDataIndexStart, course.SongDataIndexEnd - course.CourseDataIndexStart + 1); + return 0; + } + + private static void RemoveP1P2(List playerLines) + { + for (var i = 0; i < playerLines.Count; i++) + { + var line = playerLines[i]; + if (line.StartsWith("#START", StringComparison.InvariantCultureIgnoreCase)) + playerLines[i] = "#START"; + } + } + + private static async Task Convert(string tjaPath, string outputPath) + { + var fileName = Path.GetFileNameWithoutExtension(tjaPath); + + TJAMetadata metadata; + try + { + metadata = new TJAMetadata(tjaPath); + } + catch + { + return -2; + } + + var newPath = $"{outputPath}\\{Path.GetFileName(tjaPath)}"; + if (metadata.Courses.Count == 1) + { + var coursePostfix = metadata.Courses[0].CourseType.ToShort(); + if (fileName.EndsWith("_1")) + newPath = $"{outputPath}\\{fileName.Substring(0, fileName.Length - 2)}_{coursePostfix}_1.tja"; + else if (fileName.EndsWith("_2")) + newPath = $"{outputPath}\\{fileName.Substring(0, fileName.Length - 2)}_{coursePostfix}_2.tja"; + else + newPath = $"{outputPath}\\{fileName}_{coursePostfix}.tja"; + } + + var lines = ApplyGeneralFixes(File.ReadAllLines(tjaPath).ToList()); + File.WriteAllLines(newPath, lines); + + var currentDirectory = Environment.CurrentDirectory; + var exePath = $"{currentDirectory}/tja2bin.exe"; + if (!File.Exists(exePath)) + { + Console.WriteLine($"Cannot find tja2bin at {exePath}"); + return -1; + } + + var timeStamp = Guid.NewGuid().ToString(); + bool isUsingTempFilePath = false; + try + { + int attempts = 30; + string result = string.Empty; + + do + { + ProcessStartInfo info = new ProcessStartInfo() + { + FileName = exePath, + Arguments = $"\"{newPath}\"", + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + RedirectStandardOutput = true, + }; + + var process = new Process(); + process.StartInfo = info; + + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + var delayTask = Task.Delay(TimeSpan.FromSeconds(10), cancellationTokenSource.Token); + var runTask = RunProcess(); + + var taskResult = await Task.WhenAny(delayTask, runTask); + if (taskResult == delayTask) + { + // tja2bin can sometimes have memory leak + if (!process.HasExited) + { + process.Kill(); + return -3; + } + } + + attempts--; + + // todo: Not sure how to solve this, so ignore it for now + if (result.Contains("branches must have same measure count") || result.Contains("invalid #BRANCHSTART")) + return -2; + + async Task RunProcess() + { + process.Start(); + result = await process.StandardOutput.ReadToEndAsync(); + } + } while (FailedAndCanRetry(result) && attempts > 0); + + if (isUsingTempFilePath) + { + foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(newPath))) + { + var tempFileName = Path.GetFileName(file); + var newName = $"{outputPath}\\{tempFileName}".Replace(timeStamp.ToString(), fileName); + if (File.Exists(newName)) + File.Delete(newName); + File.Move(file, newName); + } + + Directory.Delete(Path.GetDirectoryName(newPath)); + if (File.Exists($"{outputPath}\\{fileName}.tja")) + File.Delete($"{outputPath}\\{fileName}.tja"); + } + else + { + File.Delete(newPath); + } + + return 0; + } + catch (Exception e) + { + Console.WriteLine(e); + return -1; + } + + List ApplyGeneralFixes(List lines) + { + var noScoreInitRegex = new Regex("SCOREINIT:\\s*$", RegexOptions.CultureInvariant); + var noScoreDiffRegex = new Regex("SCOREDIFF:\\s*$", RegexOptions.CultureInvariant); + lines = lines + // get rid of comments + .Select(x => + { + x = x.Trim(); + var commentIndex = x.IndexOf("//", StringComparison.Ordinal); + if (commentIndex >= 0) + x = x.Substring(0, commentIndex); + + // remove unwanted characters in tracks + if (x.Trim().EndsWith(",")) + x = Regex.Replace(x, "[^\\d,ABF]", ""); + + // if there's no scores, give them an arbitrary score + if (noScoreInitRegex.IsMatch(x)) + return "SCOREINIT:440"; + if (noScoreDiffRegex.IsMatch(x)) + return "SCOREDIFF:113"; + + // I saw a few typos in a bunch of tjas, so I added a fix for it here, lol + return x.Replace("##", "#").Replace("#SCROOL", "#SCROLL").Replace("#SCROLLL", "#SCROLL").Replace("#SRCOLL", "#SCROLL").Replace("#MEAUSRE", "#MEASURE").Trim(); + }) + // remove lyrics, sections and delays as they are not supported by tja2bin + .Where(x => !( + x.StartsWith("#LYRIC", StringComparison.InvariantCultureIgnoreCase) + || x.StartsWith("#DELAY", StringComparison.InvariantCultureIgnoreCase) + || x.StartsWith("#MAKER", StringComparison.InvariantCultureIgnoreCase) + || x.StartsWith("#SECTION", StringComparison.InvariantCultureIgnoreCase))) + .ToList(); + return lines; + } + + bool FailedAndCanRetry(string result) + { + metadata = new TJAMetadata(newPath); + if (result.Contains("too many balloon notes")) + { + // one of the fumens does not have the correct amount of balloon amounts + var currentLines = File.ReadLines(newPath).ToList(); + var problematicCourse = GetCourseWithProblems(); + + // if there are two 9s close to each other, remove the second one + for (int i = problematicCourse.SongDataIndexStart; i < problematicCourse.SongDataIndexEnd; i++) + { + // if we find a 9 in a song, make sure there isn't another within this line or the next + var line = currentLines[i].Trim(); + if (line.EndsWith(",", StringComparison.InvariantCultureIgnoreCase)) + { + var index = line.IndexOf("9", StringComparison.Ordinal); + if (index < 0) continue; + + var nextIndex = line.IndexOf("9", index + 1, StringComparison.Ordinal); + if (nextIndex > 0) + { + TryReplace(line, i, nextIndex); + } + else + { + // check the next line + for (int j = 1; j <= 1; j++) + { + var lineAhead = currentLines[i + j]; + nextIndex = lineAhead.IndexOf("9", 0, StringComparison.Ordinal); + if (nextIndex < 0) + continue; + + TryReplace(lineAhead, i + j, nextIndex); + } + } + + void TryReplace(string currentLine, int linesIndex, int searchStartIndex) + { + currentLine = currentLine.Remove(searchStartIndex, 1); + currentLine = currentLine.Insert(searchStartIndex, "0"); + currentLines[linesIndex] = currentLine; + } + } + } + + var balloonLine = currentLines.FindIndex(problematicCourse.CourseDataIndexStart, x => x.StartsWith("balloon:", StringComparison.InvariantCultureIgnoreCase)); + if (balloonLine < 0) + { + // dunno stop here + return false; + } + + // check to see if the balloon count matches up with the amount of 7 + var balloonMatches = Regex.Matches(currentLines[balloonLine], "(\\d+)"); + + List currentNumbers = new List(); + foreach (Match match in balloonMatches) + currentNumbers.Add(int.Parse(match.Value)); + + int balloons8 = 0; + int balloons79 = 0; + // bug perhaps 7|9 instead of 8? + Regex balloonRegex = new Regex("^.*(7|8|9).*,.*$"); + for (int i = problematicCourse.SongDataIndexStart; i < problematicCourse.SongDataIndexEnd; i++) + { + var line = currentLines[i]; + if (balloonRegex.IsMatch(line)) + { + balloons8 += line.Count(x => x is '8'); + balloons79 += line.Count(x => x is '7' or '9'); + } + } + + var balloons = Math.Max(balloons8, balloons79); + + if (balloons > currentNumbers.Count) + { + // since we're patching this, do whatever we want + var finalBalloonText = "BALLOON:"; + if (balloons >= currentNumbers.Count) + { + for (int i = currentNumbers.Count; i < balloons; i++) + currentNumbers.Add(4); + } + + finalBalloonText += string.Join(",", currentNumbers); + currentLines[balloonLine] = finalBalloonText; + } + + File.WriteAllLines(newPath, currentLines); + return true; + } + + if (result.Contains("need a #BRANCHEND")) + { + var currentLines = File.ReadLines(newPath).ToList(); + var problematicCourse = GetCourseWithProblems(); + + currentLines.Insert(problematicCourse.SongDataIndexEnd, "#BRANCHEND"); + File.WriteAllLines(newPath, currentLines); + return true; + } + + if (result.Contains("#E must be after the #N branch") || result.Contains("#M must be after the #E branch")) + { + var currentLines = File.ReadLines(newPath).ToList(); + // var problematicCourse = GetCourseWithProblems(); + + string currentBranch = ""; + int startOfBranch = -1; + int endOfBranch = -1; + var branches = new List<(string branch, int start, int end)>(); + + for (int i = 0; i < currentLines.Count; i++) + { + var line = currentLines[i]; + bool isBranchStart = line.StartsWith("#BRANCHSTART", StringComparison.InvariantCultureIgnoreCase); + + if (line.StartsWith("#BRANCHEND", StringComparison.InvariantCultureIgnoreCase) || isBranchStart) + { + if (!string.IsNullOrWhiteSpace(currentBranch)) + EndBranch(); + + currentBranch = ""; + // Order is N E M + if (branches.Count > 1) + { + // try to sort the order of branches + var branchesSorted = new List<(string branch, int start, int end)>(branches); + branchesSorted.Sort((x, y) => GetSortOrder(x.branch) - GetSortOrder(y.branch)); + var branchesSortedValue = new List>(); + + foreach (var branch in branchesSorted) + { + var text = currentLines.GetRange(branch.start, branch.end - branch.start); + // does this text actually have any music? found a case where a branch was empty /shrug + if (!text.Any(x => x.Contains(","))) + continue; + + branchesSortedValue.Add(text); + } + + for (int j = 0; j < branches.Count; j++) + { + var branch = branchesSorted[branchesSorted.Count - 1 - j]; + currentLines.RemoveRange(branch.start, branch.end - branch.start); + if (j < 3) + currentLines.InsertRange(branch.start, branchesSortedValue[j]); + } + } + + branches.Clear(); + } + + else if (line.Trim().Equals("#M", StringComparison.InvariantCultureIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(currentBranch)) + EndBranch(); + + currentBranch = "m"; + startOfBranch = i; + } + else if (line.Trim().Equals("#N", StringComparison.InvariantCultureIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(currentBranch)) + EndBranch(); + + currentBranch = "n"; + startOfBranch = i; + } + else if (line.Trim().Equals("#E", StringComparison.InvariantCultureIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(currentBranch)) + EndBranch(); + + currentBranch = "e"; + startOfBranch = i; + } + + int GetSortOrder(string branch) + { + switch (branch) + { + case "n": + return 1; + case "e": + return 2; + case "m": + return 3; + } + + return 0; + } + + void EndBranch() + { + endOfBranch = i; + if (!string.IsNullOrEmpty(currentBranch)) + branches.Add((currentBranch, startOfBranch, endOfBranch)); + } + } + + File.WriteAllLines(newPath, currentLines); + return true; + } + + if (result.Contains("unexpected EOF") && !newPath.Contains(timeStamp.ToString())) + { + // perhaps the files name is weird? let's rename it, and move this to a new folder + var newDirectory = Path.GetTempPath() + "\\" + timeStamp; + var fileName = timeStamp + ".tja"; + var tempFilePath = Path.Combine(newDirectory, fileName); + Directory.CreateDirectory(newDirectory); + File.Copy(newPath, tempFilePath); + newPath = tempFilePath; + isUsingTempFilePath = true; + return true; + } + + if (result.Contains("missing score information")) + { + // Missing score! let's just to replace the existing one with whatever + for (int i = metadata.Courses.Count - 1; i >= 0; i--) + { + var course = metadata.Courses[i]; + + if (!string.IsNullOrWhiteSpace(course.Metadata.ScoreDiff) && !string.IsNullOrWhiteSpace(course.Metadata.ScoreInit)) + continue; + + course.Metadata.ScoreInit = "980"; + course.Metadata.ScoreDiff = "320"; + + lines.RemoveRange(course.CourseDataIndexStart, course.CourseDataIndexEnd - course.CourseDataIndexStart); + lines.Insert(course.CourseDataIndexStart, ""); + var newCourseData = course.MetadataToTJA(); + lines.InsertRange(course.CourseDataIndexStart + 1, newCourseData); + lines.Insert(course.CourseDataIndexStart + 1 + newCourseData.Count, ""); + File.WriteAllLines(newPath, lines); + } + + return true; + } + + return false; + + TJAMetadata.Course GetCourseWithProblems() + { + var courses = new List(metadata.Courses); + // step 1. find the troublesome course + var resultLines = result.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in resultLines) + { + if (line.Contains("_x.bin")) + courses.RemoveAll(x => x.CourseType == CourseType.UraOni); + if (line.Contains("_m.bin")) + courses.RemoveAll(x => x.CourseType == CourseType.Oni); + if (line.Contains("_h.bin")) + courses.RemoveAll(x => x.CourseType == CourseType.Hard); + if (line.Contains("_n.bin")) + courses.RemoveAll(x => x.CourseType == CourseType.Normal); + if (line.Contains("_e.bin")) + courses.RemoveAll(x => x.CourseType == CourseType.Easy); + } + + if (courses.Count == 0) + { + // dunno stop here + return null; + } + + return courses[0]; + } + } + } + + private static bool OGGToACB(string oggPath, string outDirectory) + { + try + { + var directory = Path.GetDirectoryName(oggPath); + var fileName = Path.GetFileNameWithoutExtension(oggPath); + var acbPath = $"{directory}/{Guid.NewGuid().ToString()}"; + Directory.CreateDirectory(acbPath); + + using MemoryStream stream = new MemoryStream(Files.TemplateACBData); + using var decompressor = new GZipStream(stream, CompressionMode.Decompress); + using (FileStream compressedFileStream = File.Create($"{acbPath}.acb")) + decompressor.CopyTo(compressedFileStream); + + var hca = OggToHca(oggPath); + if (hca == null) + return false; + + File.WriteAllBytes($"{acbPath}/00000.hca", hca); + Pack(acbPath); + if (File.Exists($"{outDirectory}/song_{fileName}.bin")) + File.Delete($"{outDirectory}/song_{fileName}.bin"); + + File.Move($"{acbPath}.acb", $"{outDirectory}/song_{fileName}.bin"); + Directory.Delete(acbPath, true); + return true; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + private static bool WavToACB(string wavPath, string outDirectory, bool deleteWav = false) + { + try + { + var directory = Path.GetDirectoryName(wavPath); + var fileName = Path.GetFileNameWithoutExtension(wavPath); + var acbPath = $"{directory}/{Guid.NewGuid().ToString()}"; + Directory.CreateDirectory(acbPath); + + using MemoryStream stream = new MemoryStream(Files.TemplateACBData); + using var decompressor = new GZipStream(stream, CompressionMode.Decompress); + using (FileStream compressedFileStream = File.Create($"{acbPath}.acb")) + decompressor.CopyTo(compressedFileStream); + + var hca = WavToHca(wavPath); + File.WriteAllBytes($"{acbPath}/00000.hca", hca); + Pack(acbPath); + if (File.Exists($"{outDirectory}/song_{fileName}.bin")) + File.Delete($"{outDirectory}/song_{fileName}.bin"); + + File.Move($"{acbPath}.acb", $"{outDirectory}/song_{fileName}.bin"); + + if (deleteWav) + File.Delete(wavPath); + Directory.Delete(acbPath, true); + return true; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + private static byte[] WavToHca(string path) + { + var hcaWriter = new HcaWriter(); + var waveReader = new WaveReader(); + var audioData = waveReader.Read(File.ReadAllBytes(path)); + return hcaWriter.GetFile(audioData); + } + + private static byte[] OggToHca(string inPath) + { + try + { + using FileStream fileIn = new FileStream(inPath, FileMode.Open); + var vorbis = new VorbisWaveReader(fileIn); + var memoryStream = new MemoryStream(); + WaveFileWriter.WriteWavFileToStream(memoryStream, new SampleToWaveProvider16(vorbis)); + + var hcaWriter = new HcaWriter(); + var waveReader = new WaveReader(); + var audioData = waveReader.Read(memoryStream.ToArray()); + return hcaWriter.GetFile(audioData); + } + catch (Exception e) + { + Console.WriteLine(e); + return null; + } + } + + private static bool OggToWav(string inPath, string outPath) + { + try + { + using FileStream fileIn = new FileStream(inPath, FileMode.Open); + var vorbis = new VorbisWaveReader(fileIn); + WaveFileWriter.CreateWaveFile16(outPath, vorbis); + return true; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + private static IEnumerable GetCharsInRange(string text, int min, int max) + { + return text.Where(e => e >= min && e <= max); + } + + private static int GetFontForText(string keyword) + { + if (string.IsNullOrEmpty(keyword)) + return 1; + + var hiragana = GetCharsInRange(keyword, 0x3040, 0x309F).Any(); + var katakana = GetCharsInRange(keyword, 0x30A0, 0x30FF).Any(); + var kanji = GetCharsInRange(keyword, 0x4E00, 0x9FBF).Any(); + + if (hiragana || katakana || kanji) + return 0; + + var hangulJamo = GetCharsInRange(keyword, 0x1100, 0x11FF).Any(); + var hangulSyllables = GetCharsInRange(keyword, 0xAC00, 0xD7A3).Any(); + var hangulCompatibility = GetCharsInRange(keyword, 0x3130, 0x318F).Any(); + var hangulExtendedA = GetCharsInRange(keyword, 0xA960, 0xA97F).Any(); + var hangulExtendedB = GetCharsInRange(keyword, 0xD7B0, 0xD7FF).Any(); + + if (hangulJamo + || hangulSyllables + || hangulCompatibility + || hangulExtendedA + || hangulExtendedB) + return 4; + + var ascii = GetCharsInRange(keyword, 0x0041, 0x005A).Any(); + var ascii2 = GetCharsInRange(keyword, 0x0061, 0x007A).Any(); + if (ascii || ascii2) + return 1; + + // don't know how to distinguish between simplified and traditional chinese... sorry :( + return 3; + } + } +} diff --git a/TakoTakoScripts/TJAConvert/References/SonicAudioLib.dll b/TakoTakoScripts/TJAConvert/References/SonicAudioLib.dll new file mode 100644 index 0000000..97b1b0a Binary files /dev/null and b/TakoTakoScripts/TJAConvert/References/SonicAudioLib.dll differ diff --git a/TakoTakoScripts/TJAConvert/TJAConvert.csproj b/TakoTakoScripts/TJAConvert/TJAConvert.csproj new file mode 100644 index 0000000..1331e0c --- /dev/null +++ b/TakoTakoScripts/TJAConvert/TJAConvert.csproj @@ -0,0 +1,44 @@ + + + + Exe + net48 + default + true + win10-x64 + true + + + + + + + + + References\SonicAudioLib.dll + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/TakoTakoScripts/TJAConvert/TJAMetaData.cs b/TakoTakoScripts/TJAConvert/TJAMetaData.cs new file mode 100644 index 0000000..6825afc --- /dev/null +++ b/TakoTakoScripts/TJAConvert/TJAMetaData.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace TJAConvert; + +internal class TJAMetadata +{ + public string Id; + public string Title; + public string TitleJA; + public string TitleEN; + public string TitleCN; + public string TitleTW; + public string TitleKO; + + public string Detail; +// dunno what to put in here +#pragma warning disable CS0649 + public string DetailJA; + public string DetailEN; + public string DetailCN; + public string DetailTW; + public string DetailKO; +#pragma warning restore CS0649 + + public string Subtitle; + public string SubtitleJA; + public string SubtitleEN; + public string SubtitleCN; + public string SubtitleTW; + public string SubtitleKO; + + public string AudioPath; + public float Offset; + public float PreviewTime; + public SongGenre Genre; + + public List Courses = new(); + + public TJAMetadata(string tjaPath) + { + Id = Path.GetFileNameWithoutExtension(tjaPath); + + var lines = File.ReadLines(tjaPath).ToList(); + + Title = FindAndGetField("TITLE"); + TitleJA = FindAndGetField("TITLEJA"); + TitleEN = FindAndGetField("TITLEEN"); + TitleCN = FindAndGetField("TITLECN"); + TitleTW = FindAndGetField("TITLETW"); + TitleKO = FindAndGetField("TITLEKO"); + + Detail = FindAndGetField("MAKER"); + + ModifySubtitle("SUBTITLE", x => Subtitle = x); + ModifySubtitle("SUBTITLEJA", x => SubtitleJA = x); + ModifySubtitle("SUBTITLEEN", x => SubtitleEN = x); + ModifySubtitle("SUBTITLECN", x => SubtitleCN = x); + ModifySubtitle("SUBTITLETW", x => SubtitleTW = x); + ModifySubtitle("SUBTITLEKO", x => SubtitleKO = x); + + void ModifySubtitle(string key, Action setSubtitle) + { + var entry = FindAndGetField(key); + if (string.IsNullOrEmpty(entry)) + return; + + var subtitle = FindAndGetField(key).TrimStart('-', '+'); + setSubtitle(subtitle); + } + + AudioPath = FindAndGetField("WAVE"); + Offset = float.Parse(FindAndGetField("OFFSET")); + PreviewTime = float.Parse(FindAndGetField("DEMOSTART")); + + var genreEntry = FindAndGetField("GENRE"); + Genre = GetGenre(genreEntry); + + // start finding courses + Course currentCourse = new Course(); + // find the last metadata entry + int courseStartIndex = lines.FindLastIndex(x => + { + var match = TJAKeyValueRegex.Match(x); + if (!match.Success) + return false; + + var type = match.Groups["KEY"].Value; + return MainMetadataKeys.Contains(type.ToUpperInvariant()); + }); + // find where the track starts + int courseEndIndex = lines.FindIndex(courseStartIndex, x => x.StartsWith("#START", StringComparison.InvariantCultureIgnoreCase)); + + do + { + bool newContent = false; + for (int i = courseStartIndex + 1; i < courseEndIndex; i++) + { + var line = lines[i]; + var match = TJAKeyValueRegex.Match(line); + if (!match.Success) + continue; + + newContent = true; + var key = match.Groups["KEY"].Value.ToUpperInvariant().Trim(); + var value = match.Groups["VALUE"].Value.Trim(); + + switch (key) + { + case "COURSE": + { + currentCourse.CourseType = GetCourseType(value); + break; + } + case "LEVEL": + { + currentCourse.Level = int.Parse(value); + break; + } + case "STYLE": + { + currentCourse.PlayStyle = (PlayStyle) Enum.Parse(typeof(PlayStyle), value, true); + break; + } + } + + if (key.Equals(nameof(Course.OtherMetadata.Balloon).ToUpperInvariant())) currentCourse.Metadata.Balloon = value; + if (key.Equals(nameof(Course.OtherMetadata.ScoreInit).ToUpperInvariant())) currentCourse.Metadata.ScoreInit = value; + if (key.Equals(nameof(Course.OtherMetadata.ScoreDiff).ToUpperInvariant())) currentCourse.Metadata.ScoreDiff = value; + if (key.Equals(nameof(Course.OtherMetadata.BalloonNor).ToUpperInvariant())) currentCourse.Metadata.BalloonNor = value; + if (key.Equals(nameof(Course.OtherMetadata.BalloonExp).ToUpperInvariant())) currentCourse.Metadata.BalloonExp = value; + if (key.Equals(nameof(Course.OtherMetadata.BalloonMas).ToUpperInvariant())) currentCourse.Metadata.BalloonMas = value; + if (key.Equals(nameof(Course.OtherMetadata.Exam1).ToUpperInvariant())) currentCourse.Metadata.Exam1 = value; + if (key.Equals(nameof(Course.OtherMetadata.Exam2).ToUpperInvariant())) currentCourse.Metadata.Exam2 = value; + if (key.Equals(nameof(Course.OtherMetadata.Exam3).ToUpperInvariant())) currentCourse.Metadata.Exam3 = value; + if (key.Equals(nameof(Course.OtherMetadata.GaugeNcr).ToUpperInvariant())) currentCourse.Metadata.GaugeNcr = value; + if (key.Equals(nameof(Course.OtherMetadata.Total).ToUpperInvariant())) currentCourse.Metadata.Total = value; + if (key.Equals(nameof(Course.OtherMetadata.HiddenBranch).ToUpperInvariant())) currentCourse.Metadata.HiddenBranch = value; + } + + currentCourse.CourseDataIndexStart = courseStartIndex + 1; + currentCourse.CourseDataIndexEnd = courseEndIndex; + currentCourse.SongDataIndexStart = courseEndIndex; + + // find the next end + if (currentCourse.PlayStyle == PlayStyle.Double) + { + // go through p1 and p2 + var index = lines.FindIndex(courseEndIndex, x => x.StartsWith("#END", StringComparison.InvariantCultureIgnoreCase)); + courseStartIndex = lines.FindIndex(index + 1, x => x.StartsWith("#END", StringComparison.InvariantCultureIgnoreCase)); + } + else + { + courseStartIndex = lines.FindIndex(courseEndIndex, x => x.StartsWith("#END", StringComparison.InvariantCultureIgnoreCase)); + } + + currentCourse.SongDataIndexEnd = courseStartIndex; + + if (newContent) + Courses.Add(currentCourse); + + for (int i = currentCourse.SongDataIndexStart; i < currentCourse.SongDataIndexEnd; i++) + { + var line = lines[i]; + if (line.Contains("#BRANCH")) + { + currentCourse.IsBranching = true; + break; + } + } + + // duplicate the existing course + currentCourse = new Course(currentCourse); + // find the next start + courseEndIndex = lines.FindIndex(courseStartIndex, x => x.StartsWith("#START", StringComparison.InvariantCultureIgnoreCase)); + } while (courseEndIndex > 0); + + string FindAndGetField(string fieldName) + { + var tileRegex = new Regex(string.Format(TJAFieldRegexTemplate, fieldName), RegexOptions.IgnoreCase); + var index = lines.FindIndex(x => tileRegex.IsMatch(x)); + if (index < 0) + return null; + + return tileRegex.Match(lines[index]).Groups["VALUE"].Value; + } + + SongGenre GetGenre(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return SongGenre.Variety; + + switch (value.ToUpperInvariant()) + { + case "アニメ": + return SongGenre.Anime; + case "J-POP": + return SongGenre.Pop; + case "どうよう": + return SongGenre.Children; + case "バラエティ": + return SongGenre.Variety; + case "ボーカロイド": + case "VOCALOID": + return SongGenre.Vocaloid; + case "クラシック": + return SongGenre.Classic; + case "ゲームミュージック": + return SongGenre.Game; + case "ナムコオリジナル": + return SongGenre.Namco; + } + + return SongGenre.Variety; + } + + CourseType GetCourseType(string value) + { + switch (value.ToUpperInvariant()) + { + case "EASY": + case "0": + return CourseType.Easy; + case "NORMAL": + case "1": + return CourseType.Normal; + case "HARD": + case "2": + return CourseType.Hard; + case "ONI": + case "3": + return CourseType.Oni; + case "Edit": + case "4": + return CourseType.UraOni; + } + + return CourseType.UraOni; + } + + + } + + public const string TJAFieldRegexTemplate = "^{0}:\\s*(?.*?)\\s*$"; + public static Regex TJAKeyValueRegex = new("^(?.*?):\\s*(?.*?)\\s*$", RegexOptions.IgnoreCase); + + public static HashSet MainMetadataKeys = new() + { + "TITLE", + "TITLEEN", + "SUBTITLE", + "SUBTITLEEN", + "BPM", + "WAVE", + "OFFSET", + "DEMOSTART", + "GENRE", + "SCOREMODE", + "MAKER", + "LYRICS", + "SONGVOL", + "SEVOL", + "SIDE", + "LIFE", + "GAME", + "HEADSCROLL", + "BGIMAGE", + "BGMOVIE", + "MOVIEOFFSET", + "TAIKOWEBSKIN", + }; + + + public class Course + { + public CourseType CourseType = CourseType.Oni; + public int Level = 5; + public PlayStyle PlayStyle = PlayStyle.Single; + public bool IsBranching = false; + + public OtherMetadata Metadata = new(); + + public int CourseDataIndexStart; + public int CourseDataIndexEnd; + + public int SongDataIndexStart; + public int SongDataIndexEnd; + + public Course() + { + } + + public Course(Course course) + { + CourseType = course.CourseType; + Level = course.Level; + // PlayStyle = course.PlayStyle; + Metadata = new OtherMetadata(course.Metadata); + } + + public class OtherMetadata + { + public string Balloon; + public string ScoreInit; + public string ScoreDiff; + public string BalloonNor; + public string BalloonExp; + public string BalloonMas; + public string Exam1; + public string Exam2; + public string Exam3; + public string GaugeNcr; + public string Total; + public string HiddenBranch; + + public OtherMetadata() + { + } + + public OtherMetadata(OtherMetadata metadata) + { + Balloon = metadata.Balloon; + ScoreInit = metadata.ScoreInit; + ScoreDiff = metadata.ScoreDiff; + BalloonNor = metadata.BalloonNor; + BalloonExp = metadata.BalloonExp; + BalloonMas = metadata.BalloonMas; + Exam1 = metadata.Exam1; + Exam2 = metadata.Exam2; + Exam3 = metadata.Exam3; + GaugeNcr = metadata.GaugeNcr; + Total = metadata.Total; + HiddenBranch = metadata.HiddenBranch; + } + } + + public List MetadataToTJA(PlayStyle? playStyleOverride = null, CourseType? courseTypeOverride = null) + { + List result = new List(); + result.Add($"COURSE:{(courseTypeOverride ?? CourseType).ToString()}"); + result.Add($"LEVEL:{Level.ToString()}"); + result.Add($"STYLE:{(playStyleOverride ?? PlayStyle).ToString()}"); + + AddIfNotNull(nameof(OtherMetadata.Balloon), Metadata.Balloon); + AddIfNotNull(nameof(OtherMetadata.ScoreInit), Metadata.ScoreInit); + AddIfNotNull(nameof(OtherMetadata.ScoreDiff), Metadata.ScoreDiff); + AddIfNotNull(nameof(OtherMetadata.BalloonNor), Metadata.BalloonNor); + AddIfNotNull(nameof(OtherMetadata.BalloonExp), Metadata.BalloonExp); + AddIfNotNull(nameof(OtherMetadata.BalloonMas), Metadata.BalloonMas); + AddIfNotNull(nameof(OtherMetadata.Exam1), Metadata.Exam1); + AddIfNotNull(nameof(OtherMetadata.Exam2), Metadata.Exam2); + AddIfNotNull(nameof(OtherMetadata.Exam3), Metadata.Exam3); + AddIfNotNull(nameof(OtherMetadata.GaugeNcr), Metadata.GaugeNcr); + AddIfNotNull(nameof(OtherMetadata.Total), Metadata.Total); + AddIfNotNull(nameof(OtherMetadata.HiddenBranch), Metadata.HiddenBranch); + + void AddIfNotNull(string name, string value) + { + if (!string.IsNullOrWhiteSpace(value)) + result.Add($"{name.ToUpperInvariant()}:{value}"); + } + + return result; + } + } + + public enum PlayStyle + { + None = 0, + Single = 1, + Double = 2, + } + + + public enum SongGenre + { + Pop, + Anime, + Vocaloid, + Variety, + Children, + Classic, + Game, + Namco, + } +} + +public enum CourseType +{ + Easy, + Normal, + Hard, + Oni, + UraOni +} + +public static class CourseTypeExtensions +{ + public static string ToShort(this CourseType courseType) + { + return courseType switch + { + CourseType.Easy => "e", + CourseType.Normal => "n", + CourseType.Hard => "h", + CourseType.Oni => "m", + CourseType.UraOni => "x", + _ => throw new ArgumentOutOfRangeException(nameof(courseType), courseType, null) + }; + } +} diff --git a/TakoTakoScripts/TakoTako.Common/CustomSong.cs b/TakoTakoScripts/TakoTako.Common/CustomSong.cs new file mode 100644 index 0000000..224f14b --- /dev/null +++ b/TakoTakoScripts/TakoTako.Common/CustomSong.cs @@ -0,0 +1,204 @@ +using System; +using System.Runtime.Serialization; +using Newtonsoft.Json; + +// ReSharper disable InconsistentNaming + +namespace TakoTako.Common +{ + [DataContract(Name = "CustomSong")] + [Serializable] + public class CustomSong + { + // Song Details + [DataMember] public int uniqueId; + [DataMember] public string id; + [DataMember] public int order; + [DataMember] public int genreNo; + [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; + + [DataMember] public bool AreFilesGZipped; + + // 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; + } + + [Serializable] + public class TextEntry + { + /// + /// The text to display by default, if any override exist, the game will use that text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string text; + + /// + /// font for the default text, if any override exist, the game will use that text + /// 0 == Japanese + /// 1 == English + /// 2 == Traditional Chinese + /// 3 == Simplified Chinese + /// 4 == Korean + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int font; + + /// + /// 日本語 Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string jpText; + + /// + /// 日本語 Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int jpFont; + + /// + /// English Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string enText; + + /// + /// English Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int enFont; + + /// + /// Français Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string frText; + + /// + /// Français Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int frFont; + + /// + /// Italiano Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string itText; + + /// + /// Italiano Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int itFont; + + /// + /// Deutsch Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string deText; + + /// + /// Deutsch Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int deFont; + + /// + /// Español Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string esText; + + /// + /// Español Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int esFont; + + /// + /// 繁體中文 Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string tcText; + + /// + /// 繁體中文 Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int tcFont; + + /// + /// 简体中文 Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string scText; + + /// + /// 简体中文 Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int scFont; + + /// + /// 영어 Text + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string krText; + + /// + /// 영어 Font + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int krFont; + } +} diff --git a/TakoTakoScripts/TakoTako.Common/TakoTako.Common.csproj b/TakoTakoScripts/TakoTako.Common/TakoTako.Common.csproj new file mode 100644 index 0000000..93116fc --- /dev/null +++ b/TakoTakoScripts/TakoTako.Common/TakoTako.Common.csproj @@ -0,0 +1,16 @@ + + + + net48 + TakoTako.Common + TakoTako.Common + latest + + + + + ..\..\TakoTako\References\Newtonsoft.Json.dll + + + + diff --git a/TakoTakoScripts/TakoTakoScripts.Common/TakoTakoScripts.Common.csproj b/TakoTakoScripts/TakoTakoScripts.Common/TakoTakoScripts.Common.csproj new file mode 100644 index 0000000..93116fc --- /dev/null +++ b/TakoTakoScripts/TakoTakoScripts.Common/TakoTakoScripts.Common.csproj @@ -0,0 +1,16 @@ + + + + net48 + TakoTako.Common + TakoTako.Common + latest + + + + + ..\..\TakoTako\References\Newtonsoft.Json.dll + + + + diff --git a/TakoTakoScripts/TakoTakoScripts.sln b/TakoTakoScripts/TakoTakoScripts.sln new file mode 100644 index 0000000..62daa78 --- /dev/null +++ b/TakoTakoScripts/TakoTakoScripts.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakoTako.Common", "TakoTako.Common\TakoTako.Common.csproj", "{DDD2BC9B-15CB-47AE-A104-F45A3C65C7B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TJAConvert", "TJAConvert\TJAConvert.csproj", "{9ED2476B-FB39-4BE9-8661-21311AD9A3E8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DDD2BC9B-15CB-47AE-A104-F45A3C65C7B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDD2BC9B-15CB-47AE-A104-F45A3C65C7B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDD2BC9B-15CB-47AE-A104-F45A3C65C7B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDD2BC9B-15CB-47AE-A104-F45A3C65C7B1}.Release|Any CPU.Build.0 = Release|Any CPU + {9ED2476B-FB39-4BE9-8661-21311AD9A3E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9ED2476B-FB39-4BE9-8661-21311AD9A3E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9ED2476B-FB39-4BE9-8661-21311AD9A3E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9ED2476B-FB39-4BE9-8661-21311AD9A3E8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/TakoTakoScripts/TakoTakoScripts.sln.DotSettings b/TakoTakoScripts/TakoTakoScripts.sln.DotSettings new file mode 100644 index 0000000..0b1fe32 --- /dev/null +++ b/TakoTakoScripts/TakoTakoScripts.sln.DotSettings @@ -0,0 +1,7 @@ + + ACB + OGG + TJA + True + True + True \ No newline at end of file diff --git a/TakoTakoScripts/global.json b/TakoTakoScripts/global.json new file mode 100644 index 0000000..e5674e1 --- /dev/null +++ b/TakoTakoScripts/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "5.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/readme-image-0.png b/readme-image-0.png new file mode 100644 index 0000000..955a581 Binary files /dev/null and b/readme-image-0.png differ diff --git a/readme-image-1.png b/readme-image-1.png new file mode 100644 index 0000000..2bf79b1 Binary files /dev/null and b/readme-image-1.png differ diff --git a/readme-image-2.png b/readme-image-2.png new file mode 100644 index 0000000..aacf67a Binary files /dev/null and b/readme-image-2.png differ