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 const int PaddedSongTime = 2 * 1000; // in ms public const float TjaOffsetForPaddingSong = -1.0f; // in ms 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 originalTjaData = File.ReadAllBytes(tjaPath); var tjaHash = (int) (MurmurHash2.Hash(originalTjaData) & 0xFFFF_FFF); 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); int millisecondsAddedSilence = metadata.Offset > TjaOffsetForPaddingSong ? PaddedSongTime : 0; var audioExtension = Path.GetExtension(copyFilePath).TrimStart('.'); switch (audioExtension.ToLowerInvariant()) { case "wav": if (passed >= 0) passed = WavToACB(copyFilePath, tempOutDirectory, tjaHash, millisecondsAddedSilence: millisecondsAddedSilence) ? 0 : -1; break; case "ogg": if (passed >= 0) passed = OGGToACB(copyFilePath, tempOutDirectory, tjaHash, millisecondsAddedSilence) ? 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, int tjaHash, string outputPath) { try { var addedTime = metadata.Offset > TjaOffsetForPaddingSong ? PaddedSongTime : 0; var musicInfo = new CustomSong { id = tjaHash.ToString(), order = 0, genreNo = (int) metadata.Genre, branchEasy = false, branchNormal = false, branchHard = false, branchMania = false, branchUra = false, previewPos = (int) (metadata.PreviewTime * 1000) + addedTime, fumenOffsetPos = (int) (metadata.Offset * 10) + (addedTime), tjaFileHash = tjaHash, 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) { var isDouble = course.PlayStyle == TJAMetadata.PlayStyle.Double; var shinuti = EstimateScoreBasedOnNotes(course); //todo figure out the best score? switch (course.CourseType) { case CourseType.Easy: musicInfo.starEasy = course.Level; musicInfo.scoreEasy = 1000000; musicInfo.branchEasy = musicInfo.branchEasy || course.IsBranching; if (isDouble) musicInfo.shinutiEasyDuet = shinuti; else musicInfo.shinutiEasy = shinuti; break; case CourseType.Normal: musicInfo.starNormal = course.Level; musicInfo.scoreNormal = 1000000; musicInfo.branchNormal = musicInfo.branchNormal || course.IsBranching; if (isDouble) musicInfo.shinutiNormalDuet = shinuti; else musicInfo.shinutiNormal = shinuti; break; case CourseType.Hard: musicInfo.starHard = course.Level; musicInfo.scoreHard = 1000000; musicInfo.branchHard = musicInfo.branchHard || course.IsBranching; if (isDouble) musicInfo.shinutiHardDuet = shinuti; else musicInfo.shinutiHard = shinuti; break; case CourseType.Oni: musicInfo.starMania = course.Level; musicInfo.scoreMania = 1000000; musicInfo.branchMania = musicInfo.branchMania || course.IsBranching; if (isDouble) musicInfo.shinutiManiaDuet = shinuti; else musicInfo.shinutiMania = shinuti; break; case CourseType.UraOni: musicInfo.starUra = course.Level; musicInfo.scoreUra = 1000000; musicInfo.branchUra = musicInfo.branchUra || course.IsBranching; if (isDouble) musicInfo.shinutiUraDuet = shinuti; else musicInfo.shinutiUra = shinuti; break; default: throw new ArgumentOutOfRangeException(); } } // make sure each course as a score if (musicInfo.shinutiEasy == 0) musicInfo.shinutiEasy = musicInfo.shinutiEasyDuet != 0 ? musicInfo.shinutiEasyDuet : 7352; if (musicInfo.shinutiNormal == 0) musicInfo.shinutiNormal = musicInfo.shinutiNormalDuet != 0 ? musicInfo.shinutiNormalDuet : 4830; if (musicInfo.shinutiHard == 0) musicInfo.shinutiHard = musicInfo.shinutiHardDuet != 0 ? musicInfo.shinutiHardDuet : 3144; if (musicInfo.shinutiMania == 0) musicInfo.shinutiMania = musicInfo.shinutiManiaDuet != 0 ? musicInfo.shinutiManiaDuet : 2169; if (musicInfo.shinutiUra == 0) musicInfo.shinutiUra = musicInfo.shinutiUraDuet != 0 ? musicInfo.shinutiUraDuet : 1420; if (musicInfo.shinutiEasyDuet == 0) musicInfo.shinutiEasyDuet = musicInfo.shinutiEasy; if (musicInfo.shinutiNormalDuet == 0) musicInfo.shinutiNormalDuet = musicInfo.shinutiNormal; if (musicInfo.shinutiHardDuet == 0) musicInfo.shinutiHardDuet = musicInfo.shinutiHard; if (musicInfo.shinutiManiaDuet == 0) musicInfo.shinutiManiaDuet = musicInfo.shinutiMania; if (musicInfo.shinutiUraDuet == 0) musicInfo.shinutiUraDuet = musicInfo.shinutiUra; int EstimateScoreBasedOnNotes(TJAMetadata.Course course) { return Math.Max(1, 1000000 / course.EstimatedNotes); } 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, int tjaHash, 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, tjaHash); 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, tjaHash); 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, tjaHash); 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, int tjaHash) { 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, tjaHash); if (passed < 0) return passed; lines.RemoveRange(course.CourseDataIndexStart, course.SongDataIndexEnd - course.CourseDataIndexStart + 1); } else { var passed = await SplitP1P2(lines, course, directory, fileName, tjaHash, 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, int tjaHash) { 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, tjaHash); 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, int tjaHash, 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, tjaHash); 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, tjaHash); 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, int tjaHash) { var fileName = tjaHash.ToString(); TJAMetadata metadata; try { metadata = new TJAMetadata(tjaPath); } catch { return -2; } var newPath = $"{outputPath}\\{fileName}"; 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")) 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("invalid #BRANCHSTART")) { var currentLines = File.ReadLines(newPath).ToList(); for (var i = 0; i < currentLines.Count; i++) { var line = currentLines[i]; if (!line.StartsWith("#BRANCHSTART p,", StringComparison.InvariantCultureIgnoreCase)) continue; var arguments = line.Substring("#BRANCHSTART ".Length).Split(','); // This invalid branch start error needs to be manually resolved if (arguments.Length != 3) return false; float number1; float number2; if (!float.TryParse(arguments[1], out var test)) return false; number1 = test; if (!float.TryParse(arguments[2], out test)) return false; number2 = test; currentLines[i] = $"#BRANCHSTART p,{(int) Math.Ceiling(number1)},{(int) Math.Ceiling(number2)}"; } 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(); 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, int tjaHash, int millisecondsAddedSilence = 0) { try { var directory = Path.GetDirectoryName(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, millisecondsAddedSilence); if (hca == null) return false; File.WriteAllBytes($"{acbPath}/00000.hca", hca); Pack(acbPath); if (File.Exists($"{outDirectory}/song_{tjaHash}.bin")) File.Delete($"{outDirectory}/song_{tjaHash}.bin"); File.Move($"{acbPath}.acb", $"{outDirectory}/song_{tjaHash}.bin"); Directory.Delete(acbPath, true); return true; } catch (Exception e) { Console.WriteLine(e); return false; } } private static bool WavToACB(string wavPath, string outDirectory, int tjaHash, bool deleteWav = false, int millisecondsAddedSilence = 0) { try { var directory = Path.GetDirectoryName(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, millisecondsAddedSilence); File.WriteAllBytes($"{acbPath}/00000.hca", hca); Pack(acbPath); if (File.Exists($"{outDirectory}/song_{tjaHash}.bin")) File.Delete($"{outDirectory}/song_{tjaHash}.bin"); File.Move($"{acbPath}.acb", $"{outDirectory}/song_{tjaHash}.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, int millisecondSilence = 0) { var wavReader = new WaveReader(); var hcaWriter = new HcaWriter(); if (millisecondSilence > 0) { WaveFileReader reader = new WaveFileReader(path); var memoryStream = new MemoryStream(); var trimmed = new OffsetSampleProvider(reader.ToSampleProvider()) { DelayBy = TimeSpan.FromMilliseconds(millisecondSilence) }; WaveFileWriter.WriteWavFileToStream(memoryStream, trimmed.ToWaveProvider16()); var audioData = wavReader.Read(memoryStream.ToArray()); return hcaWriter.GetFile(audioData); } else { var audioData = wavReader.Read(File.ReadAllBytes(path)); return hcaWriter.GetFile(audioData); } } private static byte[] OggToHca(string inPath, int millisecondSilence = 0) { try { using FileStream fileIn = new FileStream(inPath, FileMode.Open); var vorbis = new VorbisWaveReader(fileIn); var wavProvider = new SampleToWaveProvider16(vorbis); var memoryStream = new MemoryStream(); if (millisecondSilence > 0) { var trimmed = new OffsetSampleProvider(wavProvider.ToSampleProvider()) { DelayBy = TimeSpan.FromMilliseconds(millisecondSilence) }; WaveFileWriter.WriteWavFileToStream(memoryStream, trimmed.ToWaveProvider16()); } else { WaveFileWriter.WriteWavFileToStream(memoryStream, wavProvider); } 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; } } }