1
0
mirror of https://github.com/SirusDoma/VoxCharger.git synced 2024-11-23 22:51:01 +01:00

Add Bulk Import

Also, fix some bugs
This commit is contained in:
SirusDoma 2020-04-19 19:13:12 +07:00
parent 2d6eb38dff
commit 1b82079a37
7 changed files with 592 additions and 445 deletions

View File

@ -2,7 +2,7 @@
- **Author**: CXO2 - **Author**: CXO2
- **Email**: com@cxo2.me - **Email**: com@cxo2.me
- **Version**: 0.9.7b - **Version**: 0.9.8b
Recharge your KFC Chicken sauce ([Download](https://github.com/SirusDoma/VoxCharger/releases)) Recharge your KFC Chicken sauce ([Download](https://github.com/SirusDoma/VoxCharger/releases))
@ -44,16 +44,21 @@ Vox have some sense in it's file format than ksh file, as the result, not all at
Remember, stupid input get stupid output. But if you believe it's a bug, feel free to open issue or PR. Remember, stupid input get stupid output. But if you believe it's a bug, feel free to open issue or PR.
### Music Preview ### Network Scores
Imported music preview from normal audio files may broken or not trimmed properly in the output file. Preview offset in ksh is also ignored, you need dedicated preview file with proper fade in and out. Keep in mind that `2dxwavconvert.exe` will not do this for you, it only trim your audio into 10 second for preview purpose. Until program supports built in encoder, you have to provide 2dx preview manually for proper preview file. Make sure you are using under Offline environment and **NOT** connected to any KFC network server while using the output files of this tool. It could break network score table and you may get banned from the network for doing so.
If you can't live without it, consider making PR to this feature.
### Music DB ### Music DB
Some attributes are kept hidden from editor, For existing songs, some of their attributes might untouched throughout save iteration, but the rest of hidden attributes might be replaced with dummy / stub data. The program also prevent you to modify Music ID and some attributes are kept hidden from editor, For existing songs, some of their attributes might untouched throughout save iteration, but the rest of hidden attributes might be replaced with dummy / stub data.
If this really concern you, make sure to backup your `music_db.xml` / `music_db.merged.xml`. If this really concern you, make sure to backup your `music_db.xml` / `music_db.merged.xml`.
### Music Preview
Imported music preview from normal audio files may broken or not trimmed properly in the output file. Preview offset in ksh is also ignored, you need dedicated preview file with proper fade in and out.
Keep in mind that `2dxwavconvert.exe` will not do this for you, it only trim your audio into 10 second for preview purpose. Until program supports built in encoder, you have to provide 2dx preview manually for proper preview file.
If you can't live without it, consider making PR to this feature.
### Asset File Modification ### Asset File Modification
Replacing asset files such as vox, 2dx and graphic files are happen immediately after changes are confirmed. In other hand, metadata need to be saved manually by clicking `File -> Save` or `CTRL+S`. Replacing asset files such as vox, 2dx and graphic files are happen immediately after changes are confirmed. In other hand, metadata need to be saved manually by clicking `File -> Save` or `CTRL+S`.

View File

@ -11,43 +11,40 @@ using System.Diagnostics;
namespace VoxCharger namespace VoxCharger
{ {
public enum ConvertMode
{
Converter = 0,
Importer = 1,
BulkConverter = 2,
BulkImporter = 3
}
public partial class ConverterForm : Form public partial class ConverterForm : Form
{ {
private class ChartInfo
{
public Ksh Source { get; set; }
public VoxLevelHeader Header { get; set; }
public string FileName { get; set; }
public string MusicFileName { get; set; }
public string JacketFileName { get; set; }
public ChartInfo(Ksh source, VoxLevelHeader header, string fileName)
{
Source = source;
Header = header;
FileName = fileName;
}
}
private readonly Image DummyJacket = VoxCharger.Properties.Resources.jk_dummy_s; private readonly Image DummyJacket = VoxCharger.Properties.Resources.jk_dummy_s;
public static string LastBackground { get; private set; } = "63"; public static string LastBackground { get; private set; } = "63";
private string target; private string target;
private string defaultAscii; private string defaultAscii;
private bool converter; private ConvertMode mode;
private Dictionary<Difficulty, ChartInfo> charts = new Dictionary<Difficulty, ChartInfo>(); private Dictionary<Difficulty, ChartInfo> charts = new Dictionary<Difficulty, ChartInfo>();
private Ksh.Exporter exporter;
public VoxHeader Header { get; private set; } = null; public VoxHeader Result { get; private set; } = null;
public Action Action { get; private set; } = null; public VoxHeader[] ResultSet { get; private set; } = new VoxHeader[0];
public Ksh.ParseOption Options { get; private set; } = new Ksh.ParseOption(); public Ksh.ParseOption Options { get; private set; } = new Ksh.ParseOption();
public Action Action { get; private set; } = null;
public Dictionary<string, Action> ActionSet { get; private set; } = new Dictionary<string, Action>();
public ConverterForm(string path, bool asConverter = false) public ConverterForm(string path, ConvertMode convert)
{ {
InitializeComponent(); InitializeComponent();
target = path; target = path;
converter = asConverter; mode = convert;
if (asConverter)
// Cancerous code to adjust layout depending what this form going to be
if (mode == ConvertMode.Converter)
{ {
MusicCodeLabel.Visible = false; MusicCodeLabel.Visible = false;
InfVerLabel.Visible = false; InfVerLabel.Visible = false;
@ -65,42 +62,67 @@ namespace VoxCharger
Height -= LevelGroupBox.Height + componentHeight; Height -= LevelGroupBox.Height + componentHeight;
ProcessConvertButton.Text = "Convert"; ProcessConvertButton.Text = "Convert";
} PathTextBox.Text = target;
else
ProcessConvertButton.Text = "Add";
PathTextBox.Text = target; return;
}
else if (mode == ConvertMode.BulkImporter)
{
MusicCodeLabel.Visible = false;
LevelGroupBox.Enabled = LevelGroupBox.Visible = false;
AsciiTextBox.Enabled = AsciiTextBox.Visible = false;
AsciiAutoCheckBox.Enabled = AsciiAutoCheckBox.Visible = false;
int componentHeight = AsciiTextBox.Height;
OptionsGroupBox.Location = LevelGroupBox.Location;
OptionsGroupBox.Height -= componentHeight;
Height -= LevelGroupBox.Height + componentHeight;
}
PathTextBox.Text = target;
BackgroundDropDown.SelectedItem = LastBackground;
VersionDropDown.SelectedIndex = 4;
InfVerDropDown.SelectedIndex = 0;
ProcessConvertButton.Text = "Add";
} }
private void OnConverterFormLoad(object sender, EventArgs e) private void OnConverterFormLoad(object sender, EventArgs e)
{ {
var main = new Ksh(); if (mode != ConvertMode.Importer)
if (converter)
return; return;
try try
{ {
var main = new Ksh();
main.Parse(target); main.Parse(target);
Result = main.ToHeader();
Result.ID = AssetManager.GetNextMusicID();
Result.Ascii = defaultAscii = AsciiTextBox.Text = Path.GetFileName(Path.GetDirectoryName(target));
exporter = new Ksh.Exporter(main);
Header = ToHeader(main); for (int i = 1; Directory.Exists(AssetManager.GetMusicPath(Result)); i++)
Header.Ascii = new DirectoryInfo(Path.GetDirectoryName(target)).Name;
defaultAscii = AsciiTextBox.Text = Header.Ascii;
for (int i = 1; Directory.Exists(AssetManager.GetMusicPath(Header)); i++)
{ {
if (i >= 100) if (i >= 100)
break; // seriously? stupid input get stupid output break; // seriously? stupid input get stupid output
Header.Ascii = $"{defaultAscii}{i:D2}"; Result.Ascii = $"{defaultAscii}{i:D2}";
} }
defaultAscii = AsciiTextBox.Text = Header.Ascii; defaultAscii = AsciiTextBox.Text = Result.Ascii;
BackgroundDropDown.SelectedItem = LastBackground; charts[main.Difficulty] = new ChartInfo(main, main.ToLevelHeader(), target);
VersionDropDown.SelectedIndex = 4;
InfVerDropDown.SelectedIndex = 0;
charts[main.Difficulty] = new ChartInfo(main, ToLevelHeader(main), target);
LoadJacket(charts[main.Difficulty]); LoadJacket(charts[main.Difficulty]);
// Try to locate another difficulty
foreach (var lv in Ksh.Exporter.GetCharts(Path.GetDirectoryName(target), main.Title))
{
// Don't replace main file, there might 2 files with similar meta or another stupid cases
if (lv.Key != main.Difficulty)
charts[lv.Key] = lv.Value;
}
UpdateUI();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -111,13 +133,9 @@ namespace VoxCharger
MessageBoxIcon.Error MessageBoxIcon.Error
); );
DialogResult = DialogResult.Cancel;
Close(); Close();
return;
} }
// Try to locate another difficulty
charts = GetCharts(target, main);
UpdateUI();
} }
private void OnAsciiAutoCheckBoxCheckedChanged(object sender, EventArgs e) private void OnAsciiAutoCheckBoxCheckedChanged(object sender, EventArgs e)
@ -160,7 +178,7 @@ namespace VoxCharger
chart.Parse(browser.FileName); chart.Parse(browser.FileName);
chart.Difficulty = diff; // make sure to replace diff chart.Difficulty = diff; // make sure to replace diff
charts[diff] = new ChartInfo(chart, ToLevelHeader(chart), browser.FileName); charts[diff] = new ChartInfo(chart, chart.ToLevelHeader(), browser.FileName);
UpdateUI(); UpdateUI();
} }
catch (Exception ex) catch (Exception ex)
@ -202,71 +220,46 @@ namespace VoxCharger
}; };
// Act as converter // Act as converter
if (converter) switch (mode)
Convert(); {
else case ConvertMode.Converter: SingleConvert(); break;
Process(); case ConvertMode.BulkConverter: BulkConvert(); break;
case ConvertMode.Importer: SingleImport(); break;
case ConvertMode.BulkImporter: BulkImport(); break;
}
} }
private VoxHeader ToHeader(Ksh chart) private void SingleImport()
{ {
return new VoxHeader() try
{ {
ID = AssetManager.GetNextMusicID(), // Assign metadata
Title = chart.Title, Result.Ascii = AsciiTextBox.Text;
Artist = chart.Artist, Result.BackgroundId = short.Parse((BackgroundDropDown.SelectedItem ?? "0").ToString().Split(' ')[0]);
BpmMin = chart.BpmMin, Result.Version = (GameVersion)(VersionDropDown.SelectedIndex + 1);
BpmMax = chart.BpmMax, Result.InfVersion = InfVerDropDown.SelectedIndex == 0 ? InfiniteVersion.MXM : (InfiniteVersion)(InfVerDropDown.SelectedIndex + 1);
Volume = chart.Volume > 0 ? (short)chart.Volume : (short)91, Result.GenreId = 16;
DistributionDate = DateTime.Now, Result.Levels = new Dictionary<Difficulty, VoxLevelHeader>();
BackgroundId = 63,
GenreId = 16,
};
}
private VoxLevelHeader ToLevelHeader(Ksh chart) if (Result.BpmMin != Result.BpmMax && exporter.Source.MusicOffset % 48 != 0 && Options.RealignOffset)
{
return new VoxLevelHeader
{
Difficulty = chart.Difficulty,
Illustrator = chart.Illustrator,
Effector = chart.Effector,
Level = chart.Level
};
}
private void Process()
{
try
{
bool warned = false;
foreach (var header in AssetManager.Headers)
{ {
if (Header.Ascii == header.Ascii) // You've been warned!
{ var prompt = MessageBox.Show(
MessageBox.Show( "Adapting music offset could break this chart.\n" +
$"Music Code is already exists.\n{AssetManager.GetMusicPath(header).Replace(AssetManager.GamePath, "")}", "Do you want to continue?",
"Error", "Warning",
MessageBoxButtons.OK, MessageBoxButtons.YesNo,
MessageBoxIcon.Error MessageBoxIcon.Warning
); );
if (prompt == DialogResult.No)
return; return;
}
} }
// Assign metadata if (Directory.Exists(AssetManager.GetMusicPath(Result)) || AssetManager.Headers.Any(h => h.Ascii == Result.Ascii))
Header.Ascii = AsciiTextBox.Text;
Header.BackgroundId = short.Parse((BackgroundDropDown.SelectedItem ?? "0").ToString().Split(' ')[0]);
Header.Version = (GameVersion)(VersionDropDown.SelectedIndex + 1);
Header.InfVersion = InfVerDropDown.SelectedIndex == 0 ? InfiniteVersion.MXM : (InfiniteVersion)(InfVerDropDown.SelectedIndex + 1);
Header.GenreId = 16;
Header.Levels = new Dictionary<Difficulty, VoxLevelHeader>();
if (Directory.Exists(AssetManager.GetMusicPath(Header)))
{ {
MessageBox.Show( MessageBox.Show(
$"Music asset for {Header.CodeName} is already exists.", $"Music Code {Result.CodeName} is already taken.",
"Error", "Error",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Error MessageBoxIcon.Error
@ -275,155 +268,8 @@ namespace VoxCharger
return; return;
} }
// Uhh, remove empty level? exporter.Export(Result, charts, Options);
var entries = charts.Where(x => x.Value != null); Action = exporter.Action;
charts = new Dictionary<Difficulty, ChartInfo>();
foreach (var entry in entries)
charts[entry.Key] = entry.Value;
// Assign level info and charts
foreach (var chart in charts)
{
var info = chart.Value;
if (!File.Exists(info.FileName))
{
MessageBox.Show(
$"Chart file was moved or deleted\n{info.FileName}",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error
);
charts[chart.Key] = null;
UpdateUI();
return;
}
// If you happen to read the source, this is probably what you're looking for
var ksh = new Ksh();
ksh.Parse(info.FileName, Options);
var bpmCount = ksh.Events.Count(ev => ev is Event.BPM);
if (!warned && bpmCount > 1 && ksh.MusicOffset % 48 != 0 && Options.RealignOffset)
{
// You've been warned!
var prompt = MessageBox.Show(
"Chart contains multiple bpm with music offset that non multiple of 48.\n" +
"Adapting music offset could break the chart.\n\n" +
"Do you want to continue?",
"Warning",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning
);
warned = true;
if (prompt == DialogResult.No)
return;
}
// Conversion is actually boring because its already "pre-converted"
var vox = new VoxChart();
vox.Import(ksh);
var level = ToLevelHeader(ksh);
level.Chart = vox;
info.Source = ksh;
Header.Levels[chart.Key] = charts[chart.Key].Header = level;
}
// Prevent resource files being moved or deleted, copy them into temporary storage
string musicFile = string.Empty;
string tmpFile = string.Empty;
foreach (var chart in charts)
{
var info = chart.Value;
// Make sure to reuse 2dx file for music that share same file
if (string.IsNullOrEmpty(musicFile) || chart.Value.MusicFileName != musicFile)
{
string music = Path.Combine(Path.GetDirectoryName(info.FileName), info.Source.MusicFileName);
if (File.Exists(music))
{
string tmp = Path.Combine(
Path.GetTempPath(),
$"{Path.GetRandomFileName()}{new FileInfo(info.Source.MusicFileName).Extension}"
);
musicFile = music;
info.MusicFileName = tmpFile = tmp;
File.Copy(music, tmp);
}
else
info.MusicFileName = string.Empty;
}
else
info.MusicFileName = tmpFile;
string jacket = Path.Combine(Path.GetDirectoryName(info.FileName), info.Source.JacketFileName);
if (File.Exists(jacket))
{
string tmp = Path.Combine(
Path.GetTempPath(),
$"{Path.GetRandomFileName()}{new FileInfo(info.Source.JacketFileName).Extension}"
);
try
{
using (var image = Image.FromFile(jacket))
info.Header.Jacket = new Bitmap(image);
}
catch (Exception)
{
info.Header.Jacket = null;
}
info.JacketFileName = tmp;
File.Copy(jacket, tmp);
}
}
Action = new Action(() =>
{
bool unique = false;
musicFile = charts.Values.First().MusicFileName;
foreach (var chart in charts)
{
if (chart.Value.MusicFileName != musicFile)
{
unique = true;
break;
}
}
// Import all music assets
AssetManager.ImportVox(Header);
// Make sure to use single asset for music for shared music file
if (!unique)
{
AssetManager.Import2DX(musicFile, Header);
AssetManager.Import2DX(musicFile, Header, true);
}
foreach (var chart in charts.Values)
{
if (unique && File.Exists(chart.MusicFileName))
{
AssetManager.Import2DX(chart.MusicFileName, Header, chart.Header.Difficulty);
AssetManager.Import2DX(chart.MusicFileName, Header, chart.Header.Difficulty, true);
}
if (File.Exists(chart.JacketFileName))
{
using (var image = Image.FromFile(chart.JacketFileName))
AssetManager.ImportJacket(Header, chart.Header.Difficulty, new Bitmap(image));
}
}
});
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();
@ -431,42 +277,140 @@ namespace VoxCharger
catch (Exception ex) catch (Exception ex)
{ {
MessageBox.Show( MessageBox.Show(
$"Failed to import ksh chart.\n{ex.Message}", $"Failed to import ksh chart.\n{ex.Message}",
"Error", "Error",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Error MessageBoxIcon.Error
); );
// Eliminate non-existent files
foreach (var chart in charts.Values.ToArray())
{
if (!File.Exists(chart.FileName))
charts.Remove(chart.Header.Difficulty);
}
// Reload jacket
UpdateUI();
} }
} }
private void Convert() private void BulkImport()
{ {
// Only serve as options dialog var output = new List<VoxHeader>();
if (string.IsNullOrEmpty(target)) var actions = new Dictionary<string, Action>();
var errors = new List<string>();
using (var loader = new LoadingForm())
{ {
DialogResult = DialogResult.OK; var action = new Action(() =>
Close(); {
var directories = Directory.GetDirectories(target);
int progress = 0;
foreach (string dir in directories)
{
loader.SetStatus($"Processing {Path.GetFileName(dir)}..");
loader.SetProgress((progress++ / (float)directories.Length) * 100f);
var files = Directory.GetFiles(dir, "*.ksh");
if (files.Length == 0)
continue;
string fn = files[0];
try
{
var ksh = new Ksh();
ksh.Parse(fn, Options);
var header = ksh.ToHeader();
header.ID = AssetManager.GetNextMusicID();
header.BackgroundId = short.Parse((BackgroundDropDown.SelectedItem ?? "0").ToString().Split(' ')[0]);
header.Version = (GameVersion)(VersionDropDown.SelectedIndex + 1);
header.InfVersion = InfVerDropDown.SelectedIndex == 0 ? InfiniteVersion.MXM : (InfiniteVersion)(InfVerDropDown.SelectedIndex + 1);
header.GenreId = 16;
header.Levels = new Dictionary<Difficulty, VoxLevelHeader>();
string ascii = Path.GetFileName(Path.GetDirectoryName(fn));
if (AssetManager.Headers.Any(v => v.Ascii == ascii) || // Duplicate header with same ascii
Directory.Exists(AssetManager.GetMusicPath(header)) || // Asset that use the ascii is already exists
output.Any(h => h.Ascii == ascii)) // Output with same ascii is already exists
{
continue;
}
var charts = Ksh.Exporter.GetCharts(Path.GetDirectoryName(fn), header.Title);
var exporter = new Ksh.Exporter(ksh);
header.Ascii = ascii;
exporter.Export(header, charts, Options);
output.Add(header);
actions.Add(ascii, exporter.Action);
}
catch (Exception ex)
{
string err = $"Failed attempt to convert ksh file: {Path.GetFileName(fn)} ({ex.Message})";
errors.Add(err);
Debug.WriteLine(err);
continue;
}
}
loader.Complete();
});
loader.SetAction(action);
loader.ShowDialog();
} }
try if (errors.Count != 0)
{ {
if (File.Exists(target) || Directory.Exists(target)) string message = "Failed to import one or more charts:";
{ foreach (string err in errors)
if (File.Exists(target)) message += $"\n{err}";
SingleConvert(Options);
else if (Directory.Exists(target)) MessageBox.Show(
BulkConvert(Options); message,
} "Warning",
else MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
ResultSet = output.ToArray();
ActionSet = actions;
DialogResult = DialogResult.OK;
Close();
}
private void SingleConvert()
{
try
{
// Single convert
using (var browser = new SaveFileDialog())
{ {
browser.Filter = "Sound Voltex Chart File|*.vox|All Files|*.*";
if (browser.ShowDialog() != DialogResult.OK)
return;
var ksh = new Ksh();
ksh.Parse(target, Options);
var vox = new VoxChart();
vox.Import(ksh);
vox.Serialize(browser.FileName);
MessageBox.Show( MessageBox.Show(
"Target path not found", "Chart has been converted successfully",
"Error", "Information",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Error MessageBoxIcon.Information
); );
DialogResult = DialogResult.Cancel; DialogResult = DialogResult.OK;
Close(); Close();
} }
} }
@ -481,31 +425,91 @@ namespace VoxCharger
} }
} }
private Dictionary<Difficulty, ChartInfo> GetCharts(string target, Ksh main) private void BulkConvert()
{ {
// Try to locate another difficulty var errors = new List<string>();
string dir = Path.GetDirectoryName(target); using (var browser = new FolderBrowserDialog())
var charts = new Dictionary<Difficulty, ChartInfo>();
foreach (string fn in Directory.GetFiles(dir, "*.ksh"))
{ {
try browser.Description = "Select output directory";
if (browser.ShowDialog() != DialogResult.OK)
return;
using (var loader = new LoadingForm())
{ {
var chart = new Ksh(); var action = new Action(() =>
chart.Parse(fn); {
var directories = Directory.GetDirectories(target);
int progress = 0;
foreach (string dir in directories)
{
loader.SetStatus($"Processing {Path.GetFileName(dir)}..");
loader.SetProgress((progress++ / (float)directories.Length) * 100f);
foreach (var fn in Directory.GetFiles(dir, "*.ksh"))
{
try
{
// Determine output path
string path = Path.Combine(
$"{browser.SelectedPath}",
$"{Path.GetFileName(dir)}\\"
);
// Create output folder if it's not exists
Directory.CreateDirectory(path);
string output = Path.Combine(path, Path.GetFileName(fn.Replace(".ksh", ".vox")));
// Different chart // If you happen to read the source, you're probably looking for these boring lines
if (chart.Title != main.Title) var ksh = new Ksh();
continue; ksh.Parse(fn, Options);
charts[chart.Difficulty] = new ChartInfo(chart, ToLevelHeader(chart), fn); var vox = new VoxChart();
} vox.Import(ksh);
catch (Exception ex) vox.Serialize(output);
{ }
Debug.WriteLine("Failed attempt to parse ksh file: {0} ({1})", fn, ex.Message); catch (Exception ex)
{
string err = $"Failed attempt to convert ksh file: {Path.GetFileName(fn)} ({ex.Message})";
errors.Add(err);
Debug.WriteLine(err);
continue;
}
}
}
loader.Complete();
});
loader.SetAction(action);
loader.ShowDialog();
} }
} }
return charts; if (errors.Count == 0)
{
MessageBox.Show(
"Chart has been converted successfully",
"Information",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
}
else
{
string message = "Failed to convert one or more charts:";
foreach (string err in errors)
message += $"\n{err}";
MessageBox.Show(
message,
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
DialogResult = DialogResult.OK;
Close();
} }
private void LoadJacket(ChartInfo info) private void LoadJacket(ChartInfo info)
@ -590,9 +594,9 @@ namespace VoxCharger
{ {
switch (diff) switch (diff)
{ {
case Difficulty.Novice: button.Text = "NOV"; break; case Difficulty.Novice: button.Text = "NOV"; break;
case Difficulty.Advanced: button.Text = "ADV"; break; case Difficulty.Advanced: button.Text = "ADV"; break;
case Difficulty.Exhaust: button.Text = "EXH"; break; case Difficulty.Exhaust: button.Text = "EXH"; break;
default: default:
button.Text = InfVerDropDown.SelectedItem.ToString(); button.Text = InfVerDropDown.SelectedItem.ToString();
break; break;
@ -603,134 +607,5 @@ namespace VoxCharger
} }
} }
} }
private void SingleConvert(Ksh.ParseOption options)
{
// Single convert
using (var browser = new SaveFileDialog())
{
browser.Filter = "Sound Voltex Chart File|*.vox|All Files|*.*";
if (browser.ShowDialog() != DialogResult.OK)
return;
var ksh = new Ksh();
ksh.Parse(target, options);
var bpmCount = ksh.Events.Count(ev => ev is Event.BPM);
if (bpmCount > 1 && ksh.MusicOffset % 48 != 0 && options.RealignOffset)
{
// You've been warned!
var prompt = MessageBox.Show(
"Chart contains multiple bpm with music offset that non multiple of 48.\n" +
"Adapting music offset could break the chart.\n\n" +
"Do you want to continue?",
"Warning",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning
);
if (prompt == DialogResult.No)
return;
}
var vox = new VoxChart();
vox.Import(ksh);
vox.Serialize(browser.FileName);
MessageBox.Show(
"Chart has been converted successfully",
"Information",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
DialogResult = DialogResult.OK;
Close();
}
}
private void BulkConvert(Ksh.ParseOption options)
{
bool warned = false;
using (var browser = new FolderBrowserDialog())
{
browser.Description = "Select output directory";
if (browser.ShowDialog() != DialogResult.OK)
return;
using (var loader = new LoadingForm())
{
var action = new Action(() =>
{
var directories = Directory.GetDirectories(target);
int progress = 0;
foreach (string dir in directories)
{
loader.SetStatus($"Processing {Path.GetFileName(dir)}..");
loader.SetProgress((progress++ / (float)directories.Length) * 100f);
foreach (var fn in Directory.GetFiles(dir, "*.ksh"))
{
try
{
var ksh = new Ksh();
ksh.Parse(fn, options);
var bpmCount = ksh.Events.Count(ev => ev is Event.BPM);
if (!warned && bpmCount > 1 && ksh.MusicOffset % 48 != 0 && options.RealignOffset)
{
// You've been warned!
var prompt = MessageBox.Show(
"Chart contains multiple bpm with music offset that non multiple of 48.\n" +
"Adapting music offset could break the chart.\n\n" +
"Do you want to continue?",
"Warning",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning
);
warned = true;
if (prompt == DialogResult.No)
return;
}
var vox = new VoxChart();
vox.Import(ksh);
string path = Path.Combine(
$"{browser.SelectedPath}",
$"{Path.GetFileName(dir)}\\"
);
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
vox.Serialize($"{Path.Combine(path, Path.GetFileName(fn.Replace(".ksh", ".vox")))}");
}
catch (Exception ex)
{
Debug.WriteLine("Failed attempt to convert ksh file: {0} ({1})", fn, ex.Message);
continue;
}
}
}
loader.Complete();
});
loader.SetAction(action);
loader.ShowDialog();
}
}
MessageBox.Show(
"Chart has been converted successfully",
"Information",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
DialogResult = DialogResult.OK;
Close();
}
} }
} }

View File

@ -46,6 +46,8 @@
this.AddEditMenu = new System.Windows.Forms.MenuItem(); this.AddEditMenu = new System.Windows.Forms.MenuItem();
this.AddNewEditMenu = new System.Windows.Forms.MenuItem(); this.AddNewEditMenu = new System.Windows.Forms.MenuItem();
this.ImportKshEditMenu = new System.Windows.Forms.MenuItem(); this.ImportKshEditMenu = new System.Windows.Forms.MenuItem();
this.MenuSeparator7 = new System.Windows.Forms.MenuItem();
this.BulkImportKshEditMenu = new System.Windows.Forms.MenuItem();
this.RemoveEditMenu = new System.Windows.Forms.MenuItem(); this.RemoveEditMenu = new System.Windows.Forms.MenuItem();
this.MenuSeparator4 = new System.Windows.Forms.MenuItem(); this.MenuSeparator4 = new System.Windows.Forms.MenuItem();
this.Import2DXEditMenu = new System.Windows.Forms.MenuItem(); this.Import2DXEditMenu = new System.Windows.Forms.MenuItem();
@ -69,6 +71,8 @@
this.ImportContextMenu = new System.Windows.Forms.ContextMenu(); this.ImportContextMenu = new System.Windows.Forms.ContextMenu();
this.ImportVoxMenu = new System.Windows.Forms.MenuItem(); this.ImportVoxMenu = new System.Windows.Forms.MenuItem();
this.ImportKshMenu = new System.Windows.Forms.MenuItem(); this.ImportKshMenu = new System.Windows.Forms.MenuItem();
this.MenuSeparator8 = new System.Windows.Forms.MenuItem();
this.BulkImportKshMenu = new System.Windows.Forms.MenuItem();
this.RemoveButton = new System.Windows.Forms.Button(); this.RemoveButton = new System.Windows.Forms.Button();
this.MusicListBox = new System.Windows.Forms.ListBox(); this.MusicListBox = new System.Windows.Forms.ListBox();
this.MetadataGroupBox = new System.Windows.Forms.GroupBox(); this.MetadataGroupBox = new System.Windows.Forms.GroupBox();
@ -228,7 +232,9 @@
this.AddEditMenu.Index = 0; this.AddEditMenu.Index = 0;
this.AddEditMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.AddEditMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.AddNewEditMenu, this.AddNewEditMenu,
this.ImportKshEditMenu}); this.ImportKshEditMenu,
this.MenuSeparator7,
this.BulkImportKshEditMenu});
this.AddEditMenu.Shortcut = System.Windows.Forms.Shortcut.Ins; this.AddEditMenu.Shortcut = System.Windows.Forms.Shortcut.Ins;
this.AddEditMenu.Text = "Add"; this.AddEditMenu.Text = "Add";
// //
@ -241,8 +247,19 @@
// ImportKshEditMenu // ImportKshEditMenu
// //
this.ImportKshEditMenu.Index = 1; this.ImportKshEditMenu.Index = 1;
this.ImportKshEditMenu.Text = "Import from Ksh.."; this.ImportKshEditMenu.Text = "Import Ksh..";
this.ImportKshEditMenu.Click += new System.EventHandler(this.OnImportKshMenuClick); this.ImportKshEditMenu.Click += new System.EventHandler(this.OnSingleImportMenuClick);
//
// MenuSeparator7
//
this.MenuSeparator7.Index = 2;
this.MenuSeparator7.Text = "-";
//
// BulkImportKshEditMenu
//
this.BulkImportKshEditMenu.Index = 3;
this.BulkImportKshEditMenu.Text = "Bulk Import Ksh..";
this.BulkImportKshEditMenu.Click += new System.EventHandler(this.OnBulkImportKshMenuClick);
// //
// RemoveEditMenu // RemoveEditMenu
// //
@ -410,7 +427,9 @@
// //
this.ImportContextMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.ImportContextMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.ImportVoxMenu, this.ImportVoxMenu,
this.ImportKshMenu}); this.ImportKshMenu,
this.MenuSeparator8,
this.BulkImportKshMenu});
// //
// ImportVoxMenu // ImportVoxMenu
// //
@ -422,7 +441,18 @@
// //
this.ImportKshMenu.Index = 1; this.ImportKshMenu.Index = 1;
this.ImportKshMenu.Text = "Import Ksh.."; this.ImportKshMenu.Text = "Import Ksh..";
this.ImportKshMenu.Click += new System.EventHandler(this.OnImportKshMenuClick); this.ImportKshMenu.Click += new System.EventHandler(this.OnSingleImportMenuClick);
//
// MenuSeparator8
//
this.MenuSeparator8.Index = 2;
this.MenuSeparator8.Text = "-";
//
// BulkImportKshMenu
//
this.BulkImportKshMenu.Index = 3;
this.BulkImportKshMenu.Text = "Bulk Import Ksh..";
this.BulkImportKshMenu.Click += new System.EventHandler(this.OnBulkImportKshMenuClick);
// //
// RemoveButton // RemoveButton
// //
@ -1079,6 +1109,10 @@
private System.Windows.Forms.MenuItem BulkConvertToolsMenu; private System.Windows.Forms.MenuItem BulkConvertToolsMenu;
private System.Windows.Forms.MenuItem MenuSeparator6; private System.Windows.Forms.MenuItem MenuSeparator6;
private System.Windows.Forms.MenuItem AutosaveEditMenu; private System.Windows.Forms.MenuItem AutosaveEditMenu;
private System.Windows.Forms.MenuItem BulkImportKshEditMenu;
private System.Windows.Forms.MenuItem MenuSeparator7;
private System.Windows.Forms.MenuItem MenuSeparator8;
private System.Windows.Forms.MenuItem BulkImportKshMenu;
} }
} }

