1142 lines
48 KiB
C#
1142 lines
48 KiB
C#
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);
|
|
}
|
|
|
|
/// <returns>0 if pass, -1 if failed unexpectedly, -2 if invalid tja, -3 Timeout</returns>
|
|
public static async Task<string> 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<int> 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<int> 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<int> 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<string>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This aims to separate P1 and P2 tracks for TJA2BIN to read
|
|
/// </summary>
|
|
private static async Task<int> 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<int> SplitP1P2(List<string> 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<string>(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<string>(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<string> 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<int> 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<string> ApplyGeneralFixes(List<string> 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<int> currentNumbers = new List<int>();
|
|
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<List<string>>();
|
|
|
|
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<TJAMetadata.Course>(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<char> 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;
|
|
}
|
|
}
|
|
}
|