mirror of
https://github.com/SirusDoma/VoxCharger.git
synced 2024-12-18 18:35:54 +01:00
761 lines
37 KiB
C#
761 lines
37 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace VoxCharger
|
|
{
|
|
public partial class Ksh
|
|
{
|
|
private static readonly Encoding DefaultEncoding = Encoding.GetEncoding("Shift_JIS");
|
|
private const string VolPositions = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmno";
|
|
|
|
public string Title { get; set; }
|
|
public string Artist { get; set; }
|
|
public string Effector { get; set; }
|
|
public string Illustrator { get; set; }
|
|
public string JacketFileName { get; set; }
|
|
public string MusicFileName { get; set; }
|
|
public int Volume { get; set; }
|
|
public Difficulty Difficulty { get; set; }
|
|
public int Level { get; set; }
|
|
public int MusicOffset { get; set; }
|
|
public int PreviewOffset { get; set; }
|
|
public float BpmMin { get; set; }
|
|
public float BpmMax { get; set; }
|
|
public string Background { get; set; }
|
|
|
|
public EventCollection Events { get; private set; } = new EventCollection();
|
|
|
|
public class ParseOption
|
|
{
|
|
public bool RealignOffset { get; set; } = false;
|
|
public bool EnableChipFx { get; set; } = true;
|
|
public bool EnableLongFx { get; set; } = true;
|
|
public bool EnableCamera { get; set; } = true;
|
|
public bool EnableSlamImpact { get; set; } = true;
|
|
public bool EnableLaserTrack { get; set; } = true;
|
|
public bool EnableButtonTrack { get; set; } = true;
|
|
}
|
|
|
|
public Ksh()
|
|
{
|
|
}
|
|
|
|
public void Parse(string fileName, ParseOption opt = null)
|
|
{
|
|
// I pulled all nighters for few days, all for this piece of trash codes :)
|
|
// Reminder: there's lot of "offset" that can be ambigous between many contexts everywhere
|
|
var lines = File.ReadAllLines(fileName, DefaultEncoding);
|
|
|
|
if (opt == null)
|
|
opt = new ParseOption();
|
|
|
|
Time time = new Time(1, 1, 0);
|
|
var signature = new Event.TimeSignature(time, 4, 4);
|
|
var filter = Event.LaserFilter.Peak;
|
|
int rangeLeft = 0;
|
|
int rangeRight = 0;
|
|
var cameras = new Dictionary<Camera.WorkType, Camera>();
|
|
var lastCamera = new Dictionary<Camera.WorkType, Camera>();
|
|
var hitFx = new Dictionary<Event.ButtonTrack, KshSoundEffect>();
|
|
var holdFx = new Dictionary<Event.ButtonTrack, Effect>();
|
|
var longNotes = new Dictionary<Event.ButtonTrack, Event.Button>();
|
|
var lasers = new Dictionary<Event.LaserTrack, List<Event.Laser>>();
|
|
|
|
int offset = 0;
|
|
int measure = 0;
|
|
|
|
int measureCount = lines.Count(l => l.Trim() == "--");
|
|
int fxCount = 0;
|
|
int noteCount = 0;
|
|
|
|
for (int i = 0; i < lines.Length; i++)
|
|
{
|
|
string line = lines[i].Trim();
|
|
if (line.StartsWith("//"))
|
|
continue;
|
|
|
|
|
|
// First measure is 1,1,0 but initial counter start from 0 until actual measure started
|
|
if (measure > 0)
|
|
{
|
|
/** Time
|
|
* Behold! time conversion between ksh format to vox
|
|
*
|
|
* Vox Time Format:
|
|
* Timestamp value has beat to be matched with current signature
|
|
* However, the offset is base of 48, this might be lead into confusion in non standard signature
|
|
*
|
|
* For example:
|
|
* In 8 / 4 signature the maximum timestamp that means:
|
|
* - Maximum timestamp is 999 / 8 / 48 for each measure, beat and cells respectively
|
|
* - Value of beat adapt to the signature beat
|
|
* - May only contain 4 notes per beat in normal behavior since signature note is 4
|
|
*
|
|
* And since offset need to be base of 48 (not 192) this value need to be converted
|
|
* for example the 3rd note in the beat should be 36 and not 24
|
|
*
|
|
* Ksh Time Format:
|
|
* This too, depends on beat but the number of actual event definitions can be much greater than the beat itself
|
|
* for example, measure that has 8/4 signature may contains 441 events, though much of them are just padding
|
|
* this padding required since the event didn't define timestamp in each line (easier to debug with larger file size tradeoff)
|
|
*/
|
|
|
|
float position = (measure * 192f) + ((offset / (float)noteCount) * 192f);
|
|
if (opt.RealignOffset)
|
|
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!
|
|
if (time.Measure < 0)
|
|
continue; // Might happen when try to realign music offset
|
|
|
|
if (time.Beat > signature.Beat)
|
|
Debug.WriteLine($"[KSH] {i + 1:D4}: Measure has more than {signature.Beat} beats");
|
|
}
|
|
|
|
if (line.Contains("="))
|
|
{
|
|
// Might just pull it to first bar
|
|
if (time.Measure == 0)
|
|
time = new Time(1, 1, 0);
|
|
|
|
var data = line.Split('=');
|
|
if (data.Length < 2)
|
|
{
|
|
Debug.WriteLine($"[KSH] {i+1:D4}: Invalid attribute");
|
|
continue;
|
|
}
|
|
|
|
string prop = data[0].Trim().ToLower();
|
|
string value = data[1].Trim();
|
|
var param = value.Split(';');
|
|
if (string.IsNullOrEmpty(value))
|
|
Debug.WriteLine($"[KSH] {i + 1:D4}: {prop} value is empty (reset value)");
|
|
|
|
/** WARNING !!!
|
|
* Cancer code ahead, proceed with caution!
|
|
* Well.. the rest of codes aren't any better either lol
|
|
*/
|
|
int maxDuration = (measureCount - measure) * 192;
|
|
switch (prop) // At first i just want try to use switch-case pattern, turns out became cancerous (indent and) code lol
|
|
{
|
|
#region --- Metadata ---
|
|
case "title": Title = value; break;
|
|
case "artist": Artist = value; break;
|
|
case "effect": Effector = value; break;
|
|
case "illustrator": Illustrator = value; break;
|
|
case "jacket": JacketFileName = value; break;
|
|
case "m": MusicFileName = value; break;
|
|
case "po" when int.TryParse(value, out int po): PreviewOffset = po; break;
|
|
case "mvol" when int.TryParse(value, out int vol): Volume = vol; break;
|
|
case "level" when int.TryParse(value, out int lv): Level = lv; break;
|
|
case "difficulty" when value == "light": Difficulty = Difficulty.Novice; break;
|
|
case "difficulty" when value == "challenge": Difficulty = Difficulty.Advanced; break;
|
|
case "difficulty" when value == "extended": Difficulty = Difficulty.Exhaust; break;
|
|
case "difficulty" when value == "infinite": Difficulty = Difficulty.Infinite; break;
|
|
case "bg" when string.IsNullOrEmpty(Background):
|
|
case "layer" when string.IsNullOrEmpty(Background):
|
|
Background = value;
|
|
break;
|
|
#endregion
|
|
#region --- Event Info ---
|
|
case "o" when int.TryParse(value, out int o):
|
|
MusicOffset = o;
|
|
|
|
// Should be fine if there's no bpm change
|
|
if (MusicOffset % 48 != 0 && opt.RealignOffset)
|
|
Debug.WriteLine($"[KSH] Music Offset is not base of 48 signature (Offset: {MusicOffset})");
|
|
|
|
break;
|
|
case "t":
|
|
foreach (string ts in value.Split('-'))
|
|
{
|
|
if (!float.TryParse(ts, out float t))
|
|
break;
|
|
|
|
if (BpmMin == 0f || t < BpmMin)
|
|
BpmMin = t;
|
|
|
|
if (BpmMax == 0f || t > BpmMax)
|
|
BpmMax = t;
|
|
|
|
if (!value.Contains("-"))
|
|
{
|
|
var bpm = new Event.BPM(new Time(time.Measure, time.Beat, 0), t); // beat still acceptable
|
|
Events.Add(bpm);
|
|
}
|
|
}
|
|
|
|
// BPM change with unaligned music offset, proceed with caution!
|
|
if (BpmMin != BpmMax && MusicOffset % 48 != 0 && opt.RealignOffset)
|
|
Debug.WriteLine($"[KSH] BPM change with unaligned music offset");
|
|
|
|
break;
|
|
case "stop" when float.TryParse(value, out float duration):
|
|
Event.BPM start = null;
|
|
time = new Time(time.Measure, time.Beat, 0);
|
|
foreach (var ev in Events[time])
|
|
{
|
|
if (ev is Event.BPM x)
|
|
{
|
|
start = x;
|
|
start.IsStop = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (start == null)
|
|
{
|
|
var last = Events.GetBPM(time);
|
|
if (last == null)
|
|
break;
|
|
|
|
start = new Event.BPM(time, last.Value);
|
|
start.IsStop = true;
|
|
Events.Add(start);
|
|
}
|
|
|
|
Event.BPM end = null;
|
|
var target = Time.FromOffset((int)duration, signature);
|
|
foreach (var ev in Events[target])
|
|
{
|
|
if (ev is Event.BPM x)
|
|
{
|
|
end = x;
|
|
end.IsStop = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (end == null)
|
|
{
|
|
var last = Events.GetBPM(target);
|
|
if (last == null)
|
|
break;
|
|
|
|
end = new Event.BPM(target, last.Value);
|
|
end.IsStop = false;
|
|
Events.Add(end);
|
|
}
|
|
|
|
// Stop with unaligned music offset can break other stuffs too
|
|
if (MusicOffset % 48 != 0 && opt.RealignOffset)
|
|
Debug.WriteLine($"[KSH] Stop event unaligned music offset");
|
|
|
|
break;
|
|
case "beat":
|
|
var sig = value.Split('/');
|
|
if (sig.Length != 2 || !int.TryParse(sig[0], out int b) || !int.TryParse(sig[1], out int n))
|
|
break;
|
|
|
|
signature = new Event.TimeSignature(new Time(time.Measure, 1, 0), b, n); // beat unacceptable, only at start measure
|
|
Events.Add(signature);
|
|
|
|
break;
|
|
case "fx-l":
|
|
case "fx-r":
|
|
if (!opt.EnableLongFx)
|
|
break;
|
|
|
|
var htrack = prop == "fx-l" ? Event.ButtonTrack.FxL : Event.ButtonTrack.FxR;
|
|
holdFx[htrack] = new Effect();
|
|
|
|
var fx = Effect.FromKsh(value);
|
|
if (fx != null)
|
|
{
|
|
fx.Id = ++fxCount;
|
|
holdFx[htrack] = fx;
|
|
}
|
|
|
|
break;
|
|
case "fx-l_se":
|
|
case "fx-r_se":
|
|
if (!opt.EnableChipFx)
|
|
break;
|
|
|
|
var fxTrack = prop == "fx-l_se" ? Event.ButtonTrack.FxL : Event.ButtonTrack.FxR;
|
|
hitFx[fxTrack] = new KshSoundEffect();
|
|
|
|
if (Enum.TryParse(param[0], true, out Event.ChipFx hit))
|
|
hitFx[fxTrack].Effect = hit;
|
|
|
|
if (param.Length >= 2 && int.TryParse(param[1], out int hitCount))
|
|
hitFx[fxTrack].HitCount = hitCount + 1;
|
|
else
|
|
hitFx[fxTrack].HitCount = 1;
|
|
|
|
break;
|
|
case "filtertype":
|
|
switch (value.Trim())
|
|
{
|
|
case "peak": filter = Event.LaserFilter.Peak; break;
|
|
case "hpf1": filter = Event.LaserFilter.HighPass; break;
|
|
case "lpf1": filter = Event.LaserFilter.LowPass; break;
|
|
case var f when f.Contains("bitc"):
|
|
filter = Event.LaserFilter.BitCrusher;
|
|
break;
|
|
default:
|
|
filter = (Event.LaserFilter)6;
|
|
break;
|
|
}
|
|
|
|
break;
|
|
case "laserrange_l" when int.TryParse(value.Replace("x", ""), out int r): rangeLeft = r; break;
|
|
case "laserrange_r" when int.TryParse(value.Replace("x", ""), out int r): rangeRight = r; break;
|
|
#endregion
|
|
#region --- Camera ---
|
|
case "tilt" when !float.TryParse(value.Trim(), out float _):
|
|
if (!opt.EnableCamera)
|
|
break;
|
|
|
|
switch (value.Trim()) // everything untested
|
|
{
|
|
case "normal":
|
|
Events.Add(new Event.TiltMode(time, Event.TiltType.Normal));
|
|
break;
|
|
case "bigger":
|
|
Events.Add(new Event.TiltMode(time, Event.TiltType.Large));
|
|
break;
|
|
case "keep_bigger":
|
|
Events.Add(new Event.TiltMode(time, Event.TiltType.Incremental));
|
|
break;
|
|
}
|
|
|
|
break;
|
|
case "tilt": // Tilt
|
|
case "zoom_top": // CAM_RotX
|
|
case "zoom_bottom": // CAM_Radi
|
|
case "lane_toggle": // LaneY
|
|
if (!opt.EnableCamera)
|
|
break;
|
|
|
|
if (!float.TryParse(value, out float cameraOffset))
|
|
break;
|
|
|
|
Camera.WorkType work = Camera.WorkType.None;
|
|
switch (prop)
|
|
{
|
|
case "zoom_top":
|
|
work = Camera.WorkType.Rotation;
|
|
cameraOffset /= 150.0f;
|
|
break;
|
|
case "zoom_bottom":
|
|
work = Camera.WorkType.Radian;
|
|
cameraOffset /= -150.0f;
|
|
break;
|
|
case "tilt":
|
|
work = Camera.WorkType.Tilt;
|
|
cameraOffset /= 1.0f; // untested
|
|
break;
|
|
case "lane_toggle":
|
|
work = Camera.WorkType.LaneClear; // untested too
|
|
break;
|
|
}
|
|
|
|
Camera camera = null;
|
|
switch (work)
|
|
{
|
|
case Camera.WorkType.Rotation:
|
|
case Camera.WorkType.Radian:
|
|
case Camera.WorkType.Tilt:
|
|
camera = Camera.Create(work, time, maxDuration, cameraOffset, cameraOffset);
|
|
break;
|
|
case Camera.WorkType.LaneClear:
|
|
camera = new Camera.LaneClear(time, (int)cameraOffset, 0.00f, 1024.00f);
|
|
break;
|
|
}
|
|
|
|
if (!cameras.ContainsKey(work))
|
|
{
|
|
cameras[work] = null;
|
|
lastCamera[work] = camera;
|
|
Events.Add(camera);
|
|
|
|
continue;
|
|
}
|
|
|
|
var pairCamera = cameras[work];
|
|
if (pairCamera == null)
|
|
{
|
|
switch (work)
|
|
{
|
|
case Camera.WorkType.Rotation:
|
|
case Camera.WorkType.Radian:
|
|
case Camera.WorkType.Tilt:
|
|
cameras[work] = Camera.Create(work, time, -1, cameraOffset, cameraOffset);
|
|
break;
|
|
case Camera.WorkType.LaneClear:
|
|
var ev = new Camera.LaneClear(time, (int)cameraOffset, 1024.00f, 0.00f);
|
|
cameras[work] = ev;
|
|
Events.Add(ev);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Can't really use Time.Difference since time signature can be different across different measures
|
|
float curOffset = time.GetAbsoluteOffset(signature);
|
|
float pairOffset = pairCamera.Time.GetAbsoluteOffset(Events.GetTimeSignature(pairCamera.Time));
|
|
|
|
if (work == Camera.WorkType.LaneClear)
|
|
{
|
|
camera.Start = pairCamera.End;
|
|
camera.End = pairCamera.Start;
|
|
|
|
if ((int)Math.Abs(curOffset - pairOffset) != pairCamera.Duration)
|
|
{
|
|
var fill = new Camera.LaneClear(null, 0, pairCamera.End, camera.Start);
|
|
var fillOffset = pairOffset + pairCamera.Duration;
|
|
var timeSig = Events.GetTimeSignature((int)(fillOffset / 192f));
|
|
|
|
fill.Time = Time.FromOffset(fillOffset, timeSig);
|
|
fill.Duration = (int)Math.Abs(curOffset - fill.Time.GetAbsoluteOffset(timeSig));
|
|
Events.Add(fill);
|
|
}
|
|
|
|
cameras[work] = camera;
|
|
Events.Add(camera);
|
|
|
|
break;
|
|
}
|
|
|
|
// Assign camera properties
|
|
camera.Time = pairCamera.Time;
|
|
camera.Duration = (int)Math.Abs(curOffset - pairOffset);
|
|
camera.Start = pairCamera.Start;
|
|
|
|
// Find gap between last camera event
|
|
var prevCamera = lastCamera[work];
|
|
|
|
float lastOffset = prevCamera.Time.GetAbsoluteOffset(Events.GetTimeSignature(prevCamera.Time));
|
|
int diff = (int)Math.Abs(pairOffset - lastOffset);
|
|
|
|
// There's seems to be gap
|
|
if (prevCamera != null && prevCamera.Duration != diff)
|
|
{
|
|
// Check gap, is indeed something that we can fill
|
|
if (prevCamera.Duration < diff)
|
|
{
|
|
// Adjust time and duration to fill the gap, make sure to use correct time signature!
|
|
var fill = Camera.Create(work, time, 0, prevCamera.End, pairCamera.Start);
|
|
var fillOffset = lastOffset + prevCamera.Duration;
|
|
var timeSig = Events.GetTimeSignature((int)(fillOffset / 192f));
|
|
|
|
fill.Time = Time.FromOffset(fillOffset, timeSig);
|
|
fill.Duration = (int)Math.Abs(pairOffset - fill.Time.GetAbsoluteOffset(timeSig));
|
|
Events.Add(fill);
|
|
}
|
|
// The previous duration is overlap with current time, readjust previous event duration
|
|
else
|
|
{
|
|
prevCamera.Duration = diff;
|
|
prevCamera.End = camera.Start;
|
|
}
|
|
}
|
|
|
|
cameras[work] = null;
|
|
lastCamera[work] = camera;
|
|
Events.Add(camera);
|
|
}
|
|
|
|
break;
|
|
#endregion
|
|
}
|
|
}
|
|
else if (line == "--")
|
|
{
|
|
// Reset time counter
|
|
measure += 1;
|
|
offset = 0;
|
|
noteCount = 0;
|
|
|
|
// Reset laser range, unless there's active laser
|
|
if (!lasers.ContainsKey(Event.LaserTrack.Left))
|
|
rangeLeft = 1;
|
|
|
|
if (!lasers.ContainsKey(Event.LaserTrack.Right))
|
|
rangeRight = 1;
|
|
|
|
float position = measure * 192f;
|
|
if (measure > 1 && opt.RealignOffset)
|
|
position -= MusicOffset;
|
|
|
|
time = Time.FromOffset(position, signature);
|
|
for (int j = i + 1; j < lines.Length; j++)
|
|
{
|
|
string ln = lines[j];
|
|
if (char.IsDigit(ln[0]))
|
|
noteCount++;
|
|
else if (ln == "--")
|
|
break;
|
|
}
|
|
}
|
|
else if (char.IsDigit(line[0]))
|
|
{
|
|
// Increment offset when current line is event
|
|
offset++;
|
|
|
|
#region --- BT & FX ---
|
|
for (int channel = 0; channel < 7; channel++)
|
|
{
|
|
if (!opt.EnableButtonTrack)
|
|
break;
|
|
|
|
Event.ButtonTrack track;
|
|
switch(channel)
|
|
{
|
|
case 0: track = Event.ButtonTrack.A; break;
|
|
case 1: track = Event.ButtonTrack.B; break;
|
|
case 2: track = Event.ButtonTrack.C; break;
|
|
case 3: track = Event.ButtonTrack.D; break;
|
|
case 5: track = Event.ButtonTrack.FxL; break;
|
|
case 6: track = Event.ButtonTrack.FxR; break;
|
|
default: continue;
|
|
}
|
|
|
|
// FxL and FxR accept any char for long note
|
|
if (!int.TryParse(line[channel].ToString(), out int flag) && (track != Event.ButtonTrack.FxL && track != Event.ButtonTrack.FxR))
|
|
continue;
|
|
|
|
// Who's right in the mind to split fx and bt event logic just because the flag is opposite? lol
|
|
bool isFx = track == Event.ButtonTrack.FxL || track == Event.ButtonTrack.FxR;
|
|
bool isChip = ((isFx && flag == 2) || (!isFx && flag == 1)) && flag != 0;
|
|
bool isHold = ((isFx && flag != 2) || (!isFx && flag == 2)) && flag != 0;
|
|
|
|
if (isChip)
|
|
{
|
|
var hit = Event.ChipFx.None;
|
|
if (isFx)
|
|
{
|
|
if (!hitFx.ContainsKey(track))
|
|
hitFx[track] = new KshSoundEffect();
|
|
|
|
var fx = hitFx[track];
|
|
hit = fx.HitCount > fx.Used ? fx.Effect : Event.ChipFx.None;
|
|
|
|
fx.Used++;
|
|
}
|
|
|
|
// Overlapping long notes
|
|
if (longNotes.ContainsKey(track))
|
|
{
|
|
var ev = longNotes[track];
|
|
var timeSig = Events.GetTimeSignature(ev.Time.Measure);
|
|
ev.HoldLength = time.Difference(ev.Time, timeSig);
|
|
|
|
Events.Add(ev);
|
|
}
|
|
|
|
longNotes.Remove(track);
|
|
Events.Add(new Event.Button(time, track, 0, null, hit));
|
|
}
|
|
else if (isHold)
|
|
{
|
|
if (!longNotes.ContainsKey(track))
|
|
{
|
|
Effect fx = null;
|
|
if (isFx)
|
|
{
|
|
if (!holdFx.ContainsKey(track))
|
|
holdFx[track] = null;
|
|
|
|
if (holdFx[track] != null && holdFx[track].Type != FxType.None)
|
|
fx = holdFx[track];
|
|
}
|
|
|
|
longNotes[track] = new Event.Button(time, track, 0, fx, Event.ChipFx.None);
|
|
}
|
|
else
|
|
{
|
|
var ev = longNotes[track];
|
|
ev.HoldLength = Math.Abs(time.Difference(ev.Time, signature));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Empty event (Event flag: 0)
|
|
if (longNotes.ContainsKey(track))
|
|
{
|
|
var ev = longNotes[track];
|
|
var timeSig = Events.GetTimeSignature(ev.Time.Measure);
|
|
ev.HoldLength = time.Difference(ev.Time, timeSig);
|
|
|
|
Events.Add(longNotes[track]);
|
|
}
|
|
|
|
longNotes.Remove(track);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region --- Laser ---
|
|
foreach (var track in new[] { Event.LaserTrack.Left, Event.LaserTrack.Right })
|
|
{
|
|
if (!opt.EnableLaserTrack)
|
|
break;
|
|
|
|
int channel = track == Event.LaserTrack.Left ? 8 : 9;
|
|
int range = track == Event.LaserTrack.Left ? rangeLeft : rangeRight;
|
|
char flag = line[channel];
|
|
var impact = Event.SlamImpact.None;
|
|
|
|
// Ignore tick
|
|
if (flag == ':')
|
|
continue;
|
|
|
|
if (line.Length >= 13 && opt.EnableSlamImpact)
|
|
{
|
|
// Ignore direction, use slam offset instead
|
|
char f = line[10];
|
|
if (f == '@')
|
|
{
|
|
// Map the impact types
|
|
char spin = line[11];
|
|
if (spin == '(' || spin == ')')
|
|
impact = Event.SlamImpact.Measure;
|
|
else if (spin == '<' || spin == '>')
|
|
impact = Event.SlamImpact.HalfMeasure;
|
|
|
|
// Try to realign with impact length, seriously, why would impact has length in kshoot?
|
|
if (int.TryParse(line.Substring(12), out int length))
|
|
{
|
|
// This mapping may incorrect.. and stupid
|
|
if ((impact == Event.SlamImpact.Measure || impact == Event.SlamImpact.HalfMeasure) && length < 96)
|
|
impact = Event.SlamImpact.Swing;
|
|
else if (impact == Event.SlamImpact.Measure && length <= 144)
|
|
impact = Event.SlamImpact.ThreeBeat;
|
|
}
|
|
}
|
|
else if (f == 'S')
|
|
impact = Event.SlamImpact.Swing;
|
|
}
|
|
|
|
var laser = new Event.Laser(
|
|
time,
|
|
track,
|
|
0,
|
|
Event.LaserFlag.Start,
|
|
impact,
|
|
filter,
|
|
range
|
|
);
|
|
|
|
laser.Slam = noteCount >= 32;
|
|
if (flag != '-')
|
|
{
|
|
// Determine laser offset
|
|
for (int x = 0; x < VolPositions.Length; x++)
|
|
{
|
|
if (VolPositions[x] == flag)
|
|
laser.Offset = (int)((x / 51f) * 127f);
|
|
}
|
|
|
|
if (lasers.ContainsKey(track))
|
|
laser.Flag = Event.LaserFlag.Tick;
|
|
else
|
|
lasers[track] = new List<Event.Laser>();
|
|
|
|
lasers[track].Add(laser);
|
|
}
|
|
else if (lasers.ContainsKey(track))
|
|
{
|
|
var nodes = lasers[track];
|
|
lasers.Remove(track);
|
|
|
|
// There suppose to be 2 point of laser, no?
|
|
if (nodes.Count < 2)
|
|
continue;
|
|
|
|
/** Slam Laser
|
|
* Every lasers inside 1/32 that 6 cells apart or less are slam in kshoot
|
|
* However, Vox may produce bug when slam defined more than 2 laser events within certain amount of distance
|
|
* It seems the threshold determined based on current active bpm, the exact formula is still unclear
|
|
* Therefore, eliminate the duplicate / unnecessary ticks
|
|
*
|
|
* For example, this code will transform following events:
|
|
*
|
|
* 001,01,00 25 1 0 0 2 0
|
|
* 001,01,00 95 0 0 0 2 0 // -> Unnecessary event, since the next event is the same offset
|
|
* 001,01,06 95 0 0 0 2 0 // -> This cause bug because same offset and located within distance threshold
|
|
* 002,01,00 95 0 0 0 2 0
|
|
* 002,01,06 50 2 0 0 2 0
|
|
*
|
|
* To this:
|
|
*
|
|
* 001,01,00 25 1 0 0 2 0
|
|
* 001,01,06 95 0 0 0 2 0 // -> Keep 001,01,06 and throw one of the duplicate
|
|
* 002,01,00 95 0 0 0 2 0
|
|
* 002,01,00 50 2 0 0 2 0
|
|
*
|
|
*/
|
|
|
|
int counter = 0;
|
|
Event.Laser last = null;
|
|
foreach (var node in nodes.ToArray())
|
|
{
|
|
if (last == null)
|
|
{
|
|
last = node;
|
|
continue;
|
|
}
|
|
|
|
if (node.Offset == last.Offset)
|
|
counter++;
|
|
else
|
|
counter = 0;
|
|
|
|
if (counter >= 2)
|
|
nodes.Remove(last);
|
|
|
|
last = node;
|
|
}
|
|
|
|
// TODO: As inefficient as it gets lol, merge into previous loop if possible
|
|
for (int n = 0; n < nodes.Count; n++)
|
|
{
|
|
if (n + 1 >= nodes.Count)
|
|
break;
|
|
|
|
var start = nodes[n];
|
|
var end = nodes[n + 1];
|
|
var timeSig = Events.GetTimeSignature(start.Time);
|
|
|
|
// Due to terrible ksh format for slam that appear in 1/32 or shorter
|
|
// the output of conversion may have 6 cells gap apart each other
|
|
// TODO: Use offset instead of Time.Difference, cuz it might start and finish in different measure
|
|
if (start.Slam && end.Slam && end.Time.Difference(start.Time, timeSig) <= 6)
|
|
{
|
|
// Pull one of the offset
|
|
// In some rare cases, not pulling the offset will ended up normal laser instead of slam, especially in slow bpm
|
|
int min = Math.Min(start.Time.Offset, end.Time.Offset);
|
|
start.Time.Offset = end.Time.Offset = min;
|
|
|
|
n += 1;
|
|
}
|
|
}
|
|
|
|
// Don't forget to set the flag
|
|
nodes[0].Flag = Event.LaserFlag.Start;
|
|
last.Flag = Event.LaserFlag.End;
|
|
|
|
// TODO: Reset laser range when it's no longer active in current measure(?)
|
|
Events.Add(nodes.ToArray());
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|