View File

@ -259,7 +259,7 @@ namespace VoxCharger
if (browser.ShowDialog() != DialogResult.OK) if (browser.ShowDialog() != DialogResult.OK)
return; return;
using (var converter = new ConverterForm(browser.FileName, true)) using (var converter = new ConverterForm(browser.FileName, ConvertMode.Converter))
converter.ShowDialog(); converter.ShowDialog();
} }
} }
@ -272,7 +272,7 @@ namespace VoxCharger
if (browser.ShowDialog() != DialogResult.OK) if (browser.ShowDialog() != DialogResult.OK)
return; return;
using (var converter = new ConverterForm(browser.SelectedPath, true)) using (var converter = new ConverterForm(browser.SelectedPath, ConvertMode.Converter))
converter.ShowDialog(); converter.ShowDialog();
} }
} }
@ -453,7 +453,7 @@ namespace VoxCharger
MusicListBox.Items.Add(header); MusicListBox.Items.Add(header);
} }
private void OnImportKshMenuClick(object sender, EventArgs e) private void OnSingleImportMenuClick(object sender, EventArgs e)
{ {
using (var browser = new OpenFileDialog()) using (var browser = new OpenFileDialog())
{ {
@ -463,12 +463,12 @@ namespace VoxCharger
if (browser.ShowDialog() != DialogResult.OK) if (browser.ShowDialog() != DialogResult.OK)
return; return;
using (var converter = new ConverterForm(browser.FileName)) using (var converter = new ConverterForm(browser.FileName, ConvertMode.Importer))
{ {
if (converter.ShowDialog() != DialogResult.OK) if (converter.ShowDialog() != DialogResult.OK)
return; return;
var header = converter.Header; var header = converter.Result;
if (!actions.ContainsKey(header.Ascii)) if (!actions.ContainsKey(header.Ascii))
actions[header.Ascii] = new Queue<Action>(); actions[header.Ascii] = new Queue<Action>();
@ -483,6 +483,37 @@ namespace VoxCharger
} }
} }
private void OnBulkImportKshMenuClick(object sender, EventArgs e)
{
using (var browser = new FolderBrowserDialog())
{
browser.Description = "Select Kshoot chart repository";
if (browser.ShowDialog() != DialogResult.OK)
return;
using (var converter = new ConverterForm(browser.SelectedPath, ConvertMode.BulkImporter))
{
if (converter.ShowDialog() != DialogResult.OK)
return;
foreach (var header in converter.ResultSet)
{
if (!actions.ContainsKey(header.Ascii))
actions[header.Ascii] = new Queue<Action>();
AssetManager.Headers.Add(header);
MusicListBox.Items.Add(header);
actions[header.Ascii].Enqueue(converter.ActionSet[header.Ascii]);
}
Pristine = false;
if (Autosave)
Save(AssetManager.MdbFilename);
}
}
}
private void OnMetadataChanged(object sender, EventArgs e) private void OnMetadataChanged(object sender, EventArgs e)
{ {
if (sender is TextBox textBox && !textBox.Modified) if (sender is TextBox textBox && !textBox.Modified)
@ -723,11 +754,11 @@ namespace VoxCharger
{ {
var proc = new Action(() => var proc = new Action(() =>
{ {
int max = actions.Count + 1; float it = 1f;
foreach (var action in actions) foreach (var action in actions)
{ {
float progress = ((float)(max - actions.Count) / max) * 100f; float progress = (it++ / actions.Count) * 100f;
loader.SetStatus($"[{progress:00}%] - Processing assets.."); loader.SetStatus($"[{progress:00}%] - Processing {action.Key} assets..");
loader.SetProgress(progress); loader.SetProgress(progress);
var queue = action.Value; var queue = action.Value;
@ -953,5 +984,6 @@ namespace VoxCharger
ExplorerEditMenu.Enabled = false; ExplorerEditMenu.Enabled = false;
} }
#endregion #endregion
} }
} }

179
Sources/Ksh/Exporter.cs Normal file
View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VoxCharger
{
public class ChartInfo
{
public Ksh Source { get; set; }
public VoxLevelHeader Header { get; set; }
public string FileName { get; set; }
public string MusicFileName { get; set; }
public ChartInfo(Ksh source, VoxLevelHeader header, string fileName)
{
Source = source;
Header = header;
FileName = fileName;
}
}
public partial class Ksh
{
public class Exporter
{
public Ksh Source { get; private set; } = null;
public Action Action { get; private set; } = null;
public Exporter(Ksh src)
{
Source = src;
}
public void Export(VoxHeader header, ParseOption options = null)
{
Export(header, null, options);
}
public void Export(VoxHeader header, Dictionary<Difficulty, ChartInfo> charts, ParseOption options = null)
{
// Assign level info and charts
foreach (var chart in charts)
{
var info = chart.Value;
if (!File.Exists(info.FileName))
throw new IOException($"Chart file was moved or deleted\n{info.FileName}");
// If you happen to read the source, this is probably what you're looking for
var ksh = new Ksh();
ksh.Parse(info.FileName, options);
// Conversion is actually boring because its already "pre-converted"
var vox = new VoxChart();
vox.Import(ksh);
var level = ksh.ToLevelHeader();
level.Chart = vox;
info.Source = ksh;
header.Levels[chart.Key] = charts[chart.Key].Header = level;
}
// Prevent resource files being moved or deleted, copy them into temporary storage
string musicFile = string.Empty;
string tmpFile = string.Empty;
foreach (var chart in charts)
{
var info = chart.Value;
// Make sure to reuse 2dx file for music that share same file
if (string.IsNullOrEmpty(musicFile) || chart.Value.MusicFileName != musicFile)
{
string music = Path.Combine(Path.GetDirectoryName(info.FileName), info.Source.MusicFileName);
if (File.Exists(music))
{
string tmp = Path.Combine(
Path.GetTempPath(),
$"{Path.GetRandomFileName()}{new FileInfo(info.Source.MusicFileName).Extension}"
);
musicFile = music;
info.MusicFileName = tmpFile = tmp;
File.Copy(music, tmp);
}
else
info.MusicFileName = string.Empty;
}
else
info.MusicFileName = tmpFile;
string jacket = Path.Combine(Path.GetDirectoryName(info.FileName), info.Source.JacketFileName);
if (File.Exists(jacket))
{
try
{
using (var image = Image.FromFile(jacket))
info.Header.Jacket = new Bitmap(image);
}
catch (Exception)
{
info.Header.Jacket = null;
}
}
}
Action = new Action(() =>
{
bool unique = false;
musicFile = charts.Values.First().MusicFileName;
foreach (var chart in charts)
{
if (chart.Value.MusicFileName != musicFile)
{
unique = true;
break;
}
}
// Import all music assets
AssetManager.ImportVox(header);
// Make sure to use single asset for music for shared music file
if (!unique)
{
AssetManager.Import2DX(musicFile, header);
AssetManager.Import2DX(musicFile, header, true);
}
foreach (var chart in charts.Values)
{
if (unique && File.Exists(chart.MusicFileName))
{
AssetManager.Import2DX(chart.MusicFileName, header, chart.Header.Difficulty);
AssetManager.Import2DX(chart.MusicFileName, header, chart.Header.Difficulty, true);
}
if (chart.Header.Jacket != null)
AssetManager.ImportJacket(header, chart.Header.Difficulty, chart.Header.Jacket);
}
});
}
public static Dictionary<Difficulty, ChartInfo> GetCharts(string dir, string title)
{
// Try to locate another difficulty
var charts = new Dictionary<Difficulty, ChartInfo>();
foreach (string fn in Directory.GetFiles(dir, "*.ksh"))
{
try
{
var chart = new Ksh();
chart.Parse(fn);
// Different chart
if (chart.Title != title)
continue;
charts[chart.Difficulty] = new ChartInfo(chart, chart.ToLevelHeader(), fn);
}
catch (Exception ex)
{
Debug.WriteLine("Failed attempt to parse ksh file: {0} ({1})", fn, ex.Message);
}
}
return charts;
}
}
}
}

View File

@ -44,6 +44,32 @@ namespace VoxCharger
{ {
} }
public VoxHeader ToHeader()
{
return new VoxHeader()
{
Title = Title,
Artist = Artist,
BpmMin = BpmMin,
BpmMax = BpmMax,
Volume = Volume > 0 ? (short)Volume : (short)91,
DistributionDate = DateTime.Now,
BackgroundId = 63,
GenreId = 16,
};
}
public VoxLevelHeader ToLevelHeader()
{
return new VoxLevelHeader
{
Difficulty = Difficulty,
Illustrator = Illustrator,
Effector = Effector,
Level = Level
};
}
public void Parse(string fileName, ParseOption opt = null) public void Parse(string fileName, ParseOption opt = null)
{ {
// I pulled all nighters for few days, all for this piece of trash codes :) // I pulled all nighters for few days, all for this piece of trash codes :)
@ -108,13 +134,8 @@ namespace VoxCharger
if (opt.RealignOffset) if (opt.RealignOffset)
position -= MusicOffset; // Attempt to align position -= MusicOffset; // Attempt to align
time = Time.FromOffset(position, signature);
//int p = time.AsAbsoluteOffset(signature);
//if ((int)Math.Round(position) != p)
// Debug.WriteLine("");
// Magic happens here! // Magic happens here!
time = Time.FromOffset(position, signature);
if (time.Measure < 0) if (time.Measure < 0)
continue; // Might happen when try to realign music offset continue; // Might happen when try to realign music offset

View File

@ -97,6 +97,7 @@
<Compile Include="Sources\Events\Signature.cs" /> <Compile Include="Sources\Events\Signature.cs" />
<Compile Include="Sources\Events\Stop.cs" /> <Compile Include="Sources\Events\Stop.cs" />
<Compile Include="Sources\Events\TiltMode.cs" /> <Compile Include="Sources\Events\TiltMode.cs" />
<Compile Include="Sources\Ksh\Exporter.cs" />
<Compile Include="Sources\Ksh\Ksh.cs" /> <Compile Include="Sources\Ksh\Ksh.cs" />
<Compile Include="Sources\Events\EventCollection.cs" /> <Compile Include="Sources\Events\EventCollection.cs" />
<Compile Include="Sources\Events\Event.cs" /> <Compile Include="Sources\Events\Event.cs" />