mirror of
https://gitea.tendokyu.moe/beerpsi/sinmai-mods.git
synced 2025-02-12 00:43:00 +01:00
394 lines
16 KiB
C#
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;
|
|
}
|
|
}
|