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(); 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 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 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 segments) { var time = parent.CurrentTime; var slideTiming = segments.Last.Value.Timing; if (slideTiming == null) { throw new InvalidSyntaxException(identityToken.line, identityToken.character); } var slideManager = Singleton.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 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 RetrieveVertices(Deserializer parent, in Token identityToken) { var vertices = new List(); 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; } }