2024-05-27 10:59:40 +07:00

394 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using MAI2.Util;
using Manager;
using MoreChartFormats.Simai.Errors;
using MoreChartFormats.Simai.LexicalAnalysis;
using MoreChartFormats.Simai.Structures;
namespace MoreChartFormats.Simai.SyntacticAnalysis.States;
internal static class SlideReader
{
public static void Process(Deserializer parent, in Token identityToken, NotesReferences refs, NoteData starNote, ref int index, ref int noteIndex,
ref int slideIndex)
{
var segments = new LinkedList<SlideSegment>();
var firstSlide = ReadSegment(parent, in identityToken, starNote.startButtonPos, index, ref noteIndex, ref slideIndex);
firstSlide.time = new NotesTime(parent.CurrentTime);
var firstSegment = new SlideSegment { NoteData = firstSlide };
segments.AddLast(firstSegment);
var currentSegment = firstSegment;
var slideStartPos = firstSlide.slideData.targetNote;
// Some readers (e.g. NoteReader) moves the enumerator automatically.
// We can skip moving the pointer if that's satisfied.
var manuallyMoved = true;
while (!parent.IsEndOfFile && (manuallyMoved || parent.MoveNext()))
{
var token = parent.TokenEnumerator.Current;
manuallyMoved = false;
switch (token.type)
{
case TokenType.Tempo:
case TokenType.Subdivision:
throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Global);
case TokenType.Decorator:
DecorateSlide(in token, ref firstSlide);
break;
case TokenType.Slide:
currentSegment = new SlideSegment
{
NoteData = ReadSegment(parent, in token, slideStartPos, ++index,
ref noteIndex, ref slideIndex),
};
segments.AddLast(currentSegment);
slideStartPos = currentSegment.NoteData.slideData.targetNote;
manuallyMoved = true;
break;
case TokenType.Duration:
currentSegment.Timing = ReadDuration(refs, parent.CurrentBpmChange, in token);
break;
case TokenType.SlideJoiner:
ProcessSlideSegments(parent, refs, in identityToken, segments);
slideStartPos = starNote.startButtonPos;
segments.Clear();
break;
case TokenType.TimeStep:
case TokenType.EachDivider:
case TokenType.EndOfFile:
case TokenType.Location:
ProcessSlideSegments(parent, refs, in identityToken, segments);
return;
case TokenType.None:
break;
default:
throw new UnsupportedSyntaxException(token.line, token.character);
}
}
}
private static void ProcessSlideSegments(Deserializer parent, NotesReferences refs, in Token identityToken, LinkedList<SlideSegment> segments)
{
// Fast path for non-festival slides
if (segments.Count == 1)
{
ProcessSingleSlideSegment(parent, refs, segments.First.Value);
return;
}
var segmentsWithTiming = segments.Count(s => s.Timing != null);
if (segmentsWithTiming != 1 && segmentsWithTiming != segments.Count)
{
throw new InvalidSyntaxException(identityToken.line, identityToken.character);
}
if (segmentsWithTiming == segments.Count)
{
ProcessSlideSegmentsAllDurations(parent, refs, segments);
}
else
{
ProcessSlideSegmentsSingleDuration(parent, refs, in identityToken, segments);
}
}
private static void ProcessSingleSlideSegment(Deserializer parent, NotesReferences refs, SlideSegment segment)
{
segment.NoteData.time = new NotesTime(parent.CurrentTime);
segment.NoteData.beatType = ParserUtilities.GetBeatType(segment.NoteData.time.grid);
segment.NoteData.slideData.shoot.time = new NotesTime(segment.NoteData.time);
if (segment.Timing.Delay.HasValue)
{
segment.NoteData.slideData.shoot.time += segment.Timing.Delay.Value;
}
else
{
segment.NoteData.slideData.shoot.time += new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
}
segment.NoteData.slideData.arrive.time = segment.NoteData.slideData.shoot.time + segment.Timing.Duration;
segment.NoteData.end = new NotesTime(segment.NoteData.slideData.arrive.time);
parent.CurrentNoteData.Add(segment.NoteData);
}
private static void ProcessSlideSegmentsAllDurations(Deserializer parent, NotesReferences refs, LinkedList<SlideSegment> segments)
{
var time = parent.CurrentTime;
var isFirstSegment = true;
foreach (var segment in segments)
{
if (!isFirstSegment)
{
segment.NoteData.type = NotesTypeID.Def.ConnectSlide;
}
segment.NoteData.time = new NotesTime(time);
segment.NoteData.beatType = ParserUtilities.GetBeatType(segment.NoteData.time.grid);
segment.NoteData.slideData.shoot.time = new NotesTime(segment.NoteData.time);
if (segment.Timing.Delay.HasValue)
{
segment.NoteData.slideData.shoot.time += segment.Timing.Delay.Value;
}
else if (isFirstSegment)
{
segment.NoteData.slideData.shoot.time += new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
}
segment.NoteData.slideData.arrive.time =
segment.NoteData.slideData.shoot.time + segment.Timing.Duration;
segment.NoteData.end = new NotesTime(segment.NoteData.slideData.arrive.time);
time = segment.NoteData.end;
parent.CurrentNoteData.Add(segment.NoteData);
isFirstSegment = false;
}
}
private static void ProcessSlideSegmentsSingleDuration(Deserializer parent, NotesReferences refs,
in Token identityToken, LinkedList<SlideSegment> segments)
{
var time = parent.CurrentTime;
var slideTiming = segments.Last.Value.Timing;
if (slideTiming == null)
{
throw new InvalidSyntaxException(identityToken.line, identityToken.character);
}
var slideManager = Singleton<SlideManager>.Instance;
var slideLengths = segments.Select(
s => slideManager.GetSlideLength(
s.NoteData.slideData.type, s.NoteData.startButtonPos, s.NoteData.slideData.targetNote)).ToList();
var totalSlideLength = slideLengths.Sum();
var segmentNode = segments.First;
var i = 0;
while (segmentNode != null)
{
var slideLength = slideLengths[i];
var segment = segmentNode.Value;
var isFirstSegment = i == 0;
segment.NoteData.time = new NotesTime(time);
segment.NoteData.beatType = ParserUtilities.GetBeatType(segment.NoteData.time.grid);
segment.NoteData.slideData.shoot.time = new NotesTime(segment.NoteData.time);
if (isFirstSegment)
{
if (slideTiming.Delay.HasValue)
{
segment.NoteData.slideData.shoot.time += slideTiming.Delay.Value;
}
else
{
segment.NoteData.slideData.shoot.time += new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
}
}
else
{
segment.NoteData.type = NotesTypeID.Def.ConnectSlide;
}
var slideDurationSlice = slideLength / totalSlideLength;
var slideDuration = new NotesTime((int)Math.Round(slideTiming.Duration.grid * slideDurationSlice));
segment.NoteData.slideData.arrive.time =
segment.NoteData.slideData.shoot.time + slideDuration;
segment.NoteData.end = new NotesTime(segment.NoteData.slideData.arrive.time);
time = segment.NoteData.end;
parent.CurrentNoteData.Add(segment.NoteData);
segmentNode = segmentNode.Next;
i++;
}
}
private static NoteData ReadSegment(Deserializer parent, in Token identityToken, int startingPosition, int index, ref int noteIndex, ref int slideIndex)
{
var length = identityToken.lexeme.Length;
var vertices = RetrieveVertices(parent, in identityToken);
return new NoteData
{
type = NotesTypeID.Def.Slide,
index = index,
indexNote = noteIndex++,
indexSlide = slideIndex++,
startButtonPos = startingPosition,
slideData = new SlideData
{
targetNote = vertices.Last(),
type = DetermineSlideType(in identityToken, startingPosition, length, vertices),
shoot = new TimingBase { index = noteIndex },
arrive = new TimingBase { index = noteIndex },
},
};
}
private static SlideTiming ReadDuration(NotesReferences refs, BPMChangeData timing, in Token token)
{
// Accepted slide duration formats:
// - {BPM}#{denominator}:{nominator}
// - #{slide duration in seconds}
// - {BPM}#{slide duration in seconds}
// - {seconds}##{slide duration in seconds}
var result = new SlideTiming { Duration = new NotesTime() };
var hashIndex = token.lexeme.IndexOf('#');
var statesIntroDelayDuration = hashIndex > 0;
var durationDeclarationStart = hashIndex + 1;
if (statesIntroDelayDuration)
{
result.Delay = new NotesTime();
var secondHashIndex = token.lexeme.LastIndexOf('#');
var isAbsoluteDelay = secondHashIndex > -1;
if (!float.TryParse(token.lexeme.Substring(0, hashIndex),
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var value))
throw new UnexpectedCharacterException(token.line, token.character + 1, "0-9 or \".\"");
if (isAbsoluteDelay)
{
durationDeclarationStart = secondHashIndex + 1;
result.Delay.Value.copy(ParserUtilities.NotesTimeFromBars(refs, value / timing.SecondsPerBar()));
}
else
{
var grids = (int)Math.Round((float)refs.Header._resolutionTime / 4 * (timing.bpm / value));
result.Delay.Value.copy(ParserUtilities.NotesTimeFromGrids(refs, grids));
}
}
var durationDeclaration = token.lexeme.Substring(durationDeclarationStart);
var separatorIndex = durationDeclaration.IndexOf(':');
if (separatorIndex > -1)
{
if (!float.TryParse(durationDeclaration.Substring(0, separatorIndex),
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var denominator))
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
if (!float.TryParse(durationDeclaration.Substring(separatorIndex + 1),
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var nominator))
throw new UnexpectedCharacterException(token.line, token.character + separatorIndex + 1, "0-9 or \".\"");
result.Duration.copy(ParserUtilities.NotesTimeFromBars(refs, nominator / denominator));
}
else
{
if (!float.TryParse(durationDeclaration,
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var seconds))
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
result.Duration.copy(ParserUtilities.NotesTimeFromBars(refs, seconds / timing.SecondsPerBar()));
}
return result;
}
private static SlideType DetermineSlideType(in Token identityToken, int startingPosition, int length, List<int> vertices)
{
return identityToken.lexeme[0] switch
{
'-' => SlideType.Slide_Straight,
'^' => DetermineRingType(startingPosition, vertices[0]),
'>' => DetermineRingType(startingPosition, vertices[0], 1),
'<' => DetermineRingType(startingPosition, vertices[0], -1),
'V' => DetermineRingType(startingPosition, vertices[0]) switch
{
SlideType.Slide_Circle_L => SlideType.Slide_Skip_L,
SlideType.Slide_Circle_R => SlideType.Slide_Skip_R,
_ => throw new ArgumentOutOfRangeException(),
},
'v' => SlideType.Slide_Corner,
's' => SlideType.Slide_Thunder_R,
'z' => SlideType.Slide_Thunder_L,
'p' when length == 2 && identityToken.lexeme[1] == 'p' => SlideType.Slide_Bend_L,
'q' when length == 2 && identityToken.lexeme[1] == 'q' => SlideType.Slide_Bend_R,
'p' => SlideType.Slide_Curve_L,
'q' => SlideType.Slide_Curve_R,
'w' => SlideType.Slide_Fan,
_ => throw new UnexpectedCharacterException(identityToken.line, identityToken.character, "-, ^, >, <, v, V, s, z, pp, qq, p, q, w"),
};
}
private static void DecorateSlide(in Token token, ref NoteData note)
{
switch (token.lexeme[0])
{
case 'b':
note.type = NotesTypeID.Def.BreakSlide;
return;
default:
throw new UnsupportedSyntaxException(token.line, token.character);
}
}
private static SlideType DetermineRingType(int startPosition, int endPosition, int direction = 0)
{
switch (direction)
{
case 1:
return (startPosition + 2) % 8 >= 4 ? SlideType.Slide_Circle_L : SlideType.Slide_Circle_R;
case -1:
return (startPosition + 2) % 8 >= 4 ? SlideType.Slide_Circle_R : SlideType.Slide_Circle_L;
default:
{
var difference = endPosition - startPosition;
var rotation = difference >= 0
? difference > 4 ? -1 : 1
: difference < -4 ? 1 : -1;
return rotation > 0 ? SlideType.Slide_Circle_R : SlideType.Slide_Circle_L;
}
}
}
private static List<int> RetrieveVertices(Deserializer parent, in Token identityToken)
{
var vertices = new List<int>();
do
{
if (!parent.TokenEnumerator.MoveNext())
throw new UnexpectedCharacterException(identityToken.line, identityToken.character,
"1, 2, 3, 4, 5, 6, 7, 8");
var current = parent.TokenEnumerator.Current;
if (Deserializer.TryReadLocation(in current, out var location, out _))
vertices.Add(location);
} while (parent.TokenEnumerator.Current.type == TokenType.Location);
return vertices;
}
}