diff --git a/Assets/Cryville/Common/Math/FractionUtils.cs b/Assets/Cryville/Common/Math/FractionUtils.cs new file mode 100644 index 0000000..a2d26ed --- /dev/null +++ b/Assets/Cryville/Common/Math/FractionUtils.cs @@ -0,0 +1,52 @@ +using System; + +namespace Cryville.Common.Math { + /// + /// Provides a set of methods related to fractions. + /// + public static class FractionUtils { + /// + /// Converts a decimal to a fraction. + /// + /// The decimal. + /// The error. + /// The numerator. + /// The denominator. + /// is less than 0 or is not greater than 0 or not less than 1. + public static void ToFraction(double value, double error, out int n, out int d) { + if (value < 0.0) + throw new ArgumentOutOfRangeException("value", "Must be >= 0."); + if (error <= 0.0 || error >= 1.0) + throw new ArgumentOutOfRangeException("accuracy", "Must be > 0 and < 1."); + + int num = (int)System.Math.Floor(value); + value -= num; + + if (value < error) { n = num; d = 1; return; } + if (1 - error < value) { n = num + 1; d = 1; return; } + + int lower_n = 0; + int lower_d = 1; + int upper_n = 1; + int upper_d = 1; + while (true) { + int middle_n = lower_n + upper_n; + int middle_d = lower_d + upper_d; + + if (middle_d * (value + error) < middle_n) { + upper_n = middle_n; + upper_d = middle_d; + } + else if (middle_n < (value - error) * middle_d) { + lower_n = middle_n; + lower_d = middle_d; + } + else { + n = num * middle_d + middle_n; + d = middle_d; + return; + } + } + } + } +} diff --git a/Assets/Cryville/Common/Math/FractionUtils.cs.meta b/Assets/Cryville/Common/Math/FractionUtils.cs.meta new file mode 100644 index 0000000..73b8154 --- /dev/null +++ b/Assets/Cryville/Common/Math/FractionUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6829ada596979a545a935785eeea2972 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Cryville/Crtr/Extensions/Bestdori.meta b/Assets/Cryville/Crtr/Extensions/Bestdori.meta new file mode 100644 index 0000000..ae4a8db --- /dev/null +++ b/Assets/Cryville/Crtr/Extensions/Bestdori.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b9bd9e24d7c553341a2a12391843542f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Cryville/Crtr/Extensions/Bestdori/BestdoriChartConverter.cs b/Assets/Cryville/Crtr/Extensions/Bestdori/BestdoriChartConverter.cs new file mode 100644 index 0000000..6b574ba --- /dev/null +++ b/Assets/Cryville/Crtr/Extensions/Bestdori/BestdoriChartConverter.cs @@ -0,0 +1,183 @@ +using Cryville.Common; +using Cryville.Common.Math; +using Cryville.Crtr.Browsing; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Cryville.Crtr.Extensions.Bestdori { + public class BestdoriChartConverter : ResourceConverter { + static readonly string[] SUPPORTED_FORMATS = { ".json" }; + public override string[] GetSupportedFormats() { + return SUPPORTED_FORMATS; + } + + public override IEnumerable ConvertFrom(FileInfo file) { + List result = new List(); + List src; + using (var reader = new StreamReader(file.FullName)) { + src = JsonConvert.DeserializeObject>(reader.ReadToEnd()); + } + var group = new Chart.Group() { + tracks = new List(), + notes = new List(), + motions = new List(), + }; + Chart chart = new Chart { + format = 2, + time = new BeatTime(0, 0, 1), + ruleset = "bang_dream_girls_band_party", + sigs = new List(), + sounds = new List(), + motions = new List(), + groups = new List { group }, + }; + string bgm = null; + double? cbpm = null; + double pbeat = 0, ctime = 0; + double endbeat = 0; + foreach (var ev in src) { + double cbeat = ev.StartBeat; + ctime += cbpm == null ? 0 : (cbeat - pbeat) / cbpm.Value * 60; + pbeat = cbeat; + if (cbeat > endbeat) endbeat = cbeat; + if (ev is BestdoriChartEvent.System) { + if (bgm != null) continue; + var tev = (BestdoriChartEvent.System)ev; + bgm = StringUtils.TrimExt(tev.data); + var name = "bang_dream_girls_band_party__" + bgm; + result.Add(new SongResource(name, new FileInfo(Path.Combine(file.Directory.FullName, tev.data)))); + chart.sounds.Add(new Chart.Sound { time = ToBeatTime(tev.beat), id = name }); + } + else if (ev is BestdoriChartEvent.BPM) { + var tev = (BestdoriChartEvent.BPM)ev; + cbpm = tev.bpm; + chart.sigs.Add(new Chart.Signature { time = ToBeatTime(tev.beat), tempo = (float)tev.bpm }); + } + else if (ev is BestdoriChartEvent.Single) { + var tev = (BestdoriChartEvent.Single)ev; + group.notes.Add(new Chart.Note { + time = ToBeatTime(tev.beat), + judges = new List { new Chart.Judge { name = tev.flick ? "single_flick" : "single" } }, + motions = new List { new Chart.Motion { motion = "track:" + tev.lane.ToString() } }, + }); + } + else if (ev is BestdoriChartEvent.Long) { + var tev = (BestdoriChartEvent.Long)ev; + var c1 = tev.connections[tev.connections.Count - 1]; + var note = new Chart.Note { + time = ToBeatTime(tev.connections[0].beat), + endtime = ToBeatTime(c1.beat), + judges = new List(), + }; + for (int i = 0; i < tev.connections.Count; i++) { + BestdoriChartEvent.Connection c = tev.connections[i]; + note.motions.Add(new Chart.Motion { motion = "track:" + c.lane.ToString() }); + if (i == 0) + note.judges.Add(new Chart.Judge { name = "single" }); + else if (i == tev.connections.Count - 1) + note.judges.Add(new Chart.Judge { time = ToBeatTime(c.beat), name = c.flick ? "longend_flick" : "longend" }); + else if (!c.hidden) + note.judges.Add(new Chart.Judge { time = ToBeatTime(c.beat), name = "longnode" }); + } + if (c1.beat > endbeat) endbeat = c1.beat; + group.notes.Add(note); + } + else throw new NotImplementedException("Unsupported event: " + ev.type); + } + if (bgm == null) throw new FormatException("Chart contains no song"); + chart.endtime = ToBeatTime(endbeat); + result.Add(new RawChartResource(string.Format("bang_dream_girls_band_party__{0}__{1}", bgm, StringUtils.TrimExt(file.Name)), chart, new ChartMeta { + name = string.Format("Bandori {0} {1}", bgm, StringUtils.TrimExt(file.Name)), + author = "©BanG Dream! Project ©Craft Egg Inc. ©bushiroad", + ruleset = "bang_dream_girls_band_party", + note_count = group.notes.Count, + length = (float)ctime, + song = new SongMetaInfo { + name = bgm, + author = "©BanG Dream! Project ©Craft Egg Inc. ©bushiroad", + } + })); + return result; + } + + BeatTime ToBeatTime(double beat, double error = 1e-4) { + int i, n, d; + FractionUtils.ToFraction(beat, error, out n, out d); + i = n / d; n %= d; + return new BeatTime(i, n, d); + } + } + +#pragma warning disable IDE1006 + [JsonConverter(typeof(BestdoriChartEventCreator))] + abstract class BestdoriChartEvent { + public abstract string type { get; } + public abstract double StartBeat { get; } + public abstract class InstantEvent : BestdoriChartEvent { + public double beat; + public override double StartBeat { get { return beat; } } + } + public class BPM : InstantEvent { + public override string type { get { return "BPM"; } } + public double bpm; + } + public class System : InstantEvent { + public override string type { get { return "System"; } } + public string data; + } + public abstract class SingleBase : InstantEvent { + public double lane; + public bool skill; + public bool flick; + } + public class Single : SingleBase { + public override string type { get { return "Single"; } } + } + public class Directional : SingleBase { + public override string type { get { return "Directional"; } } + public string direction; + public int width; + } + public class Connection : SingleBase { + public override string type { get { return null; } } + public bool hidden; + } + public class Long : BestdoriChartEvent { + public override string type { get { return "Long"; } } + public List connections; + public override double StartBeat { get { return connections[0].beat; } } + } + public class Slide : Long { + public override string type { get { return "Slide"; } } + } + } +#pragma warning restore IDE1006 + class BestdoriChartEventCreator : CustomCreationConverter { + string _currentType; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { + var obj = JObject.ReadFrom(reader); + var type = obj["type"]; + if (type == null) _currentType = null; + else _currentType = obj["type"].ToObject(); + return base.ReadJson(obj.CreateReader(), objectType, existingValue, serializer); + } + + public override BestdoriChartEvent Create(Type objectType) { + switch (_currentType) { + case "BPM": return new BestdoriChartEvent.BPM(); + case "System": return new BestdoriChartEvent.System(); + case "Single": return new BestdoriChartEvent.Single(); + case "Directional": return new BestdoriChartEvent.Directional(); + case null: return new BestdoriChartEvent.Connection(); + case "Long": return new BestdoriChartEvent.Long(); + case "Slide": return new BestdoriChartEvent.Slide(); + default: throw new ArgumentException("Unknown event type: " + _currentType); + } + } + } +} diff --git a/Assets/Cryville/Crtr/Extensions/Bestdori/BestdoriChartConverter.cs.meta b/Assets/Cryville/Crtr/Extensions/Bestdori/BestdoriChartConverter.cs.meta new file mode 100644 index 0000000..4ce9e63 --- /dev/null +++ b/Assets/Cryville/Crtr/Extensions/Bestdori/BestdoriChartConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3c5a8bf05d5e284ba498e91cb0dd35e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: