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: