diff --git a/Aff2Preview/AffTools/Aff2Preview/AffRenderer.cs b/Aff2Preview/AffTools/Aff2Preview/AffRenderer.cs index 84647fd..a3d4828 100644 --- a/Aff2Preview/AffTools/Aff2Preview/AffRenderer.cs +++ b/Aff2Preview/AffTools/Aff2Preview/AffRenderer.cs @@ -6,7 +6,7 @@ namespace AffTools.Aff2Preview; -internal class AffRenderer +internal partial class AffRenderer { private abstract class DrawObjectBase { @@ -20,18 +20,23 @@ private class ArcSegment : DrawObjectBase { public Vector3 End { get; init; } public int ColorId { get; init; } - public bool IsVoid { get; init; } + public ArcType ArcType { get; init; } public override void Draw(GraphicsAdapter g) { float drawX = IsEnwiden ? Location.X * 4 / 6 + 0.25f * Config.DrawingTrackWidth * 4 / 6 : Location.X; float endX = IsEnwiden ? End.X * 4 / 6 + 0.25f * Config.DrawingTrackWidth * 4 / 6 : End.X; ColorDesc color = new(); float w; - if (IsVoid) + if (ArcType == ArcType.Void) { w = Config.ArcVoidWidth; color.SetColor(Config.GetArcVoidColor()); } + else if (ArcType == ArcType.Designant) + { + w = Config.ArcVoidWidth; + color.SetColor(0xffa82860); + } else { w = Config.ArcWidth; @@ -45,14 +50,18 @@ public override void Draw(GraphicsAdapter g) private class ArcTap : DrawObjectBase { + public ArcType ArcType { get; init; } public override void Draw(GraphicsAdapter g) { var airTap = Property switch { - "voice_wav" => Config.Side == 1 ? Config.SfxDTap : Config.SfxLTap, - "glass_wav" => Config.Side == 1 ? Config.SfxDTap : Config.SfxLTap, - _ => Config.AirTap + "none" => Config.AirTap, + _ => Config.SoundAirTap }; + if (ArcType == ArcType.Designant) + { + airTap = Config.DesignantAirTap; + } int drawX = IsEnwiden ? (int)(Location.X * 4 / 6 + Config.EnwidenTrackWidth / 2 + 3) : (int)(Location.X - Config.SingleTrackWidth / 2 + 1); @@ -99,15 +108,23 @@ internal class ChartConfig public ImageDesc Tap = new GdiImage(); public ImageDesc Hold = new GdiImage(); public ImageDesc AirTap = new GdiImage(); - - public readonly ImageDesc SfxDTap = new GdiImage(); - public readonly ImageDesc SfxLTap = new GdiImage(); + public ImageDesc SoundAirTap = new GdiImage(); + public ImageDesc DesignantAirTap = new GdiImage(); public int NoteHeight { get; set; } = 64; public int SkyNoteHeight { get; set; } = 61; public float ArcWidth { get; set; } = 20f; public float ArcVoidWidth { get; set; } = 3f; + public float GlobalArcSamplingDensity + { + get; + set + { + field = Math.Max(1f, value); + } + } + public int TotalTrackWidth { get; set; } = 248; public int DrawingTrackWidth => TotalTrackWidth - 4; public int SingleTrackWidth => DrawingTrackWidth / 4; @@ -131,6 +148,7 @@ public uint TrackColor 0 => 0xffffffff, 1 => 0xff382a47, 2 => 0xffffffff, + 3 => 0xffffffff, _ => 0 }; @@ -140,6 +158,7 @@ public uint TrackLineColor 0 => 0xffd3d3d3, 1 => 0xff2e1f3c, 2 => 0xffd3d3d3, + 3 => 0xffd3d3d3, _ => 0 }; @@ -149,6 +168,7 @@ public uint TrackSegmentLineColor 0 => 0xa0d3d3d3, 1 => 0xc02e1f3c, 2 => 0xa0d3d3d3, + 3 => 0xa0d3d3d3, _ => 0 }; @@ -158,6 +178,7 @@ public uint TrackStripColor 0 => 0x0f808080, 1 => 0x08f0f8ff, 2 => 0x0f808080, + 3 => 0x0f808080, _ => 0 }; @@ -167,6 +188,7 @@ public uint GetArcVoidColor() 0 => 0xffd3d3d3, 1 => 0xffa9a9a9, 2 => 0xffd3d3d3, + 3 => 0xffd3d3d3, _ => 0, }; @@ -191,6 +213,12 @@ public uint GetArcColor(int type) 1 => 0xffff69b4, _ => 0, }, + 3 => type switch + { + 0 => 0xff31dae7, + 1 => 0xffff69b4, + _ => 0, + }, _ => 0xff31dae7, }; @@ -200,6 +228,7 @@ public uint GetConnectLineColor() 0 => 0xdc90ee90, 1 => 0xdcff1493, 2 => 0xdc90ee90, + 3 => 0xdc90ee90, _ => 0, }; @@ -237,10 +266,12 @@ public AffRenderer(string affFile) 1 => "Present", 2 => "Future", 3 => "Beyond", + 4 => "Eternal", _ => "" }; - public void LoadResource(string tap, string hold, string airTap, string bg, string cover) + public static void LoadResource(string tap, string hold, string airTap, string sfxAirTap, + string designantAirTap, string bg, string cover) { Config.Background.FromFile(bg); @@ -249,6 +280,9 @@ public void LoadResource(string tap, string hold, string airTap, string bg, stri Config.Tap.FromFile(tap); Config.Hold.FromFile(hold); Config.AirTap.FromFile(airTap); + Config.SoundAirTap.FromFile(sfxAirTap); + + Config.DesignantAirTap.FromFile(designantAirTap); // not always necessary. } private void MirrorAff() @@ -321,7 +355,9 @@ public ImageDesc Draw() { 0 => 0xc8f0f0f0, 1 => 0xc8202020, - 2 => 0xc8f0f0f0, + 2 => 0xc8f0f0f0, + 3 => 0xc8f0f0f0, + _ => throw new NotImplementedException(), }); GraphicsAdapter g = new GdiPlusAdapter(); @@ -351,6 +387,7 @@ public ImageDesc Draw() for (int x = 0; x < Config.Cols; x++) { double y = trackImg.GetHeight() - (x + 1) * Config.Rows * Config.SegmentLengthInBaseBpm; + g.DrawImageCliped(trackImg, x * colWidth + colWidth - Config.TotalTrackWidth + 50, 50, 0, (int)y, Config.TotalTrackWidth, (int)(Config.Rows * Config.SegmentLengthInBaseBpm)); } @@ -480,10 +517,10 @@ void DrawFloorNotes(GraphicsAdapter g) public void DrawAirObjects(GraphicsAdapter g) { - List airObjects = new(); - List airTaps = new(); + List airObjects = []; + List airTaps = []; - var AddDoubleTip = (float x, float z, bool isEnwiden) => + void AddDoubleTip(float x, float z, bool isEnwiden) { int sw = isEnwiden ? Config.EnwidenTrackWidth : Config.SingleTrackWidth; float sx = x <= Config.DrawingTrackWidth / 2 ? x + sw / 2 + 5 : x - sw / 2 - 20; @@ -494,7 +531,7 @@ public void DrawAirObjects(GraphicsAdapter g) Color = Config.Side == 1 ? ColorDesc.FromArgb(0xddffffff) : ColorDesc.FromArgb(0xff000000), Font = new FontDesc("exo", 10f, FontDescStyle.Bold) }); - }; + } foreach (var ev in _affReader.Events) { @@ -503,10 +540,10 @@ public void DrawAirObjects(GraphicsAdapter g) int duration = t.EndTiming - t.Timing; bool isEnwiden = !Interval4K.Any(inv => t.Timing > inv.Item1 && t.Timing < inv.Item2); - int segSize = duration / (duration < 1000 ? 14 : 7); + int segSize = (int)((float)duration / (duration < 1000 ? 14 : 7) / t.SamplingDensity / Config.GlobalArcSamplingDensity); int segmentCount = (segSize == 0 ? 0 : duration / segSize) + 1; - List segments = new(); + List segments = []; Vector3 start = new(); Vector3 end = new((t.XStart + 0.5f) * Config.DrawingTrackWidth / 2 + 3, t.YStart, t.Timing); @@ -539,7 +576,7 @@ public void DrawAirObjects(GraphicsAdapter g) airObjects.Add(new ArcSegment() { - IsVoid = t.IsVoid, + ArcType = t.ArcType, ColorId = t.Color, Location = st, End = ed, @@ -565,6 +602,7 @@ public void DrawAirObjects(GraphicsAdapter g) Location = new Vector3(x + 2, y, airTapTiming), Property = (ev as ArcaeaAffArc)?.Fx, IsEnwiden = isInEnwiden, + ArcType = t.ArcType }); if (isEnwiden) @@ -831,7 +869,7 @@ public void DrawFooter(GraphicsAdapter g) if (hasCover) g.DrawImageScaled(Config.Cover, footerX, footerY, 100, 100); - Regex enRegex = new(@"^[A-Za-z\d_\s/\(\)\+\=\-\.\[\]:\(\)&']+$"); + Regex enRegex = regex(); string? title = Title + $" [ {DiffStr} {Rating:F1} ]" + @@ -858,6 +896,8 @@ public void DrawFooter(GraphicsAdapter g) footerX + footerW - 500, footerY + footerH - 30, 500, 30, StringAdapterAlignment.Far); - } - + } + + [GeneratedRegex(@"^[A-Za-z\d_\s/\(\)\+\=\-\.\[\]:\(\)&']+$")] + private static partial Regex regex(); } diff --git a/Aff2Preview/AffTools/AffAnalyzer/Analyzer.cs b/Aff2Preview/AffTools/AffAnalyzer/Analyzer.cs index b91d9d5..4e77c90 100644 --- a/Aff2Preview/AffTools/AffAnalyzer/Analyzer.cs +++ b/Aff2Preview/AffTools/AffAnalyzer/Analyzer.cs @@ -1,560 +1,568 @@ -using AffTools.AffReader; - -namespace AffTools.AffAnalyzer; - -internal class Analyzer -{ - private List _noteRaws = new(); - - public List Notes { get; private set; } = new(); - - private readonly ArcaeaAffReader _affReader; - - public readonly List SegmentTimings = new(); - - public float totalTime; - public float realTotalTime; - public float baseBpm; - public float baseBpl; - public float baseTimePerSegment; - public int segmentCountInBaseBpm; - - public readonly Dictionary timingCombos = new(); - public readonly Dictionary timingTaps = new(); - - public int Tap = 0; - public int Hold = 0; - public readonly List Arc = new() { 0, 0, 0, 0 }; - public int ArcTap = 0; - public int Total = 0; - public int TapTotal => Tap + ArcTap; - - public Analyzer(ArcaeaAffReader affReader) - { - _affReader = affReader; - - var globalTimingGroup = affReader.Events[0] as ArcaeaAffTiming; - if (globalTimingGroup is not null) - { - baseBpm = globalTimingGroup.Bpm; - baseBpl = globalTimingGroup.BeatsPerLine; - baseTimePerSegment = 60 * 1000 * (int)baseBpl / baseBpm; - } - - foreach (var ev in affReader.Events) - { - if (IsGroupNoInput(ev.TimingGroup)) - continue; - - totalTime = ev switch - { - ArcaeaAffTap => MathF.Max(totalTime, ev.Timing), - ArcaeaAffArc arc => MathF.Max(totalTime, arc.EndTiming), - ArcaeaAffHold hd => MathF.Max(totalTime, hd.EndTiming), - ArcaeaAffTiming tm => MathF.Max(totalTime, tm.Timing), - _ => totalTime - }; - } - - realTotalTime = totalTime; - totalTime += baseTimePerSegment / 4; - - for (double i = 0; i < totalTime; i += baseTimePerSegment) - { - segmentCountInBaseBpm++; - } - } - - /// - /// To pair the scenecontrol statement - /// - /// - public List<(ArcaeaAffSceneControl, ArcaeaAffSceneControl)> GetPairEnwidenLanes() - { - var list = new List(); - var result = new List<(ArcaeaAffSceneControl, ArcaeaAffSceneControl)>(); - - foreach (var affEvent in _affReader.Events) - { - if (affEvent.Type != EventType.SceneControl) continue; - if ((affEvent as ArcaeaAffSceneControl)?.SceneControlTypeName != "enwidenlanes") continue; - - list.Add((ArcaeaAffSceneControl)affEvent); - } - - if (list.Count % 2 != 0) // Pair the enwidenlane - { - var end = new ArcaeaAffSceneControl - { - Timing = _affReader.Events.Last().Timing, - Type = EventType.SceneControl, - Parameters = new List() { 0, 0 }, - SceneControlTypeName = "enwidenlanes" - }; - list.Add(end); - } - - for (int i = 0; i < list.Count - 1; i++) - { - var statement = list[i]; - if (Convert.ToInt32(statement.Parameters[1]) != 1) continue; - if (Convert.ToInt32(list[i + 1].Parameters[1]) == 0) result.Add((statement, list[i + 1])); - } - return result; - } - - public List<(int, int)> Get4LaneInterval(int? end) - { - var list = new List(); - var result = new List<(int, int)>(); - - foreach (var affEvent in _affReader.Events) - { - if (affEvent.Type != EventType.SceneControl) continue; - if ((affEvent as ArcaeaAffSceneControl)?.SceneControlTypeName != "enwidenlanes") continue; - - list.Add((ArcaeaAffSceneControl)affEvent); - } - - - if (list.Count == 0) - { - result.Add((0, end ?? _affReader.Events.Last().Timing)); - return result; - } - - var startSegment = 0; - var pair = GetPairEnwidenLanes(); - foreach (var statement in list) - { - if (pair.TrueForAll(x => x.Item1.Timing != startSegment && x.Item2.Timing != statement.Timing)) - result.Add((startSegment, statement.Timing)); - startSegment = statement.Timing; - } - - if (Convert.ToInt32(list.Last().Parameters[1]) == 0) - { - result.Add((list.Last().Timing, end ?? _affReader.Events.Last().Timing)); - } - - return result; - } - - public void AnalyzeNotes() - { - _noteRaws.Clear(); - - Dictionary> arcColors = new(); - arcColors.Add(0, new()); - arcColors.Add(1, new()); - arcColors.Add(2, new()); - arcColors.Add(3, new()); - - foreach (var ev in _affReader.Events) - { - if (_affReader.TimingGroupProperties[ev.TimingGroup].NoInput) - continue; - - switch (ev) - { - case ArcaeaAffTap evTap: - _noteRaws.Add(new(ev.Timing, 0)); - break; - case ArcaeaAffHold evHold: - _noteRaws.Add(new(ev.Timing, evHold.EndTiming - evHold.Timing)); - break; - case ArcaeaAffArc evArc: - { - if (!evArc.IsVoid) - arcColors[evArc.Color].Add(evArc); - - if (evArc.ArcTaps is not null) - { - foreach (var at in evArc.ArcTaps) - { - _noteRaws.Add(new(at, 0)); - } - } - - break; - } - } - } - - foreach (var (_, arcList) in arcColors) - { - for (var i = arcList.Count - 1; i > 0; i--) - { - var arc = arcList[i]; - var prev = arcList[i - 1]; - if (arc.Timing != prev.EndTiming) - { - _noteRaws.Add(new(arc.Timing, 0)); - } - } - if (arcList.Any()) - _noteRaws.Add(new(arcList[0].Timing, 0)); - } - - _noteRaws = _noteRaws.OrderBy(x => x.TimePoint).ToList(); - - Notes.Clear(); - for (var i = 0; i < _noteRaws.Count - 1; i++) - { - var dt = _noteRaws[i + 1].TimePoint - _noteRaws[i].TimePoint; - if (dt <= 3) - continue; - - var currBpm = GetCurrentTiming(_noteRaws[i].TimePoint, 0).Bpm; - Note n = new(); - if (!n.Analyze(_noteRaws[i].TimePoint, dt, currBpm)) - n.Analyze(_noteRaws[i].TimePoint, dt, baseBpm); - Notes.Add(n); - } - - } - - private ArcaeaAffTiming GetCurrentTiming(int timing, int timingGroup) - { - var timings = _affReader.Events - .Where(ev => ev is ArcaeaAffTiming && ev.TimingGroup == timingGroup) - .Select(x => x as ArcaeaAffTiming); - - return timings.Last(x => x.Timing <= timing); - } - - public bool IsGroupNoInput(int timingGroup) - { - return _affReader.TimingGroupProperties[timingGroup].NoInput; - } - - public int CalcSingleHold(int start, int end, bool hasHead, float bpm, float tpdf) - { - if (start >= end) return 0; - - // Do NOT check "Code Optimization" in the Project Properties!!! - // I HATE FLOATING POINT ERROR... - float d = end - start; - float unit = bpm >= 256 ? 60000 : 30000; - unit /= bpm; - unit /= tpdf; - float cf = d / unit; - var ci = (int)cf; - return ci <= 1 ? 1 : hasHead ? ci - 1 : ci; - } - - public int GetCombo(int timing) - { - if (timingCombos.ContainsKey(timing)) - return timingCombos[timing]; - - try - { - var t = timingCombos.Last(x => x.Key <= timing); - return t.Value; - } - catch - { - return 0; - } - } - - public float GetTapDensity(int timing, int threshold) - { - try - { - var t = timingTaps.Where(x => (timing - threshold <= x.Key && x.Key < timing + threshold)).ToList(); - if (!t.Any()) - return 0; - float density = (float)t.Sum(x => x.Value) * 1000 / (threshold * 2); - return density; - } - catch - { - return 0; - } - } - - public void CalcNotes() - { - List arcs = - _affReader.Events.Where(x => x is ArcaeaAffArc a && !a.IsVoid && !IsGroupNoInput(x.TimingGroup)) - .Select(x => x as ArcaeaAffArc).ToList(); - - arcs.Sort((a, b) => a.Timing.CompareTo(b.Timing)); - ArcaeaAffArc[] scra = arcs.ToArray(); - Array.Sort(scra, (a, b) => a.EndTiming.CompareTo(b.EndTiming)); - int m = scra.Length; - int i = 0; - - List timingNotePoints = new(); - - foreach (ArcaeaAffArc evArc in arcs) - { - for (var j = i; j < m; ++j) - { - ArcaeaAffArc prev = scra[j]; - if (prev.EndTiming <= evArc.Timing - 10) - { - i = j; - } - else if (prev.EndTiming >= evArc.Timing + 10) - { - break; - } - else if (evArc != prev && evArc.YStart == prev.YEnd && Math.Abs(evArc.XStart - prev.XEnd) < 0.1) - { - evArc.HasHead = false; - } - } - } - - List tapTimings = new(); - - foreach (var ev in _affReader.Events) - { - if (IsGroupNoInput(ev.TimingGroup)) - continue; - - switch (ev) - { - case ArcaeaAffTap: - timingNotePoints.Add(ev.Timing); - Tap++; - Total++; - tapTimings.Add(ev.Timing); - break; - case ArcaeaAffHold evHold: - { - var timing = GetCurrentTiming(evHold.Timing, evHold.TimingGroup); - var t = CalcSingleHold(evHold.Timing, evHold.EndTiming, true, timing.Bpm, _affReader.TimingPointDensityFactor); - for (var tx = 0; tx < t; tx++) - { - timingNotePoints.Add(evHold.Timing + (evHold.EndTiming - evHold.Timing) * tx / t); - } - Hold += t; - Total += t; - break; - } - case ArcaeaAffArc evArc: - { - ArcTap += evArc.ArcTaps?.Count ?? 0; - - for (var x = 0; x < evArc.ArcTaps?.Count; x++) - { - timingNotePoints.Add(evArc.ArcTaps[x]); - Total++; - tapTimings.Add(evArc.ArcTaps[x]); - } - - if (evArc.IsVoid) - continue; - - var timing = GetCurrentTiming(evArc.Timing, evArc.TimingGroup); - var t = CalcSingleHold(evArc.Timing, evArc.EndTiming, evArc.HasHead, timing.Bpm, _affReader.TimingPointDensityFactor); - for (var tx = 0; tx < t; tx++) - { - timingNotePoints.Add(evArc.Timing + (evArc.EndTiming - evArc.Timing) * tx / t); - } - Arc[evArc.Color] += t; - Total += t; - break; - } - } - } - - tapTimings = tapTimings.OrderBy(x => x).ToList(); - - foreach (var timing in tapTimings) - { - if (timingTaps.ContainsKey(timing)) - timingTaps[timing]++; - - else - timingTaps[timing] = 1; - } - - timingNotePoints = timingNotePoints.OrderBy(x => x).ToList(); - int total = 0; - foreach (var timing in timingNotePoints) - { - if (timingCombos.ContainsKey(timing)) - { - total++; - timingCombos[timing]++; - } - else - { - total++; - timingCombos[timing] = total; - } - } - - Console.WriteLine($"F{Tap} L{Hold} A{Arc[0] + Arc[1]} (blue{Arc[0]} red{Arc[1]}) S{ArcTap} t:{Tap + Hold + Arc[0] + Arc[1] + ArcTap}"); - } - - public void AnalyzeSegments() - { - List Timings = _affReader.Events - .Where(ev => ev is ArcaeaAffTiming && ev.TimingGroup == 0) - .Select(x => x as ArcaeaAffTiming).ToList(); - - for (int i = 0; i < Timings.Count - 1; ++i) - { - float segment = Timings[i].Bpm == 0 ? - Timings[i + 1].Timing - Timings[i].Timing : - 60000 / Math.Abs(Timings[i].Bpm) * Timings[i].BeatsPerLine; - if (segment == 0) continue; - int n = 0; - while (true) - { - float j = Timings[i].Timing + n++ * segment; - if (j >= Timings[i + 1].Timing) - break; - SegmentTimings.Add(j); - } - } - - if (Timings.Count >= 1) - { - float segmentRemain = Timings[^1].Bpm == 0 ? totalTime - Timings[^1].Timing - : 60000 / Math.Abs(Timings[^1].Bpm) * Timings[^1].BeatsPerLine; - if (segmentRemain != 0) - { - int n = 0; - float j = Timings[^1].Timing; - while (j < totalTime) - { - j = Timings[^1].Timing + n++ * segmentRemain; - SegmentTimings.Add(j); - } - } - } - - if (Timings.Count >= 1 && Timings[0].Bpm != 0 && Timings[0].BeatsPerLine != 0) - { - float t = 0; - float delta = 60000 / Math.Abs(Timings[0].Bpm) * Timings[0].BeatsPerLine; - int n = 0; - if (delta != 0) - { - while (t >= -3000) - { - n++; - t = -n * delta; - SegmentTimings.Insert(0, t); - } - } - } - } - - public Dictionary GetChartQuality(int threshold) - { - List<(int, ArcaeaAffEvent)> timingList = new(); - - foreach (var ev in _affReader.Events) - { - if (IsGroupNoInput(ev.TimingGroup)) - continue; - - switch (ev) - { - case ArcaeaAffTap evTap: - timingList.Add((evTap.Timing, ev)); - break; - case ArcaeaAffHold evHold: - timingList.Add((evHold.Timing, ev)); - break; - case ArcaeaAffArc evArc: - { - if (evArc.ArcTaps is not null) - { - timingList.AddRange(evArc.ArcTaps.Select(v => (v, ev))); - } - - break; - } - - } - } - - Dictionary msec = new(); - for (int i = -threshold; i <= threshold; i++) - msec.Add(i, 0); - - for (int i = 0; i < timingList.Count; i++) - { - var (timing, ev) = timingList[i]; - - for (int t = 0; t < timingList.Count; t++) - { - var (timingt, evt) = timingList[t]; - if (evt == ev) - continue; - - var dt = timingt - timing; - if (Math.Abs(dt) <= threshold) - msec[dt]++; - } - } - - return msec; - } - - /// - /// Analyze charts for double-tap alignment problem - /// - /// - public static void OutputAllChartDoubleTapAnalyze(string affFolder) - { - var d = new DirectoryInfo(affFolder); - int total = 0; - int totalTwin = 0; - int ptotal = 0; - int ptotalTwin = 0; - int threshold = 5; - - Dictionary dtList = new(); - for (int i = 0; i <= threshold; i++) - dtList.Add(i, 0); - - foreach (var f in d.GetDirectories()) - { - foreach (var f2 in f.GetFiles("*.aff")) - { - ArcaeaAffReader affReader = new(); - affReader.Parse(f2.FullName); - - Analyzer analyzer = new(affReader); - var result = analyzer.GetChartQuality(threshold); - - totalTwin += result[0]; - - if (result[1] > 0) - { - ptotal++; - for (int i = 1; i <= threshold; i++) - ptotalTwin += result[i]; - - for (int i = 0; i <= threshold; i++) - dtList[i] += result[i]; - - Console.WriteLine(f2.FullName); - Console.WriteLine($"dt: " + - string.Join(" ", - result.Where(k => k.Key >= 0 && k.Value > 0). - Select(k => $"[{k.Key},{k.Value}]") - )); - } - - total++; - } - } - - Console.WriteLine($"total problem charts: {ptotal}/{total}"); - Console.WriteLine($"total problem doubles: {ptotalTwin}/{totalTwin + ptotalTwin}"); - for (int i = 1; i <= threshold; i++) - Console.WriteLine($"total {i}ms: {dtList[i]}"); - } - -} +using AffTools.AffReader; + +namespace AffTools.AffAnalyzer; + +internal class Analyzer +{ + private List _noteRaws = []; + + public List Notes { get; private set; } = []; + + private readonly ArcaeaAffReader _affReader; + + public readonly List SegmentTimings = []; + + public float totalTime; + public float realTotalTime; + public float baseBpm; + public float baseBpl; + public float baseTimePerSegment; + public int segmentCountInBaseBpm; + + public readonly Dictionary timingCombos = []; + public readonly Dictionary timingTaps = []; + + public int Tap = 0; + public int Hold = 0; + public readonly List Arc = [0, 0, 0, 0]; + public int ArcTap = 0; + public int Total = 0; + public int TapTotal => Tap + ArcTap; + + public Analyzer(ArcaeaAffReader affReader) + { + _affReader = affReader; + + var globalTimingGroup = affReader.Events[0] as ArcaeaAffTiming; + if (globalTimingGroup is not null) + { + baseBpm = globalTimingGroup.Bpm; + baseBpl = globalTimingGroup.BeatsPerLine; + baseTimePerSegment = 60 * 1000 * (int)baseBpl / baseBpm; + } + + foreach (var ev in affReader.Events) + { + if (IsGroupNoInput(ev.TimingGroup)) + continue; + + totalTime = ev switch + { + ArcaeaAffTap => MathF.Max(totalTime, ev.Timing), + ArcaeaAffArc arc => MathF.Max(totalTime, arc.EndTiming), + ArcaeaAffHold hd => MathF.Max(totalTime, hd.EndTiming), + ArcaeaAffTiming tm => MathF.Max(totalTime, tm.Timing), + _ => totalTime + }; + } + + realTotalTime = totalTime; + totalTime += baseTimePerSegment / 4; + + for (double i = 0; i < totalTime; i += baseTimePerSegment) + { + segmentCountInBaseBpm++; + } + } + + /// + /// To pair the scenecontrol statement + /// + /// + public List<(ArcaeaAffSceneControl, ArcaeaAffSceneControl)> GetPairEnwidenLanes() + { + var list = new List(); + var result = new List<(ArcaeaAffSceneControl, ArcaeaAffSceneControl)>(); + + foreach (var affEvent in _affReader.Events) + { + if (affEvent.Type != EventType.SceneControl) continue; + if ((affEvent as ArcaeaAffSceneControl)?.SceneControlTypeName != "enwidenlanes") continue; + + list.Add((ArcaeaAffSceneControl)affEvent); + } + + if (list.Count % 2 != 0) // Pair the enwidenlane + { + var end = new ArcaeaAffSceneControl + { + Timing = _affReader.Events.Last().Timing, + Type = EventType.SceneControl, + Parameters = [0, 0], + SceneControlTypeName = "enwidenlanes" + }; + list.Add(end); + } + + for (int i = 0; i < list.Count - 1; i++) + { + var statement = list[i]; + if (Convert.ToInt32(statement.Parameters[1]) != 1) continue; + if (Convert.ToInt32(list[i + 1].Parameters[1]) == 0) result.Add((statement, list[i + 1])); + } + return result; + } + + public List<(int, int)> Get4LaneInterval(int? end) + { + var list = new List(); + var result = new List<(int, int)>(); + + foreach (var affEvent in _affReader.Events) + { + if (affEvent.Type != EventType.SceneControl) continue; + if ((affEvent as ArcaeaAffSceneControl)?.SceneControlTypeName != "enwidenlanes") continue; + + list.Add((ArcaeaAffSceneControl)affEvent); + } + + + if (list.Count == 0) + { + result.Add((0, end ?? _affReader.Events.Last().Timing)); + return result; + } + + var startSegment = 0; + var pair = GetPairEnwidenLanes(); + foreach (var statement in list) + { + if (pair.TrueForAll(x => x.Item1.Timing != startSegment && x.Item2.Timing != statement.Timing)) + result.Add((startSegment, statement.Timing)); + startSegment = statement.Timing; + } + + if (Convert.ToInt32(list.Last().Parameters[1]) == 0) + { + result.Add((list.Last().Timing, end ?? _affReader.Events.Last().Timing)); + } + + return result; + } + + public void AnalyzeNotes() + { + _noteRaws.Clear(); + + Dictionary> arcColors = new() + { + { 0, new() }, + { 1, new() }, + { 2, new() }, + { 3, new() } + }; + + foreach (var ev in _affReader.Events) + { + if (_affReader.TimingGroupProperties[ev.TimingGroup].NoInput) + continue; + + switch (ev) + { + case ArcaeaAffTap evTap: + _noteRaws.Add(new(ev.Timing, 0)); + break; + case ArcaeaAffHold evHold: + _noteRaws.Add(new(ev.Timing, evHold.EndTiming - evHold.Timing)); + break; + case ArcaeaAffArc evArc: + { + if (evArc.ArcType != ArcType.Void) + arcColors[evArc.Color].Add(evArc); + + if (evArc.ArcTaps is not null && evArc.ArcType != ArcType.Designant) + { + foreach (var at in evArc.ArcTaps) + { + _noteRaws.Add(new(at, 0)); + } + } + + break; + } + } + } + + foreach (var (_, arcList) in arcColors) + { + for (var i = arcList.Count - 1; i > 0; i--) + { + var arc = arcList[i]; + var prev = arcList[i - 1]; + if (arc.Timing != prev.EndTiming) + { + _noteRaws.Add(new(arc.Timing, 0)); + } + } + if (arcList.Count != 0) + _noteRaws.Add(new(arcList[0].Timing, 0)); + } + + _noteRaws = [.. _noteRaws.OrderBy(x => x.TimePoint)]; + + Notes.Clear(); + for (var i = 0; i < _noteRaws.Count - 1; i++) + { + var dt = _noteRaws[i + 1].TimePoint - _noteRaws[i].TimePoint; + if (dt <= 3) + continue; + + var currBpm = GetCurrentTiming(_noteRaws[i].TimePoint, 0).Bpm; + Note n = new(); + if (!n.Analyze(_noteRaws[i].TimePoint, dt, currBpm)) + n.Analyze(_noteRaws[i].TimePoint, dt, baseBpm); + Notes.Add(n); + } + + } + + private ArcaeaAffTiming GetCurrentTiming(int timing, int timingGroup) + { + var timings = _affReader.Events + .Where(ev => ev is ArcaeaAffTiming && ev.TimingGroup == timingGroup) + .Select(x => x as ArcaeaAffTiming); + + return timings.Last(x => x.Timing <= timing); + } + + public bool IsGroupNoInput(int timingGroup) + { + return _affReader.TimingGroupProperties[timingGroup].NoInput; + } + + public static int CalcSingleHold(int start, int end, bool hasHead, float bpm, float tpdf) + { + if (start >= end) return 0; + + // Do NOT check "Code Optimization" in the Project Properties!!! + // I HATE FLOATING POINT ERROR... + float d = end - start; + float unit = bpm >= 256 ? 60000 : 30000; + unit /= bpm; + unit /= tpdf; + float cf = d / unit; + var ci = (int)cf; + return ci <= 1 ? 1 : hasHead ? ci - 1 : ci; + } + + public int GetCombo(int timing) + { + if (timingCombos.TryGetValue(timing, out int value)) + return value; + + try + { + var t = timingCombos.Last(x => x.Key <= timing); + return t.Value; + } + catch + { + return 0; + } + } + + public float GetTapDensity(int timing, int threshold) + { + try + { + var t = timingTaps.Where(x => (timing - threshold <= x.Key && x.Key < timing + threshold)).ToList(); + if (t.Count == 0) + return 0; + float density = (float)t.Sum(x => x.Value) * 1000 / (threshold * 2); + return density; + } + catch + { + return 0; + } + } + + public void CalcNotes() + { + List arcs = + _affReader.Events.Where(x => x is ArcaeaAffArc a && (a.ArcType != ArcType.Void) + && (a.ArcType != ArcType.Designant) + && !IsGroupNoInput(x.TimingGroup)) + .Select(x => x as ArcaeaAffArc).ToList(); + + arcs.Sort((a, b) => a.Timing.CompareTo(b.Timing)); + ArcaeaAffArc[] scra = [.. arcs]; + Array.Sort(scra, (a, b) => a.EndTiming.CompareTo(b.EndTiming)); + int m = scra.Length; + int i = 0; + + List timingNotePoints = []; + + foreach (ArcaeaAffArc evArc in arcs) + { + for (var j = i; j < m; ++j) + { + ArcaeaAffArc prev = scra[j]; + if (prev.EndTiming <= evArc.Timing - 10) + { + i = j; + } + else if (prev.EndTiming >= evArc.Timing + 10) + { + break; + } + else if (evArc != prev && evArc.YStart == prev.YEnd && Math.Abs(evArc.XStart - prev.XEnd) < 0.1) + { + evArc.HasHead = false; + } + } + } + + List tapTimings = []; + + foreach (var ev in _affReader.Events) + { + if (IsGroupNoInput(ev.TimingGroup)) + continue; + + switch (ev) + { + case ArcaeaAffTap: + timingNotePoints.Add(ev.Timing); + Tap++; + Total++; + tapTimings.Add(ev.Timing); + break; + case ArcaeaAffHold evHold: + { + var timing = GetCurrentTiming(evHold.Timing, evHold.TimingGroup); + var t = CalcSingleHold(evHold.Timing, evHold.EndTiming, true, timing.Bpm, _affReader.TimingPointDensityFactor); + for (var tx = 0; tx < t; tx++) + { + timingNotePoints.Add(evHold.Timing + (evHold.EndTiming - evHold.Timing) * tx / t); + } + Hold += t; + Total += t; + break; + } + case ArcaeaAffArc evArc: + { + if (evArc.ArcType == ArcType.Designant) + { + continue; + } + + ArcTap += evArc.ArcTaps?.Count ?? 0; + for (var x = 0; x < evArc.ArcTaps?.Count; x++) + { + timingNotePoints.Add(evArc.ArcTaps[x]); + Total++; + tapTimings.Add(evArc.ArcTaps[x]); + } + + if (evArc.ArcType == ArcType.Void || evArc.ArcType == ArcType.Designant) + continue; + + var timing = GetCurrentTiming(evArc.Timing, evArc.TimingGroup); + var t = CalcSingleHold(evArc.Timing, evArc.EndTiming, evArc.HasHead, timing.Bpm, _affReader.TimingPointDensityFactor); + for (var tx = 0; tx < t; tx++) + { + timingNotePoints.Add(evArc.Timing + (evArc.EndTiming - evArc.Timing) * tx / t); + } + Arc[evArc.Color] += t; + Total += t; + break; + } + } + } + + tapTimings = tapTimings.OrderBy(x => x).ToList(); + + foreach (var timing in tapTimings) + { + if (timingTaps.TryGetValue(timing, out int value)) + timingTaps[timing] = ++value; + + else + timingTaps[timing] = 1; + } + + timingNotePoints = [.. timingNotePoints.OrderBy(x => x)]; + int total = 0; + foreach (var timing in timingNotePoints) + { + if (timingCombos.TryGetValue(timing, out int value)) + { + total++; + timingCombos[timing] = ++value; + } + else + { + total++; + timingCombos[timing] = total; + } + } + + Console.WriteLine($"F{Tap} L{Hold} A{Arc[0] + Arc[1]} (blue{Arc[0]} red{Arc[1]}) S{ArcTap} t:{Tap + Hold + Arc[0] + Arc[1] + ArcTap}"); + } + + public void AnalyzeSegments() + { + List Timings = _affReader.Events + .Where(ev => ev is ArcaeaAffTiming && ev.TimingGroup == 0) + .Select(x => x as ArcaeaAffTiming).ToList(); + + for (int i = 0; i < Timings.Count - 1; ++i) + { + float segment = Timings[i].Bpm == 0 ? + Timings[i + 1].Timing - Timings[i].Timing : + 60000 / Math.Abs(Timings[i].Bpm) * Timings[i].BeatsPerLine; + if (segment == 0) continue; + int n = 0; + while (true) + { + float j = Timings[i].Timing + n++ * segment; + if (j >= Timings[i + 1].Timing) + break; + SegmentTimings.Add(j); + } + } + + if (Timings.Count >= 1) + { + float segmentRemain = Timings[^1].Bpm == 0 ? totalTime - Timings[^1].Timing + : 60000 / Math.Abs(Timings[^1].Bpm) * Timings[^1].BeatsPerLine; + if (segmentRemain != 0) + { + int n = 0; + float j = Timings[^1].Timing; + while (j < totalTime) + { + j = Timings[^1].Timing + n++ * segmentRemain; + SegmentTimings.Add(j); + } + } + } + + if (Timings.Count >= 1 && Timings[0].Bpm != 0 && Timings[0].BeatsPerLine != 0) + { + float t = 0; + float delta = 60000 / Math.Abs(Timings[0].Bpm) * Timings[0].BeatsPerLine; + int n = 0; + if (delta != 0) + { + while (t >= -3000) + { + n++; + t = -n * delta; + SegmentTimings.Insert(0, t); + } + } + } + } + + public Dictionary GetChartQuality(int threshold) + { + List<(int, ArcaeaAffEvent)> timingList = []; + + foreach (var ev in _affReader.Events) + { + if (IsGroupNoInput(ev.TimingGroup)) + continue; + + switch (ev) + { + case ArcaeaAffTap evTap: + timingList.Add((evTap.Timing, ev)); + break; + case ArcaeaAffHold evHold: + timingList.Add((evHold.Timing, ev)); + break; + case ArcaeaAffArc evArc: + { + if (evArc.ArcTaps is not null) + { + timingList.AddRange(evArc.ArcTaps.Select(v => (v, ev))); + } + + break; + } + + } + } + + Dictionary msec = []; + for (int i = -threshold; i <= threshold; i++) + msec.Add(i, 0); + + for (int i = 0; i < timingList.Count; i++) + { + var (timing, ev) = timingList[i]; + + for (int t = 0; t < timingList.Count; t++) + { + var (timingt, evt) = timingList[t]; + if (evt == ev) + continue; + + var dt = timingt - timing; + if (Math.Abs(dt) <= threshold) + msec[dt]++; + } + } + + return msec; + } + + /// + /// Analyze charts for double-tap alignment problem + /// + /// + public static void OutputAllChartDoubleTapAnalyze(string affFolder) + { + var d = new DirectoryInfo(affFolder); + int total = 0; + int totalTwin = 0; + int ptotal = 0; + int ptotalTwin = 0; + int threshold = 5; + + Dictionary dtList = new(); + for (int i = 0; i <= threshold; i++) + dtList.Add(i, 0); + + foreach (var f in d.GetDirectories()) + { + foreach (var f2 in f.GetFiles("*.aff")) + { + ArcaeaAffReader affReader = new(); + affReader.Parse(f2.FullName); + + Analyzer analyzer = new(affReader); + var result = analyzer.GetChartQuality(threshold); + + totalTwin += result[0]; + + if (result[1] > 0) + { + ptotal++; + for (int i = 1; i <= threshold; i++) + ptotalTwin += result[i]; + + for (int i = 0; i <= threshold; i++) + dtList[i] += result[i]; + + Console.WriteLine(f2.FullName); + Console.WriteLine($"dt: " + + string.Join(" ", + result.Where(k => k.Key >= 0 && k.Value > 0). + Select(k => $"[{k.Key},{k.Value}]") + )); + } + + total++; + } + } + + Console.WriteLine($"total problem charts: {ptotal}/{total}"); + Console.WriteLine($"total problem doubles: {ptotalTwin}/{totalTwin + ptotalTwin}"); + for (int i = 1; i <= threshold; i++) + Console.WriteLine($"total {i}ms: {dtList[i]}"); + } + +} diff --git a/Aff2Preview/AffTools/AffAnalyzer/Note.cs b/Aff2Preview/AffTools/AffAnalyzer/Note.cs index be89bef..62bbda7 100644 --- a/Aff2Preview/AffTools/AffAnalyzer/Note.cs +++ b/Aff2Preview/AffTools/AffAnalyzer/Note.cs @@ -1,15 +1,9 @@ namespace AffTools.AffAnalyzer; -internal struct NoteRaw +internal struct NoteRaw(int timePoint, int duration) { - public int TimePoint = 0; - public int Duration = 0; - - public NoteRaw(int timePoint, int duration) - { - TimePoint = timePoint; - Duration = duration; - } + public int TimePoint = timePoint; + public int Duration = duration; } internal class Note @@ -31,7 +25,7 @@ public float InvDuration } } - private static bool isDoubleEqual(double a, double b, double e) => Math.Abs(a - b) <= e; + private static bool IsDoubleEqual(double a, double b, double e) => Math.Abs(a - b) <= e; public Note(int timeing, int length, double bpm) { @@ -55,7 +49,7 @@ public bool Analyze(int timing, int length, double bpm) return true; } - if (isDoubleEqual(length, time_full_note, threshold)) + if (IsDoubleEqual(length, time_full_note, threshold)) { Divide = 1; return true; @@ -66,13 +60,13 @@ public bool Analyze(int timing, int length, double bpm) var t_len = time_full_note / i; var t_dot_len = t_len * 1.5; - if (isDoubleEqual(length, t_len, threshold)) + if (IsDoubleEqual(length, t_len, threshold)) { Divide = i; return true; } - if (isDoubleEqual(length, t_dot_len, threshold)) + if (IsDoubleEqual(length, t_dot_len, threshold)) { Divide = i; hasDot = true; diff --git a/Aff2Preview/AffTools/AffReader/AffReader.cs b/Aff2Preview/AffTools/AffReader/AffReader.cs index df70c21..0f51b9c 100644 --- a/Aff2Preview/AffTools/AffReader/AffReader.cs +++ b/Aff2Preview/AffTools/AffReader/AffReader.cs @@ -1,418 +1,444 @@ -using System.Numerics; - -namespace AffTools.AffReader; - -public class ArcaeaAffReader -{ - public int TotalTimingGroup = 1; - - public int CurrentTimingGroup; - - public int AudioOffset; - public float TimingPointDensityFactor = 1; - - public List Events = new List(); - - public List TimingGroupProperties = new List(); - - private static EventType DetermineType(string line) - { - if (line.StartsWith("(")) - return EventType.Tap; - if (line.StartsWith("timing(")) - return EventType.Timing; - if (line.StartsWith("hold(")) - return EventType.Hold; - if (line.StartsWith("arc(")) - return EventType.Arc; - if (line.StartsWith("camera(")) - return EventType.Camera; - if (line.StartsWith("scenecontrol(")) - return EventType.SceneControl; - if (line.StartsWith("timinggroup(")) - return EventType.TimingGroup; - if (line.StartsWith("};")) - return EventType.TimingGroupEnd; - return EventType.Unknown; - } - - private void ParseTiming(string line) - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(7); - var num = stringParser.ReadInt(","); - var num2 = stringParser.ReadFloat(","); - var num3 = stringParser.ReadFloat(")"); - Events.Add(new ArcaeaAffTiming - { - Timing = num, - BeatsPerLine = num3, - Bpm = num2, - Type = EventType.Timing, - TimingGroup = CurrentTimingGroup - }); - if (num3 < 0f) - throw new ArcaeaAffFormatException("节拍线密度小于0"); - if (num == 0 && num2 == 0f) - throw new ArcaeaAffFormatException("基准BPM为0(Timing为0的timing事件BPM不能为0)"); - } - catch (ArcaeaAffFormatException) - { - throw; - } - catch (Exception) - { - throw new ArcaeaAffFormatException("符号错误"); - } - } - - private void ParseTap(string line, bool noInput) - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(1); - var timing = stringParser.ReadInt(","); - var num = stringParser.ReadInt(")"); - if (!noInput) Events.Add(new ArcaeaAffTap - { - Timing = timing, - Track = num, - Type = EventType.Tap, - TimingGroup = CurrentTimingGroup, - NoInput = noInput - }); - if (num is < 0 or > 5) - throw new ArcaeaAffFormatException("轨道错误"); - } - catch (ArcaeaAffFormatException) - { - throw; - } - catch (Exception) - { - throw new ArcaeaAffFormatException("符号错误"); - } - } - - private void ParseHold(string line, bool noInput) - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(5); - var num = stringParser.ReadInt(","); - var num2 = stringParser.ReadInt(","); - var num3 = stringParser.ReadInt(")"); - if (!noInput) Events.Add(new ArcaeaAffHold - { - Timing = num, - EndTiming = num2, - Track = num3, - Type = EventType.Hold, - TimingGroup = CurrentTimingGroup, - NoInput = noInput - }); - if (num3 is < 0 or > 5) - throw new ArcaeaAffFormatException("轨道错误"); - if (num2 < num) - throw new ArcaeaAffFormatException("持续时间小于0"); - } - catch (ArcaeaAffFormatException) - { - throw; - } - catch (Exception) - { - throw new ArcaeaAffFormatException("符号错误"); - } - } - - private void ParseArc(string line, bool noInput) - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(4); - var num = stringParser.ReadInt(","); - var num2 = stringParser.ReadInt(","); - var xStart = stringParser.ReadFloat(","); - var xEnd = stringParser.ReadFloat(","); - var lineType = stringParser.ReadString(","); - var yStart = stringParser.ReadFloat(","); - var yEnd = stringParser.ReadFloat(","); - var color = stringParser.ReadInt(","); - var fx = stringParser.ReadString(","); - var isVoid = stringParser.ReadBool(")"); - List? list = null; - if (stringParser.Current != ";") - { - list = new List(); - isVoid = true; - do - { - stringParser.Skip(8); - list.Add(stringParser.ReadInt(")")); - } - while (!(stringParser.Current != ",")); - } - if (!noInput) Events.Add(new ArcaeaAffArc - { - Timing = num, - EndTiming = num2, - XStart = xStart, - XEnd = xEnd, - LineType = lineType, - YStart = yStart, - YEnd = yEnd, - Color = color, - Fx = fx, - IsVoid = isVoid, - Type = EventType.Arc, - ArcTaps = list, - TimingGroup = CurrentTimingGroup, - NoInput = noInput - }); - if (num2 < num) - throw new ArcaeaAffFormatException("持续时间小于0"); - } - catch (ArcaeaAffFormatException) - { - throw; - } - catch (Exception) - { - throw new ArcaeaAffFormatException("符号错误"); - } - } - - private void ParseCamera(string line) - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(7); - var timing = stringParser.ReadInt(","); - var move = new Vector3(stringParser.ReadFloat(","), stringParser.ReadFloat(","), stringParser.ReadFloat(",")); - var rotate = new Vector3(stringParser.ReadFloat(","), stringParser.ReadFloat(","), stringParser.ReadFloat(",")); - var cameraType = stringParser.ReadString(","); - var num = stringParser.ReadInt(")"); - Events.Add(new ArcaeaAffCamera - { - Timing = timing, - Duration = num, - Move = move, - Rotate = rotate, - CameraType = cameraType, - Type = EventType.Camera - }); - if (num < 0) - throw new ArcaeaAffFormatException("持续时间小于0"); - } - catch (ArcaeaAffFormatException) - { - throw; - } - catch (Exception) - { - throw new ArcaeaAffFormatException("符号错误"); - } - } - - private void ParseSceneControl(string line) - { - try - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(13); - var timing = stringParser.ReadInt(","); - var sceneControlTypeName = stringParser.ReadString(",").Trim(); - var text = stringParser.ReadString(); - text = text.Substring(0, text.LastIndexOf(')')); - var array = text.Split(','); - var list = new List(); - var text2 = ""; - var array2 = array; - foreach (var text3 in array2) - { - var num = text3[0] == '\''; - var flag = text2 != ""; - var flag2 = text3[^1] == '\''; - if (num || flag || flag2) - text2 += text3; - if (flag2) - { - list.Add(text2.Substring(1, text2.Length - 2)); - text2 = ""; - } - if (!num && !flag && !flag2) - list.Add(float.Parse(text3)); - } - Events.Add(new ArcaeaAffSceneControl - { - Timing = timing, - Type = EventType.SceneControl, - Parameters = list, - SceneControlTypeName = sceneControlTypeName, - TimingGroup = CurrentTimingGroup - }); - } - catch (Exception) - { - var stringParser2 = new AffStringParser(line); - stringParser2.Skip(13); - var timing2 = stringParser2.ReadInt(","); - var sceneControlTypeName2 = stringParser2.ReadString(")"); - var parameters = new List(); - Events.Add(new ArcaeaAffSceneControl - { - Timing = timing2, - Type = EventType.SceneControl, - Parameters = parameters, - SceneControlTypeName = sceneControlTypeName2, - TimingGroup = CurrentTimingGroup - }); - } - } - catch (ArcaeaAffFormatException) - { - throw; - } - catch (Exception) - { - throw new ArcaeaAffFormatException("符号错误"); - } - } - - private static bool NoInputGroup(string line) - { - try - { - var stringParser = new AffStringParser(line); - stringParser.Skip(12); - return stringParser.ReadString(")") == "noinput"; - } - catch (ArcaeaAffFormatException) - { - return false; - } - catch (Exception) - { - return false; - } - } - - public void Parse(string path) - { - TotalTimingGroup = 1; - CurrentTimingGroup = 0; - TimingGroupProperties.Add(new TimingGroupProperties()); - var noInput = false; - var array = File.ReadAllLines(path); - try - { - AudioOffset = int.Parse(array[0].Replace("AudioOffset:", "")); - } - catch (Exception) - { - throw new ArcaeaAffFormatException(array[0], 1); - } - int i; - if (array[1].Contains("TimingPointDensityFactor")) - { - try - { - TimingPointDensityFactor = float.Parse(array[1].Replace("TimingPointDensityFactor:", "")); - } - catch (Exception) - { - throw new ArcaeaAffFormatException(array[1], 2); - } - try - { - ParseTiming(array[3]); - } - catch (Exception) - { - throw new ArcaeaAffFormatException(EventType.Timing, array[3], 4); - } - i = 4; - } - else - { - try - { - ParseTiming(array[2]); - } - catch (Exception) - { - throw new ArcaeaAffFormatException(EventType.Timing, array[2], 3); - } - i = 3; - } - - for (; i < array.Length; i++) - { - var text = array[i].Trim(); - var eventType = DetermineType(text); - try - { - switch (eventType) - { - case EventType.Timing: - ParseTiming(text); - break; - case EventType.Tap: - ParseTap(text, noInput); - break; - case EventType.Hold: - ParseHold(text, noInput); - break; - case EventType.Arc: - ParseArc(text, noInput); - break; - case EventType.Camera: - ParseCamera(text); - break; - case EventType.SceneControl: - ParseSceneControl(text); - break; - case EventType.TimingGroup: - TotalTimingGroup++; - CurrentTimingGroup = TotalTimingGroup - 1; - noInput = NoInputGroup(text); - while (TimingGroupProperties.Count <= CurrentTimingGroup) - { - TimingGroupProperties.Add(new TimingGroupProperties()); - } - TimingGroupProperties[CurrentTimingGroup] = new TimingGroupProperties - { - NoInput = noInput, - }; - break; - case EventType.TimingGroupEnd: - CurrentTimingGroup = 0; - noInput = false; - break; - case EventType.Unknown: - break; - } - } - catch (ArcaeaAffFormatException ex3) - { - throw new ArcaeaAffFormatException(eventType, text, i + 1, ex3.Reason); - } - catch (Exception) - { - throw new ArcaeaAffFormatException(eventType, text, i + 1); - } - } - Events.Sort((a, b) => a.Timing.CompareTo(b.Timing)); - if (CurrentTimingGroup == 0) return; - throw new ArcaeaAffFormatException("Timing Group { 与 } 数量不匹配,请确保\ntiminggroup(){\n与\n};\n各占一行"); - } -} +using System.Numerics; + +namespace AffTools.AffReader; + +public class ArcaeaAffReader +{ + public int TotalTimingGroup = 1; + + public int CurrentTimingGroup; + + public int AudioOffset; + public float TimingPointDensityFactor = 1; + + public List Events = new List(); + + public List TimingGroupProperties = new List(); + + private static EventType DetermineType(string line) + { + if (line.StartsWith('(')) + return EventType.Tap; + if (line.StartsWith("timing(")) + return EventType.Timing; + if (line.StartsWith("hold(")) + return EventType.Hold; + if (line.StartsWith("arc(")) + return EventType.Arc; + if (line.StartsWith("camera(")) + return EventType.Camera; + if (line.StartsWith("scenecontrol(")) + return EventType.SceneControl; + if (line.StartsWith("timinggroup(")) + return EventType.TimingGroup; + if (line.StartsWith("};")) + return EventType.TimingGroupEnd; + return EventType.Unknown; + } + + private void ParseTiming(string line) + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(7); + var num = stringParser.ReadInt(","); + var num2 = stringParser.ReadFloat(","); + var num3 = stringParser.ReadFloat(")"); + Events.Add(new ArcaeaAffTiming + { + Timing = num, + BeatsPerLine = num3, + Bpm = num2, + Type = EventType.Timing, + TimingGroup = CurrentTimingGroup + }); + if (num3 < 0f) + throw new ArcaeaAffFormatException("节拍线密度小于0"); + if (num == 0 && num2 == 0f) + throw new ArcaeaAffFormatException("基准BPM为0(Timing为0的timing事件BPM不能为0)"); + } + catch (ArcaeaAffFormatException) + { + throw; + } + catch (Exception) + { + throw new ArcaeaAffFormatException("符号错误"); + } + } + + private void ParseTap(string line, bool noInput) + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(1); + var timing = stringParser.ReadInt(","); + var num = stringParser.ReadInt(")"); + if (!noInput) Events.Add(new ArcaeaAffTap + { + Timing = timing, + Track = num, + Type = EventType.Tap, + TimingGroup = CurrentTimingGroup, + NoInput = noInput + }); + if (num is < 0 or > 5) + throw new ArcaeaAffFormatException("轨道错误"); + } + catch (ArcaeaAffFormatException) + { + throw; + } + catch (Exception) + { + throw new ArcaeaAffFormatException("符号错误"); + } + } + + private void ParseHold(string line, bool noInput) + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(5); + var num = stringParser.ReadInt(","); + var num2 = stringParser.ReadInt(","); + var num3 = stringParser.ReadInt(")"); + if (!noInput) Events.Add(new ArcaeaAffHold + { + Timing = num, + EndTiming = num2, + Track = num3, + Type = EventType.Hold, + TimingGroup = CurrentTimingGroup, + NoInput = noInput + }); + if (num3 is < 0 or > 5) + throw new ArcaeaAffFormatException("轨道错误"); + if (num2 < num) + throw new ArcaeaAffFormatException("持续时间小于0"); + } + catch (ArcaeaAffFormatException) + { + throw; + } + catch (Exception) + { + throw new ArcaeaAffFormatException("符号错误"); + } + } + + private void ParseArc(string line, bool noInput) + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(4); + var num = stringParser.ReadInt(","); + var num2 = stringParser.ReadInt(","); + var xStart = stringParser.ReadFloat(","); + var xEnd = stringParser.ReadFloat(","); + var lineType = stringParser.ReadString(","); + var yStart = stringParser.ReadFloat(","); + var yEnd = stringParser.ReadFloat(","); + var color = stringParser.ReadInt(","); + var fx = stringParser.ReadString(","); + // var isVoid = stringParser.ReadBool(")"); // we're updating to ver 6.8.0 or later + string arcTypeString = stringParser.ReadString([",", ")"], out string arcTypeTerminator); + float sampleDensity = 1.0f; + if (arcTypeTerminator == ",") + { + sampleDensity = Math.Max(stringParser.ReadFloat(")"), 1.0f); + } + + List? list = null; + if (stringParser.Current != ";") + { + list = []; + //isVoid = true; + // Designant arc has made an exception so we could not set the arc type to void. + do + { + stringParser.Skip(8); + int arctapTiming = stringParser.ReadInt(")"); + if (arctapTiming > num2) + { + Console.WriteLine($"ArcTap out of trace. The timing of ArcTap is {arctapTiming}, but the trace ends at {num2}."); + } + list.Add(arctapTiming); + } + while (!(stringParser.Current != ",")); + } + + ArcType type = arcTypeString switch + { + "false" => ArcType.Arc, + "true" => ArcType.Void, + "designant" => ArcType.Designant, + _ => ArcType.Arc, + }; + if (color == 3 && num == num2) + { + type = ArcType.ScaledArctap; + } + if (!noInput) Events.Add(new ArcaeaAffArc + { + Timing = num, + EndTiming = num2, + XStart = xStart, + XEnd = xEnd, + LineType = lineType, + YStart = yStart, + YEnd = yEnd, + Color = color, + Fx = fx, + ArcType = type, + Type = EventType.Arc, + ArcTaps = list, + TimingGroup = CurrentTimingGroup, + NoInput = noInput, + SamplingDensity = sampleDensity + }); + if (num2 < num) + throw new ArcaeaAffFormatException("持续时间小于0"); + } + catch (ArcaeaAffFormatException) + { + throw; + } + catch (Exception) + { + throw new ArcaeaAffFormatException("符号错误"); + } + } + + private void ParseCamera(string line) + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(7); + var timing = stringParser.ReadInt(","); + var move = new Vector3(stringParser.ReadFloat(","), stringParser.ReadFloat(","), stringParser.ReadFloat(",")); + var rotate = new Vector3(stringParser.ReadFloat(","), stringParser.ReadFloat(","), stringParser.ReadFloat(",")); + var cameraType = stringParser.ReadString(","); + var num = stringParser.ReadInt(")"); + Events.Add(new ArcaeaAffCamera + { + Timing = timing, + Duration = num, + Move = move, + Rotate = rotate, + CameraType = cameraType, + Type = EventType.Camera + }); + if (num < 0) + throw new ArcaeaAffFormatException("持续时间小于0"); + } + catch (ArcaeaAffFormatException) + { + throw; + } + catch (Exception) + { + throw new ArcaeaAffFormatException("符号错误"); + } + } + + private void ParseSceneControl(string line) + { + try + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(13); + var timing = stringParser.ReadInt(","); + var sceneControlTypeName = stringParser.ReadString(",").Trim(); + var text = stringParser.ReadString(); + text = text[..text.LastIndexOf(')')]; + var array = text.Split(','); + var list = new List(); + var text2 = ""; + var array2 = array; + foreach (var text3 in array2) + { + var num = text3[0] == '\''; + var flag = text2 != ""; + var flag2 = text3[^1] == '\''; + if (num || flag || flag2) + text2 += text3; + if (flag2) + { + list.Add(text2.Substring(1, text2.Length - 2)); + text2 = ""; + } + if (!num && !flag && !flag2) + list.Add(float.Parse(text3)); + } + Events.Add(new ArcaeaAffSceneControl + { + Timing = timing, + Type = EventType.SceneControl, + Parameters = list, + SceneControlTypeName = sceneControlTypeName, + TimingGroup = CurrentTimingGroup + }); + } + catch (Exception) + { + var stringParser2 = new AffStringParser(line); + stringParser2.Skip(13); + var timing2 = stringParser2.ReadInt(","); + var sceneControlTypeName2 = stringParser2.ReadString(")"); + var parameters = new List(); + Events.Add(new ArcaeaAffSceneControl + { + Timing = timing2, + Type = EventType.SceneControl, + Parameters = parameters, + SceneControlTypeName = sceneControlTypeName2, + TimingGroup = CurrentTimingGroup + }); + } + } + catch (ArcaeaAffFormatException) + { + throw; + } + catch (Exception) + { + throw new ArcaeaAffFormatException("符号错误"); + } + } + + private static bool NoInputGroup(string line) + { + try + { + var stringParser = new AffStringParser(line); + stringParser.Skip(12); + return stringParser.ReadString(")") == "noinput"; + } + catch (ArcaeaAffFormatException) + { + return false; + } + catch (Exception) + { + return false; + } + } + + public void Parse(string path) + { + TotalTimingGroup = 1; + CurrentTimingGroup = 0; + TimingGroupProperties.Add(new TimingGroupProperties()); + var noInput = false; + var array = File.ReadAllLines(path); + try + { + AudioOffset = int.Parse(array[0].Replace("AudioOffset:", "")); + } + catch (Exception) + { + throw new ArcaeaAffFormatException(array[0], 1); + } + int i; + if (array[1].Contains("TimingPointDensityFactor")) + { + try + { + TimingPointDensityFactor = float.Parse(array[1].Replace("TimingPointDensityFactor:", "")); + } + catch (Exception) + { + throw new ArcaeaAffFormatException(array[1], 2); + } + try + { + ParseTiming(array[3]); + } + catch (Exception) + { + throw new ArcaeaAffFormatException(EventType.Timing, array[3], 4); + } + i = 4; + } + else + { + try + { + ParseTiming(array[2]); + } + catch (Exception) + { + throw new ArcaeaAffFormatException(EventType.Timing, array[2], 3); + } + i = 3; + } + + for (; i < array.Length; i++) + { + var text = array[i].Trim(); + var eventType = DetermineType(text); + try + { + switch (eventType) + { + case EventType.Timing: + ParseTiming(text); + break; + case EventType.Tap: + ParseTap(text, noInput); + break; + case EventType.Hold: + ParseHold(text, noInput); + break; + case EventType.Arc: + ParseArc(text, noInput); + break; + case EventType.Camera: + ParseCamera(text); + break; + case EventType.SceneControl: + ParseSceneControl(text); + break; + case EventType.TimingGroup: + TotalTimingGroup++; + CurrentTimingGroup = TotalTimingGroup - 1; + noInput = NoInputGroup(text); + while (TimingGroupProperties.Count <= CurrentTimingGroup) + { + TimingGroupProperties.Add(new TimingGroupProperties()); + } + TimingGroupProperties[CurrentTimingGroup] = new TimingGroupProperties + { + NoInput = noInput, + }; + break; + case EventType.TimingGroupEnd: + CurrentTimingGroup = 0; + noInput = false; + break; + case EventType.Unknown: + break; + } + } + catch (ArcaeaAffFormatException ex3) + { + throw new ArcaeaAffFormatException(eventType, text, i + 1, ex3.Reason); + } + catch (Exception) + { + throw new ArcaeaAffFormatException(eventType, text, i + 1); + } + } + Events.Sort((a, b) => a.Timing.CompareTo(b.Timing)); + if (CurrentTimingGroup == 0) return; + throw new ArcaeaAffFormatException("Timing Group { 与 } 数量不匹配,请确保\ntiminggroup(){\n与\n};\n各占一行"); + } +} diff --git a/Aff2Preview/AffTools/AffReader/AffStringParser.cs b/Aff2Preview/AffTools/AffReader/AffStringParser.cs index 2ce58a5..307a439 100644 --- a/Aff2Preview/AffTools/AffReader/AffStringParser.cs +++ b/Aff2Preview/AffTools/AffReader/AffStringParser.cs @@ -1,62 +1,80 @@ -namespace AffTools.AffReader; - -public class AffStringParser -{ - private int pos; - private string str; - - public AffStringParser(string str) - { - this.str = str; - } - - public void Skip(int length) - { - pos += length; - } - - public float ReadFloat(string? terminator = null) - { - int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; - float value = float.Parse(str.Substring(pos, end - pos)); - pos += end - pos + 1; - return value; - } - - public int ReadInt(string? terminator = null) - { - int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; - int value = int.Parse(str.Substring(pos, end - pos)); - pos += end - pos + 1; - return value; - } - - public bool ReadBool(string? terminator = null) - { - int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; - bool value = bool.Parse(str.Substring(pos, end - pos)); - pos += end - pos + 1; - return value; - } - - public string ReadString(string? terminator = null) - { - int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; - string value = str.Substring(pos, end - pos); - pos += end - pos + 1; - return value; - } - - public string Current - { - get - { - return str[pos].ToString(); - } - } - - public string Peek(int count) - { - return str.Substring(pos, count); - } -} +namespace AffTools.AffReader; + +public class AffStringParser(string str) +{ + private int pos; + private string str = str; + + public void Skip(int length) + { + pos += length; + } + + public float ReadFloat(string? terminator = null) + { + int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; + float value = float.Parse(str[pos..end]); + pos += end - pos + 1; + return value; + } + + public int ReadInt(string? terminator = null) + { + int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; + int value = int.Parse(str[pos..end]); + pos += end - pos + 1; + return value; + } + + public bool ReadBool(string? terminator = null) + { + int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; + bool value = bool.Parse(str.AsSpan(pos, end - pos)); + pos += end - pos + 1; + return value; + } + + public string ReadString(string? terminator = null) + { + int end = terminator != null ? str.IndexOf(terminator, pos) : str.Length - 1; + string value = str[pos..end]; + pos += end - pos + 1; + return value; + } + + public string ReadString(string[] optionalTerminator, out string realTerminator) + { + realTerminator = string.Empty; + int end = -1; + foreach (string terminator in optionalTerminator) + { + int terminatorPosition = str.IndexOf(terminator, pos); + if (terminatorPosition > end && !str[pos..terminatorPosition].Contains('[')) + { + end = terminatorPosition; + realTerminator = terminator; + } + } + if (end == -1) + { + end += str.Length; + } + string value = str[pos..end]; + pos += end - pos + 1; + + return value; + } + + public string Current + { + get + { + return str[pos].ToString(); + } + } + + public string Peek(int count) + { + return str.Substring(pos, count); + } +} diff --git a/Aff2Preview/AffTools/AffReader/ArcAlgorithm.cs b/Aff2Preview/AffTools/AffReader/ArcAlgorithm.cs index f288f5a..fa45f89 100644 --- a/Aff2Preview/AffTools/AffReader/ArcAlgorithm.cs +++ b/Aff2Preview/AffTools/AffReader/ArcAlgorithm.cs @@ -82,7 +82,7 @@ public static float Qi(float value) } public static float Qo(float value) { - return (value = value - 1) * value * value + 1; + return (--value) * value * value + 1; } } diff --git a/Aff2Preview/AffTools/AffReader/ArcaeaFileFormat.cs b/Aff2Preview/AffTools/AffReader/ArcaeaFileFormat.cs index 66f9cf9..ff3869e 100644 --- a/Aff2Preview/AffTools/AffReader/ArcaeaFileFormat.cs +++ b/Aff2Preview/AffTools/AffReader/ArcaeaFileFormat.cs @@ -1,170 +1,180 @@ -using System.Numerics; - -namespace AffTools.AffReader -{ - public enum ArcLineType - { - B, - S, - Si, - So, - SiSi, - SiSo, - SoSi, - SoSo - } - - namespace Advanced - { - public enum SpecialType - { - Unknown, - TextArea, - Fade - } - - public class ArcadeAffSpecial : ArcaeaAffEvent - { - public SpecialType SpecialType; - public string param1; - public string param2; - public string param3; - } - } - - public class ArcaeaAffFormatException : Exception - { - public string? Reason; - - public ArcaeaAffFormatException(string? reason) : base(reason) - { - Reason = reason; - } - - public ArcaeaAffFormatException(string content, int line) - { - Console.WriteLine($"在第 {line} 行发生读取错误。\n{content}"); - } - - public ArcaeaAffFormatException(EventType type, string content, int line) - { - Console.WriteLine($"在第 {line} 行处的 {type} 型事件中发生读取错误。\n{content}"); - } - - public ArcaeaAffFormatException(EventType type, string content, int line, string? reason) - { - Console.WriteLine($"在第 {line} 行处的 {type} 型事件中发生读取错误。\n{content}\n{reason}"); - } - } - - public class ArcaeaAffEvent - { - public int Timing; - - public EventType Type; - - public int TimingGroup; - } - - public class ArcaeaAffArc : ArcaeaAffEvent - { - public int EndTiming; - - public float XStart; - - public float XEnd; - - public string LineType = null!; - - public float YStart; - - public float YEnd; - - public int Color; - - public string Fx = null!; - - public bool IsVoid; - - public List? ArcTaps; - - public bool NoInput; - - public bool HasHead = true; - - public static ArcLineType ToArcLineType(string type) - { - return type switch - { - "b" => ArcLineType.B, - "s" => ArcLineType.S, - "si" => ArcLineType.Si, - "so" => ArcLineType.So, - "sisi" => ArcLineType.SiSi, - "siso" => ArcLineType.SiSo, - "sosi" => ArcLineType.SoSi, - "soso" => ArcLineType.SoSo, - _ => ArcLineType.S - }; - } - } - - public class ArcaeaAffCamera : ArcaeaAffEvent - { - public Vector3 Move; - - public Vector3 Rotate; - - public string CameraType = null!; - - public int Duration; - } - - public class ArcaeaAffHold : ArcaeaAffEvent - { - public int EndTiming; - - public int Track; - - public bool NoInput; - } - - public class ArcaeaAffSceneControl : ArcaeaAffEvent - { - public string SceneControlTypeName = null!; - - public List Parameters = null!; - } - - public class ArcaeaAffTap : ArcaeaAffEvent - { - public int Track; - - public bool NoInput; - } - - public class ArcaeaAffTiming : ArcaeaAffEvent - { - public float Bpm; - - public float BeatsPerLine; - } - - public enum EventType - { - Timing, - Tap, - Hold, - Arc, - Camera, - Unknown, - SceneControl, - TimingGroup, - TimingGroupEnd - } - - public class TimingGroupProperties - { - public bool NoInput; - } +using System.Numerics; + +namespace AffTools.AffReader +{ + public enum ArcLineType + { + B, + S, + Si, + So, + SiSi, + SiSo, + SoSi, + SoSo + } + + namespace Advanced + { + public enum SpecialType + { + Unknown, + TextArea, + Fade + } + + public class ArcadeAffSpecial : ArcaeaAffEvent + { + public SpecialType SpecialType; + public string Param1; + public string Param2; + public string Param3; + } + } + + public class ArcaeaAffFormatException : Exception + { + public string? Reason; + + public ArcaeaAffFormatException(string? reason) : base(reason) + { + Reason = reason; + } + + public ArcaeaAffFormatException(string content, int line) + { + Console.WriteLine($"在第 {line} 行发生读取错误。\n{content}"); + } + + public ArcaeaAffFormatException(EventType type, string content, int line) + { + Console.WriteLine($"在第 {line} 行处的 {type} 型事件中发生读取错误。\n{content}"); + } + + public ArcaeaAffFormatException(EventType type, string content, int line, string? reason) + { + Console.WriteLine($"在第 {line} 行处的 {type} 型事件中发生读取错误。\n{content}\n{reason}"); + } + } + + public class ArcaeaAffEvent + { + public int Timing; + + public EventType Type; + + public int TimingGroup; + } + + public enum ArcType + { + Arc, + Void, + Designant, + ScaledArctap // not implemented yet + } + + public class ArcaeaAffArc : ArcaeaAffEvent + { + public int EndTiming; + + public float XStart; + + public float XEnd; + + public string LineType = null!; + + public float YStart; + + public float YEnd; + + public int Color; + + public string Fx = "none"; + + public ArcType ArcType; + + public List? ArcTaps; + + public bool NoInput; + + public float SamplingDensity = 1.0f; + + public bool HasHead = true; + + public static ArcLineType ToArcLineType(string type) + { + return type switch + { + "b" => ArcLineType.B, + "s" => ArcLineType.S, + "si" => ArcLineType.Si, + "so" => ArcLineType.So, + "sisi" => ArcLineType.SiSi, + "siso" => ArcLineType.SiSo, + "sosi" => ArcLineType.SoSi, + "soso" => ArcLineType.SoSo, + _ => ArcLineType.S + }; + } + } + + public class ArcaeaAffCamera : ArcaeaAffEvent + { + public Vector3 Move; + + public Vector3 Rotate; + + public string CameraType = null!; + + public int Duration; + } + + public class ArcaeaAffHold : ArcaeaAffEvent + { + public int EndTiming; + + public int Track; + + public bool NoInput; + } + + public class ArcaeaAffSceneControl : ArcaeaAffEvent + { + public string SceneControlTypeName = null!; + + public List Parameters = null!; + } + + public class ArcaeaAffTap : ArcaeaAffEvent + { + public int Track; + + public bool NoInput; + } + + public class ArcaeaAffTiming : ArcaeaAffEvent + { + public float Bpm; + + public float BeatsPerLine; + } + + public enum EventType + { + Timing, + Tap, + Hold, + Arc, + Camera, + Unknown, + SceneControl, + TimingGroup, + TimingGroupEnd + } + + public class TimingGroupProperties + { + public bool NoInput; + } } \ No newline at end of file diff --git a/README.md b/README.md index 3d6e8b5..89aedf2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # AffTools [![Version](https://img.shields.io/badge/AffTools-2.1-coral)](#) -[![C#](https://img.shields.io/badge/.NET-6.0-blue)](#) +[![C#](https://img.shields.io/badge/.NET-10.0-blue)](#) [![License](https://img.shields.io/static/v1?label=LICENSE&message=616%20SB&color=1f1e33)](/LICENSE) A toolset for Arcaea aff files. @@ -20,6 +20,7 @@ Usage: ```csharp using AffTools.Aff2Preview; +AffRenderer.Config.GlobalArcSamplingDensity = 2.0f; // default is 1.0f AffRenderer affRenderer = new("assets/2.aff") { Title = "Gift", @@ -38,7 +39,9 @@ AffRenderer affRenderer = new("assets/2.aff") affRenderer.LoadResource( "assets/note.png", "assets/note_hold.png", - "assets/arc_body.png", + "assets/arctap.png", + "assets/sfx_arctap.png" // empty string if unwanted + "assets/designant_arctap.png" // empty string if unwanted "assets/base.jpg", // empty string if unwanted "assets/base.jpg"); // empty string if unwanted