2020-04-18 22:24:48 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Xml;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
|
|
|
|
namespace VoxCharger
|
|
|
|
|
{
|
|
|
|
|
public class MusicDb : IEnumerable<VoxHeader>
|
|
|
|
|
{
|
|
|
|
|
private const string DummyYomigana = "ダミー";
|
|
|
|
|
private static readonly Encoding DefaultEncoding = Encoding.GetEncoding("Shift_JIS");
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
private Dictionary<int, VoxHeader> _headers;
|
|
|
|
|
private int _max = 0;
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
public int Count => _headers.Count;
|
2020-04-19 10:02:55 +02:00
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
public int LastId => _headers.Values.Max(h => h.Id);
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
|
|
|
|
public MusicDb()
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
_headers = new Dictionary<int, VoxHeader>();
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Load(string path, bool append = false)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
_headers = append ? _headers : new Dictionary<int, VoxHeader>();
|
2020-04-18 22:24:48 +02:00
|
|
|
|
var doc = new XmlDocument();
|
|
|
|
|
using (var reader = new StreamReader(path, DefaultEncoding))
|
|
|
|
|
{
|
|
|
|
|
// Load via reader to ensure correct encoding is used
|
|
|
|
|
doc.Load(reader);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (XmlNode music in doc.SelectSingleNode("mdb"))
|
|
|
|
|
{
|
|
|
|
|
int id = int.Parse(music.Attributes["id"].Value);
|
2023-01-06 14:58:29 +01:00
|
|
|
|
if (append && _headers.ContainsKey(id))
|
2020-04-18 22:24:48 +02:00
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var info = music.SelectSingleNode("info");
|
|
|
|
|
var header = new VoxHeader
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
Id = id,
|
2020-04-18 22:24:48 +02:00
|
|
|
|
Title = info.SelectSingleNode("title_name").InnerText,
|
2023-01-06 14:58:29 +01:00
|
|
|
|
TitleYomigana = info.SelectSingleNode("title_yomigana").InnerText,
|
2020-04-18 22:24:48 +02:00
|
|
|
|
Artist = info.SelectSingleNode("artist_name").InnerText,
|
2023-01-06 14:58:29 +01:00
|
|
|
|
ArtistYomigana = info.SelectSingleNode("artist_yomigana").InnerText,
|
2020-04-18 22:24:48 +02:00
|
|
|
|
Ascii = info.SelectSingleNode("ascii").InnerText,
|
|
|
|
|
DistributionDate = DateTime.ParseExact(info.SelectSingleNode("distribution_date").InnerText, "yyyyMMdd", CultureInfo.InvariantCulture),
|
|
|
|
|
Volume = short.Parse(info.SelectSingleNode("volume").InnerText),
|
|
|
|
|
BackgroundId = short.Parse(info.SelectSingleNode("bg_no").InnerText),
|
|
|
|
|
GenreId = int.Parse(info.SelectSingleNode("genre").InnerText),
|
|
|
|
|
Version = (GameVersion)int.Parse(info.SelectSingleNode("version").InnerText),
|
|
|
|
|
InfVersion = (InfiniteVersion)int.Parse(info.SelectSingleNode("inf_ver").InnerText),
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
// For some reason, konmai decide to use int32 with thousands value for the BPM, e.g 18000 to represent 180.00 BPM
|
|
|
|
|
// Why? I don't know, probably the usual fuckery
|
2020-04-18 22:24:48 +02:00
|
|
|
|
string bpmMax = info.SelectSingleNode("bpm_max").InnerText;
|
|
|
|
|
header.BpmMax = double.Parse($"{bpmMax.Substring(0, bpmMax.Length - 2)}.{bpmMax.Substring(bpmMax.Length - 2)}");
|
2023-01-06 14:58:29 +01:00
|
|
|
|
header.BpmMax = header.BpmMax >= 1000 ? header.BpmMax / 100D : header.BpmMax;
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
|
|
|
|
string bpmMin = info.SelectSingleNode("bpm_min").InnerText;
|
|
|
|
|
header.BpmMin = double.Parse($"{bpmMin.Substring(0, bpmMin.Length - 2)}.{bpmMin.Substring(bpmMin.Length - 2)}");
|
2023-01-06 14:58:29 +01:00
|
|
|
|
header.BpmMin = header.BpmMin >= 1000 ? header.BpmMin / 100D : header.BpmMin;
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
|
|
|
|
header.Levels = new Dictionary<Difficulty, VoxLevelHeader>();
|
|
|
|
|
foreach (XmlNode diffInfo in music.SelectSingleNode("difficulty"))
|
|
|
|
|
{
|
|
|
|
|
Difficulty current;
|
|
|
|
|
if (diffInfo.Name == "novice")
|
|
|
|
|
current = Difficulty.Novice;
|
|
|
|
|
else if (diffInfo.Name == "advanced")
|
|
|
|
|
current = Difficulty.Advanced;
|
|
|
|
|
else if (diffInfo.Name == "exhaust")
|
|
|
|
|
current = Difficulty.Exhaust;
|
|
|
|
|
else if (diffInfo.Name == "infinite" || diffInfo.Name == "maximum")
|
|
|
|
|
current = Difficulty.Infinite;
|
|
|
|
|
else
|
|
|
|
|
continue; // Unknown difficulty,
|
|
|
|
|
|
|
|
|
|
var lvHeader = new VoxLevelHeader
|
|
|
|
|
{
|
|
|
|
|
Difficulty = current,
|
|
|
|
|
Level = int.Parse(diffInfo.SelectSingleNode("difnum").InnerText),
|
|
|
|
|
Effector = diffInfo.SelectSingleNode("effected_by").InnerText,
|
|
|
|
|
Illustrator = diffInfo.SelectSingleNode("illustrator").InnerText,
|
|
|
|
|
Price = int.Parse(diffInfo.SelectSingleNode("price").InnerText),
|
2023-01-06 14:58:29 +01:00
|
|
|
|
Limited = int.Parse(diffInfo.SelectSingleNode("limited").InnerText),
|
|
|
|
|
JacketPrint = int.Parse(diffInfo.SelectSingleNode("jacket_print").InnerText),
|
|
|
|
|
JacketMask = int.Parse(diffInfo.SelectSingleNode("jacket_mask").InnerText),
|
2020-04-18 22:24:48 +02:00
|
|
|
|
};
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
if (diffInfo.SelectSingleNode("radar") != null)
|
|
|
|
|
{
|
|
|
|
|
var radar = diffInfo.SelectSingleNode("radar");
|
|
|
|
|
lvHeader.Radar = new VoxLevelRadar()
|
|
|
|
|
{
|
|
|
|
|
Notes = byte.Parse(radar.SelectSingleNode("notes").InnerText),
|
|
|
|
|
Peak = byte.Parse(radar.SelectSingleNode("peak").InnerText),
|
|
|
|
|
Lasers = byte.Parse(radar.SelectSingleNode("tsumami").InnerText),
|
|
|
|
|
Tricky = byte.Parse(radar.SelectSingleNode("tricky").InnerText),
|
|
|
|
|
HandTrip = byte.Parse(radar.SelectSingleNode("hand-trip").InnerText),
|
|
|
|
|
OneHand = byte.Parse(radar.SelectSingleNode("one-hand").InnerText),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-18 22:24:48 +02:00
|
|
|
|
if (lvHeader.Level <= 0)
|
|
|
|
|
continue; // Invalid level?
|
|
|
|
|
|
|
|
|
|
header.Levels[current] = lvHeader;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Add(header);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Save(string filename)
|
|
|
|
|
{
|
|
|
|
|
var doc = new XmlDocument();
|
|
|
|
|
var master = doc.CreateElement("mdb");
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
var createMetaElement = new Func<XmlDocument, string, string, string, XmlElement>((d, n, v, t) =>
|
2020-04-18 22:24:48 +02:00
|
|
|
|
{
|
|
|
|
|
var prop = doc.CreateElement(n);
|
2023-01-06 14:58:29 +01:00
|
|
|
|
if (v != null)
|
|
|
|
|
prop.AppendChild(doc.CreateTextNode(v));
|
|
|
|
|
|
2020-04-18 22:24:48 +02:00
|
|
|
|
if (!string.IsNullOrEmpty(t))
|
|
|
|
|
prop.SetAttribute("__type", t);
|
|
|
|
|
|
|
|
|
|
return prop;
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
foreach (var header in _headers.Values)
|
2020-04-18 22:24:48 +02:00
|
|
|
|
{
|
|
|
|
|
if (header == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var music = doc.CreateElement("music");
|
2023-01-06 14:58:29 +01:00
|
|
|
|
music.SetAttribute("id", header.Id.ToString());
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
|
|
|
|
var info = doc.CreateElement("info");
|
2023-01-06 14:58:29 +01:00
|
|
|
|
info.AppendChild(createMetaElement(doc, "label", header.Id.ToString(), string.Empty));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "title_name", header.Title, string.Empty));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "title_yomigana", header.TitleYomigana, string.Empty));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "artist_name", header.Artist, string.Empty));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "artist_yomigana", header.ArtistYomigana, string.Empty));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "ascii", header.Ascii, string.Empty));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "bpm_max", header.BpmMax.ToString("000.00").Replace(".", string.Empty).Replace(",", string.Empty), "u32"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "bpm_min", header.BpmMin.ToString("000.00").Replace(".", string.Empty).Replace(",", string.Empty), "u32"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "distribution_date", header.DistributionDate.ToString("yyyyMMdd"), "u32"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "volume", header.Volume.ToString(), "u16"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "bg_no", header.BackgroundId.ToString(), "u16"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "genre", header.GenreId.ToString(), "u8"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "is_fixed", "1", "u8"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "version", ((int)header.Version).ToString(), "u8"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "demo_pri", "0", "s8"));
|
|
|
|
|
info.AppendChild(createMetaElement(doc, "inf_ver", ((int)header.InfVersion).ToString(), "u8"));
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
|
|
|
|
var diffInfo = doc.CreateElement("difficulty");
|
|
|
|
|
foreach (Difficulty difficulty in Enum.GetValues(typeof(Difficulty)))
|
|
|
|
|
{
|
|
|
|
|
string diffName = Enum.GetName(typeof(Difficulty), difficulty).ToLower();
|
2023-01-06 14:58:29 +01:00
|
|
|
|
if (difficulty == Difficulty.Infinite && header.InfVersion == InfiniteVersion.Mxm)
|
2020-04-18 22:24:48 +02:00
|
|
|
|
diffName = "maximum";
|
|
|
|
|
|
|
|
|
|
var detail = doc.CreateElement(diffName);
|
|
|
|
|
var lvHeader = new VoxLevelHeader() { Level = 0 };
|
|
|
|
|
if (header.Levels.ContainsKey(difficulty))
|
|
|
|
|
lvHeader = header.Levels[difficulty];
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
detail.AppendChild(createMetaElement(doc, "difnum", lvHeader.Level.ToString(), "u8"));
|
|
|
|
|
detail.AppendChild(createMetaElement(doc, "illustrator", lvHeader.Illustrator, string.Empty));
|
|
|
|
|
detail.AppendChild(createMetaElement(doc, "effected_by", lvHeader.Effector, string.Empty));
|
|
|
|
|
detail.AppendChild(createMetaElement(doc, "price", lvHeader.Price.ToString(), "s32"));
|
|
|
|
|
detail.AppendChild(createMetaElement(doc, "limited", lvHeader.Limited.ToString(), "u8"));
|
|
|
|
|
detail.AppendChild(createMetaElement(doc, "jacket_print", lvHeader.JacketPrint.ToString(), "s32"));
|
|
|
|
|
detail.AppendChild(createMetaElement(doc, "jacket_mask", lvHeader.JacketMask.ToString(), "s32"));
|
|
|
|
|
|
|
|
|
|
if (lvHeader.Radar != null)
|
|
|
|
|
{
|
|
|
|
|
var radar = detail.AppendChild(createMetaElement(doc, "radar", null, null));
|
|
|
|
|
radar.AppendChild(createMetaElement(doc, "notes", lvHeader.Radar.Notes.ToString(), "u8"));
|
|
|
|
|
radar.AppendChild(createMetaElement(doc, "peak", lvHeader.Radar.Peak.ToString(), "u8"));
|
|
|
|
|
radar.AppendChild(createMetaElement(doc, "tsumami", lvHeader.Radar.Lasers.ToString(), "u8"));
|
|
|
|
|
radar.AppendChild(createMetaElement(doc, "tricky", lvHeader.Radar.Tricky.ToString(), "u8"));
|
|
|
|
|
radar.AppendChild(createMetaElement(doc, "hand-trip", lvHeader.Radar.HandTrip.ToString(), "u8"));
|
|
|
|
|
radar.AppendChild(createMetaElement(doc, "one-hand", lvHeader.Radar.OneHand.ToString(), "u8"));
|
|
|
|
|
}
|
2020-04-18 22:24:48 +02:00
|
|
|
|
|
|
|
|
|
diffInfo.AppendChild(detail);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
music.AppendChild(info);
|
|
|
|
|
music.AppendChild(diffInfo);
|
|
|
|
|
|
|
|
|
|
master.AppendChild(music);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
var settings = new XmlWriterSettings { Indent = true, Encoding = DefaultEncoding };
|
2020-04-18 22:24:48 +02:00
|
|
|
|
using (var output = new StreamWriter(filename, false, DefaultEncoding))
|
|
|
|
|
using (var writer = XmlWriter.Create(output, settings))
|
|
|
|
|
{
|
|
|
|
|
doc.AppendChild(master);
|
|
|
|
|
doc.Save(writer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Add(VoxHeader header)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
if (_max < header.Id)
|
|
|
|
|
_max = header.Id;
|
2020-04-19 10:02:55 +02:00
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
_headers[header.Id] = header;
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Remove(int id)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
if (_max == id)
|
|
|
|
|
_max = 0;
|
2020-04-19 10:02:55 +02:00
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
_headers.Remove(id);
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Remove(VoxHeader header)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
Remove(header.Id);
|
2020-04-19 10:02:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool Contains(int id)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
return _headers.ContainsKey(id);
|
2020-04-19 10:02:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool Contains(VoxHeader header)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
return Contains(header.Id);
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public VoxHeader GetHeader(int id)
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
return _headers[id];
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Clear()
|
|
|
|
|
{
|
|
|
|
|
// Clear the list but not the last id
|
2023-01-06 14:58:29 +01:00
|
|
|
|
_headers.Clear();
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IEnumerator<VoxHeader> GetEnumerator()
|
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
return _headers.Values.GetEnumerator();
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 14:58:29 +01:00
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
2020-04-18 22:24:48 +02:00
|
|
|
|
{
|
2023-01-06 14:58:29 +01:00
|
|
|
|
return _headers.Values.GetEnumerator();
|
2020-04-18 22:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|