using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; using Manager; using MoreChartFormats.Simai.Errors; using MoreChartFormats.Simai.LexicalAnalysis; using MoreChartFormats.Simai.Structures; using MoreChartFormats.Simai.SyntacticAnalysis.States; namespace MoreChartFormats.Simai.SyntacticAnalysis; internal class Deserializer(IEnumerable sequence) : IDisposable { internal readonly IEnumerator TokenEnumerator = sequence.GetEnumerator(); internal BPMChangeData CurrentBpmChange; internal NotesTime CurrentTime = new(0); // References all notes between two TimeSteps internal LinkedList> CurrentNoteDataCollections; // References the current note between EACH dividers internal List CurrentNoteData; internal float Subdivision = 4f; internal bool IsEndOfFile; public void Dispose() { TokenEnumerator.Dispose(); } public void GetChart(NotesReferences refs) { var manuallyMoved = false; var deltaGrids = 0f; var index = 1; var noteIndex = 0; var slideIndex = 0; var currentBpmChangeIndex = 0; var firstBpmChangeEventIgnored = false; var fakeEach = false; CurrentBpmChange = refs.Composition._bpmList[currentBpmChangeIndex]; while (!IsEndOfFile && (manuallyMoved || MoveNext())) { var token = TokenEnumerator.Current; manuallyMoved = false; switch (token.type) { case TokenType.Subdivision: { if (token.lexeme[0] == '#') { if (!float.TryParse(token.lexeme.Substring(1), NumberStyles.Any, CultureInfo.InvariantCulture, out var absoluteSubdivision)) { throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\""); } Subdivision = CurrentBpmChange.SecondsPerBar() / absoluteSubdivision; } else if (!float.TryParse(token.lexeme, NumberStyles.Any, CultureInfo.InvariantCulture, out Subdivision)) { throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\""); } break; } case TokenType.Location: { // We have to stack up all deltas before adding them because adding it every time // we reach a TimeStep would cause precision loss due to casting floating points // back to integers. var delta = ParserUtilities.NotesTimeFromGrids(refs, (int)deltaGrids); CurrentTime += delta; deltaGrids -= delta.grid; CurrentNoteDataCollections ??= []; CurrentNoteData = []; CurrentNoteDataCollections.AddLast(CurrentNoteData); // ForceEach not supported if (token.lexeme[0] == '0') { throw new UnsupportedSyntaxException(token.line, token.character); } NoteReader.Process(this, in token, refs, ref index, ref noteIndex, ref slideIndex); manuallyMoved = true; break; } case TokenType.TimeStep: { if (CurrentNoteDataCollections != null) { if (fakeEach) { ProcessFakeEach(refs); fakeEach = false; } refs.Notes._noteData.AddRange(CurrentNoteDataCollections.SelectMany(c => c)); CurrentNoteDataCollections = null; CurrentNoteData = null; } deltaGrids += refs.Header._resolutionTime / Subdivision; break; } case TokenType.EachDivider: fakeEach = fakeEach || token.lexeme[0] == '`'; break; case TokenType.Decorator: throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Note); case TokenType.Slide: throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Note); case TokenType.Duration: throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Note | ScopeMismatchException.ScopeType.Slide); case TokenType.SlideJoiner: throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Slide); case TokenType.EndOfFile: // There isn't a way to signal to the game engine that the chart // is ending prematurely, as it expects that every note in the // chart is actually in the chart. IsEndOfFile = true; break; case TokenType.Tempo: if (!firstBpmChangeEventIgnored) { firstBpmChangeEventIgnored = true; } else { CurrentBpmChange = refs.Composition._bpmList[++currentBpmChangeIndex]; } break; case TokenType.None: break; default: throw new UnsupportedSyntaxException(token.line, token.character); } index++; } if (CurrentNoteDataCollections == null) { return; } if (fakeEach) { ProcessFakeEach(refs); } refs.Notes._noteData.AddRange(CurrentNoteDataCollections.SelectMany(c => c)); CurrentNoteDataCollections = null; CurrentNoteData = null; } private void ProcessFakeEach(NotesReferences refs) { var node = CurrentNoteDataCollections.First.Next; var singleTick = new NotesTime(refs.Header._resolutionTime / 384); var delta = singleTick; while (node != null) { foreach (var note in node.Value) { note.time += delta; note.end += delta; note.beatType = ParserUtilities.GetBeatType(note.time.grid); if (note.type.isSlide()) { note.slideData.shoot.time += delta; note.slideData.arrive.time += delta; } delta += singleTick; } node = node.Next; } } internal static bool TryReadLocation(in Token token, out int position, out TouchSensorType touchGroup) { var isSensor = token.lexeme[0] is >= 'A' and <= 'E'; var index = isSensor ? token.lexeme.Substring(1) : token.lexeme; touchGroup = TouchSensorType.Invalid; if (isSensor) { touchGroup = token.lexeme[0] switch { 'A' => TouchSensorType.A, 'B' => TouchSensorType.B, 'C' => TouchSensorType.C, 'D' => TouchSensorType.D, 'E' => TouchSensorType.E, _ => TouchSensorType.Invalid, }; switch (touchGroup) { case TouchSensorType.Invalid: position = -1; return false; case TouchSensorType.C: position = 0; return true; } } if (!int.TryParse(index, out position)) { position = -1; return false; } // Convert to 0-indexed position position -= 1; return true; } internal bool MoveNext() { IsEndOfFile = !TokenEnumerator.MoveNext(); return !IsEndOfFile; } }