diff --git a/.gitignore b/.gitignore index 1f732d5..bd476dd 100644 --- a/.gitignore +++ b/.gitignore @@ -396,5 +396,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.idea TakoTako/Executables/TJAConvert.exe \ No newline at end of file diff --git a/TakoTako/MusicPatch.cs b/TakoTako/MusicPatch.cs index f2a3bc6..255afc0 100644 --- a/TakoTako/MusicPatch.cs +++ b/TakoTako/MusicPatch.cs @@ -372,24 +372,36 @@ public class MusicPatch 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)) + // Clip off the last bit of the hash to make sure that the number is positive. This will lead to more collisions, but we should be fine. + if (isTjaSong) { - var uniqueIdTest = unchecked(song.id.GetHashCode() + song.previewPos + song.fumenOffsetPos); - while (uniqueIdToSong.ContainsKey(uniqueIdTest) || (uniqueIdTest >= 0 && uniqueIdTest <= SaveDataMax)) - uniqueIdTest = unchecked((uniqueIdTest + 1) * (uniqueIdTest + 1)); + // For TJAs, we need to hash the TJA file. + song.UniqueId = song.tjaFileHash; - song.uniqueId = uniqueIdTest; + if (song.UniqueId == 0) + throw new Exception("Converted TJA had no hash."); + } + else + { + // For official songs, we can just use the hash of the song internal name. + song.UniqueId = (int)(MurmurHash2.Hash(song.id) & 0xFFFF_FFF); + } + + if (song.UniqueId <= SaveDataMax) + song.UniqueId += SaveDataMax; + + if (uniqueIdToSong.ContainsKey(song.UniqueId)) + { + throw new Exception($"Song \"{song.id}\" has collision with \"{uniqueIdToSong[song.UniqueId].id}\", bailing out..."); } customSongsList.Add(song); idToSong[song.id] = song; - uniqueIdToSong[song.uniqueId] = song; - Log.LogInfo($"Added {(isTjaSong ? "TJA" : "")} Song {song.songName.text}"); + uniqueIdToSong[song.UniqueId] = song; + Log.LogInfo($"Added{(isTjaSong ? " TJA" : "")} Song {song.songName.text}({song.UniqueId})"); } } @@ -451,15 +463,15 @@ public class MusicPatch continue; musicInfoAccessors.Add(new MusicDataInterface.MusicInfoAccesser( - song.uniqueId, + song.UniqueId, // From SongInstance, as we always recalculate it now song.id, $"song_{song.id}", song.order, song.genreNo, - !Plugin.Instance.ConfigDisableCustomDLCSongs.Value, + true, // We always want to mark songs as DLC, otherwise ranked games will be broken as you are gonna match songs that other people don't have false, 0, false, - 0, + 2, // Always mark custom songs as "both players need to have this song to play it" new[] { song.branchEasy, @@ -1287,83 +1299,6 @@ public class MusicPatch #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 @@ -1965,5 +1900,6 @@ public class MusicPatch { public string FolderPath; public string SongName; + public int UniqueId; } -} +} \ No newline at end of file diff --git a/TakoTako/Plugin.cs b/TakoTako/Plugin.cs index 703bde2..0a099b8 100644 --- a/TakoTako/Plugin.cs +++ b/TakoTako/Plugin.cs @@ -20,7 +20,6 @@ namespace TakoTako public ConfigEntry ConfigSongDirectory; public ConfigEntry ConfigSaveEnabled; public ConfigEntry ConfigSaveDirectory; - public ConfigEntry ConfigDisableCustomDLCSongs; public ConfigEntry ConfigOverrideDefaultSongLanguage; public ConfigEntry ConfigApplyGenreOverride; @@ -64,12 +63,6 @@ namespace TakoTako $"{userFolder}/Documents/{typeof(Plugin).Namespace}/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, @@ -124,4 +117,4 @@ namespace TakoTako StartCoroutine(enumerator); } } -} +} \ No newline at end of file diff --git a/TakoTakoScripts/TJAConvert/Program.cs b/TakoTakoScripts/TJAConvert/Program.cs index 4338f0d..46a349a 100644 --- a/TakoTakoScripts/TJAConvert/Program.cs +++ b/TakoTakoScripts/TJAConvert/Program.cs @@ -99,9 +99,11 @@ namespace TJAConvert Directory.Delete(tempOutDirectory, true); Directory.CreateDirectory(tempOutDirectory); - var passed = await TJAToFumens(metadata, tjaPath, tempOutDirectory); + var originalTjaData = File.ReadAllBytes(tjaPath); + var tjaHash = (int)(MurmurHash2.Hash(originalTjaData) & 0xFFFF_FFF); - if (passed >= 0) passed = CreateMusicFile(metadata, tempOutDirectory) ? 0 : -1; + var passed = await TJAToFumens(metadata, tjaPath, tjaHash, tempOutDirectory); + if (passed >= 0) passed = CreateMusicFile(metadata, tjaHash, tempOutDirectory) ? 0 : -1; var copyFilePath = Path.Combine(newDirectory, Path.GetFileName(originalAudioPath)); File.Copy(originalAudioPath, copyFilePath); @@ -110,10 +112,10 @@ namespace TJAConvert switch (audioExtension.ToLowerInvariant()) { case "wav": - if (passed >= 0) passed = WavToACB(copyFilePath, tempOutDirectory) ? 0 : -1; + if (passed >= 0) passed = WavToACB(copyFilePath, tempOutDirectory, tjaHash) ? 0 : -1; break; case "ogg": - if (passed >= 0) passed = OGGToACB(copyFilePath, tempOutDirectory) ? 0 : -1; + if (passed >= 0) passed = OGGToACB(copyFilePath, tempOutDirectory, tjaHash) ? 0 : -1; break; default: Console.WriteLine($"Do not support {audioExtension} audio files"); @@ -196,14 +198,13 @@ namespace TJAConvert acbFile.Save(acbPath, bufferSize); } - private static bool CreateMusicFile(TJAMetadata metadata, string outputPath) + private static bool CreateMusicFile(TJAMetadata metadata, int tjaHash, string outputPath) { try { var musicInfo = new CustomSong { - uniqueId = metadata.Title.GetHashCode(), - id = metadata.Id, + id = tjaHash.ToString(), order = 0, genreNo = (int) metadata.Genre, branchEasy = false, @@ -213,6 +214,7 @@ namespace TJAConvert branchUra = false, previewPos = (int) (metadata.PreviewTime * 1000), fumenOffsetPos = (int) (metadata.Offset * 10), + tjaFileHash = tjaHash, songName = new TextEntry() { text = metadata.Title, @@ -316,7 +318,7 @@ namespace TJAConvert } } - private static async Task TJAToFumens(TJAMetadata metadata, string tjaPath, string outputPath) + private static async Task TJAToFumens(TJAMetadata metadata, string tjaPath, int tjaHash, string outputPath) { var fileName = Path.GetFileName(tjaPath); var newPath = Path.Combine(outputPath, fileName); @@ -332,7 +334,7 @@ namespace TJAConvert 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); + passed = await ConvertUraOni(metadata, newPath, tjaHash); if (passed < 0) return passed; // for every .bin in this directory, we can now add the prefix _x @@ -365,7 +367,7 @@ namespace TJAConvert { // will need to create additional files to splice them out - passed = await SpliceDoubles(metadata, newPath); + passed = await SpliceDoubles(metadata, newPath, tjaHash); if (passed < 0) return passed; @@ -380,7 +382,7 @@ namespace TJAConvert } if (metadata.Courses.All(x => x.PlayStyle != TJAMetadata.PlayStyle.Double)) - passed = await Convert(newPath, outputPath); + passed = await Convert(newPath, outputPath, tjaHash); if (passed < 0) return passed; @@ -395,7 +397,7 @@ namespace TJAConvert return passed; } - private static async Task ConvertUraOni(TJAMetadata metadata, string newPath) + private static async Task ConvertUraOni(TJAMetadata metadata, string newPath, int tjaHash) { var directory = Path.GetDirectoryName(newPath); var fileName = Path.GetFileNameWithoutExtension(newPath); @@ -427,7 +429,7 @@ namespace TJAConvert var path = $"{directory}/{fileName}.tja"; File.WriteAllLines(path, file); - var passed = await Convert(path, directory); + var passed = await Convert(path, directory, tjaHash); if (passed < 0) return passed; @@ -435,7 +437,7 @@ namespace TJAConvert } else { - var passed = await SplitP1P2(lines, course, directory, fileName, CourseType.Oni); + var passed = await SplitP1P2(lines, course, directory, fileName, tjaHash, CourseType.Oni); if (passed < 0) return passed; } @@ -449,7 +451,7 @@ namespace TJAConvert /// /// This aims to separate P1 and P2 tracks for TJA2BIN to read /// - private static async Task SpliceDoubles(TJAMetadata metadata, string newPath) + private static async Task SpliceDoubles(TJAMetadata metadata, string newPath, int tjaHash) { var directory = Path.GetDirectoryName(newPath); var fileName = Path.GetFileNameWithoutExtension(newPath); @@ -482,7 +484,7 @@ namespace TJAConvert // remove doubles section foreach (var course in doubleCourses) { - var passed = await SplitP1P2(lines, course, directory, fileName); + var passed = await SplitP1P2(lines, course, directory, fileName, tjaHash); if (passed < 0) return passed; } @@ -491,7 +493,7 @@ namespace TJAConvert return 0; } - private static async Task SplitP1P2(List lines, TJAMetadata.Course course, string directory, string fileName, CourseType? courseTypeOverride = null) + private static async Task SplitP1P2(List lines, TJAMetadata.Course course, string directory, string fileName, int tjaHash, CourseType? courseTypeOverride = null) { // metadata end int courseStartIndex = lines.FindLastIndex(x => @@ -528,7 +530,7 @@ namespace TJAConvert var path = $"{directory}/{fileName}_1.tja"; File.WriteAllLines(path, p1File); - var passed = await Convert(path, directory); + var passed = await Convert(path, directory, tjaHash); if (passed < 0) return passed; @@ -540,7 +542,7 @@ namespace TJAConvert path = $"{directory}/{fileName}_2.tja"; File.WriteAllLines(path, p2File); - passed = await Convert(path, directory); + passed = await Convert(path, directory, tjaHash); if (passed < 0) return passed; @@ -558,9 +560,9 @@ namespace TJAConvert } } - private static async Task Convert(string tjaPath, string outputPath) + private static async Task Convert(string tjaPath, string outputPath, int tjaHash) { - var fileName = Path.GetFileNameWithoutExtension(tjaPath); + var fileName = tjaHash.ToString(); TJAMetadata metadata; try @@ -572,7 +574,7 @@ namespace TJAConvert return -2; } - var newPath = $"{outputPath}\\{Path.GetFileName(tjaPath)}"; + var newPath = $"{outputPath}\\{fileName}"; if (metadata.Courses.Count == 1) { var coursePostfix = metadata.Courses[0].CourseType.ToShort(); @@ -986,12 +988,11 @@ namespace TJAConvert } } - private static bool OGGToACB(string oggPath, string outDirectory) + private static bool OGGToACB(string oggPath, string outDirectory, int tjaHash) { try { var directory = Path.GetDirectoryName(oggPath); - var fileName = Path.GetFileNameWithoutExtension(oggPath); var acbPath = $"{directory}/{Guid.NewGuid().ToString()}"; Directory.CreateDirectory(acbPath); @@ -1006,10 +1007,10 @@ namespace TJAConvert File.WriteAllBytes($"{acbPath}/00000.hca", hca); Pack(acbPath); - if (File.Exists($"{outDirectory}/song_{fileName}.bin")) - File.Delete($"{outDirectory}/song_{fileName}.bin"); + if (File.Exists($"{outDirectory}/song_{tjaHash}.bin")) + File.Delete($"{outDirectory}/song_{tjaHash}.bin"); - File.Move($"{acbPath}.acb", $"{outDirectory}/song_{fileName}.bin"); + File.Move($"{acbPath}.acb", $"{outDirectory}/song_{tjaHash}.bin"); Directory.Delete(acbPath, true); return true; } @@ -1020,12 +1021,11 @@ namespace TJAConvert } } - private static bool WavToACB(string wavPath, string outDirectory, bool deleteWav = false) + private static bool WavToACB(string wavPath, string outDirectory, int tjaHash, bool deleteWav = false) { try { var directory = Path.GetDirectoryName(wavPath); - var fileName = Path.GetFileNameWithoutExtension(wavPath); var acbPath = $"{directory}/{Guid.NewGuid().ToString()}"; Directory.CreateDirectory(acbPath); @@ -1037,10 +1037,10 @@ namespace TJAConvert 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"); + if (File.Exists($"{outDirectory}/song_{tjaHash}.bin")) + File.Delete($"{outDirectory}/song_{tjaHash}.bin"); - File.Move($"{acbPath}.acb", $"{outDirectory}/song_{fileName}.bin"); + File.Move($"{acbPath}.acb", $"{outDirectory}/song_{tjaHash}.bin"); if (deleteWav) File.Delete(wavPath); @@ -1138,4 +1138,4 @@ namespace TJAConvert return 3; } } -} +} \ No newline at end of file diff --git a/TakoTakoScripts/TakoTako.Common/CustomSong.cs b/TakoTakoScripts/TakoTako.Common/CustomSong.cs index 2080763..142adab 100644 --- a/TakoTakoScripts/TakoTako.Common/CustomSong.cs +++ b/TakoTakoScripts/TakoTako.Common/CustomSong.cs @@ -11,7 +11,6 @@ namespace TakoTako.Common public class CustomSong { // Song Details - [DataMember] public int uniqueId; [DataMember] public string id; [DataMember] public int order; [DataMember] public int genreNo; @@ -41,6 +40,9 @@ namespace TakoTako.Common [DataMember] public int scoreMania; [DataMember] public int scoreUra; + // Used for UID + [DataMember] public int tjaFileHash; + // Preview Details [DataMember] public int previewPos; [DataMember] public int fumenOffsetPos; @@ -201,4 +203,4 @@ namespace TakoTako.Common [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] public int krFont; } -} +} \ No newline at end of file diff --git a/TakoTakoScripts/TakoTako.Common/MurmurHash2.cs b/TakoTakoScripts/TakoTako.Common/MurmurHash2.cs new file mode 100644 index 0000000..177c2c3 --- /dev/null +++ b/TakoTakoScripts/TakoTako.Common/MurmurHash2.cs @@ -0,0 +1,85 @@ +using System.Text; + +// MIT License +// +// Copyright (c) 2017 Jitbit, 2022 TakoTako contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace TakoTako.Common; + +public class MurmurHash2 +{ + public static uint Hash(string data) + { + return Hash(Encoding.UTF8.GetBytes(data)); + } + + public static uint Hash(byte[] data) + { + return Hash(data, 0xc58f1a7a); + } + + private const uint m = 0x5bd1e995; + private const int r = 24; + + public static uint Hash(byte[] data, uint seed) + { + var length = data.Length; + if (length == 0) + return 0; + var h = seed ^ (uint)length; + var currentIndex = 0; + while (length >= 4) + { + var k = (uint)(data[currentIndex++] | (data[currentIndex++] << 8) | (data[currentIndex++] << 16) | + (data[currentIndex++] << 24)); + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + length -= 4; + } + + switch (length) + { + case 3: + h ^= (ushort)(data[currentIndex++] | (data[currentIndex++] << 8)); + h ^= (uint)(data[currentIndex] << 16); + h *= m; + break; + case 2: + h ^= (ushort)(data[currentIndex++] | (data[currentIndex] << 8)); + h *= m; + break; + case 1: + h ^= data[currentIndex]; + h *= m; + break; + } + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; + } +} \ No newline at end of file