using System.Collections.Generic; using System.Globalization; using Manager; using MoreChartFormats.Simai.Errors; using MoreChartFormats.Simai.LexicalAnalysis; namespace MoreChartFormats.Simai.SyntacticAnalysis.States; internal static class NoteReader { public static void Process(Deserializer parent, in Token identityToken, NotesReferences refs, ref int index, ref int noteIndex, ref int slideIndex) { if (!Deserializer.TryReadLocation(in identityToken, out var position, out var touchGroup)) { throw new InvalidSyntaxException(identityToken.line, identityToken.character); } var forceNormal = false; var forceTapless = false; var noteData = new NoteData { type = NotesTypeID.Def.Tap, index = index, startButtonPos = position, beatType = ParserUtilities.GetBeatType(parent.CurrentTime.grid), indexNote = noteIndex++, }; noteData.time.copy(parent.CurrentTime); noteData.end = noteData.time; if (touchGroup != TouchSensorType.Invalid) { // simai does not have syntax for specifying touch size. noteData.touchSize = NoteSize.M1; noteData.touchArea = touchGroup; noteData.type = NotesTypeID.Def.TouchTap; } // Some readers (e.g. NoteReader) moves the enumerator automatically. // We can skip moving the pointer if that's satisfied. var manuallyMoved = false; var noteDataAdded = false; 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: DecorateNote(in token, ref noteData, ref forceNormal, ref forceTapless); break; case TokenType.Slide: { if (!forceNormal && !forceTapless) { if (!StarMapping.TryGetValue(noteData.type, out var def)) { throw new InvalidSyntaxException(token.line, token.character); } noteData.type = def; } if (!forceTapless) { parent.CurrentNoteData.Add(noteData); } SlideReader.Process(parent, in token, refs, noteData, ref index, ref noteIndex, ref slideIndex); noteDataAdded = true; manuallyMoved = true; break; } case TokenType.Duration: ReadDuration(refs, parent.CurrentBpmChange, in token, ref noteData); break; case TokenType.SlideJoiner: throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Slide); case TokenType.TimeStep: case TokenType.EachDivider: case TokenType.EndOfFile: case TokenType.Location: // note terminates here if (!noteDataAdded) { parent.CurrentNoteData.Add(noteData); } return; case TokenType.None: break; default: throw new UnsupportedSyntaxException(token.line, token.character); } } parent.CurrentNoteData.Add(noteData); } private static readonly Dictionary BreakMapping = new() { { NotesTypeID.Def.Tap, NotesTypeID.Def.Break }, { NotesTypeID.Def.ExTap, NotesTypeID.Def.ExBreakTap }, { NotesTypeID.Def.Star, NotesTypeID.Def.BreakStar }, { NotesTypeID.Def.ExStar, NotesTypeID.Def.ExBreakStar }, { NotesTypeID.Def.Hold, NotesTypeID.Def.BreakHold }, { NotesTypeID.Def.ExHold, NotesTypeID.Def.ExBreakHold }, { NotesTypeID.Def.Slide, NotesTypeID.Def.BreakSlide }, }; private static readonly Dictionary ExMapping = new() { { NotesTypeID.Def.Tap, NotesTypeID.Def.ExTap }, { NotesTypeID.Def.Break, NotesTypeID.Def.ExBreakTap }, { NotesTypeID.Def.Star, NotesTypeID.Def.ExStar }, { NotesTypeID.Def.BreakStar, NotesTypeID.Def.ExBreakStar }, { NotesTypeID.Def.Hold, NotesTypeID.Def.ExHold }, { NotesTypeID.Def.BreakHold, NotesTypeID.Def.ExBreakHold }, }; private static readonly Dictionary HoldMapping = new() { { NotesTypeID.Def.Tap, NotesTypeID.Def.Hold }, { NotesTypeID.Def.Break, NotesTypeID.Def.BreakHold }, { NotesTypeID.Def.ExTap, NotesTypeID.Def.ExHold }, { NotesTypeID.Def.ExBreakTap, NotesTypeID.Def.ExBreakHold }, { NotesTypeID.Def.TouchTap, NotesTypeID.Def.TouchHold }, }; private static readonly Dictionary StarMapping = new() { { NotesTypeID.Def.Tap, NotesTypeID.Def.Star }, { NotesTypeID.Def.Break, NotesTypeID.Def.BreakStar }, { NotesTypeID.Def.ExTap, NotesTypeID.Def.ExStar }, { NotesTypeID.Def.ExBreakTap, NotesTypeID.Def.ExBreakStar }, }; private static void DecorateNote(in Token token, ref NoteData noteData, ref bool forceNormal, ref bool forceTapless) { switch (token.lexeme[0]) { case 'f' when noteData.type.isTouch(): noteData.effect = TouchEffectType.Eff1; return; case 'b': { if (!BreakMapping.TryGetValue(noteData.type, out var def)) { return; } noteData.type = def; return; } case 'x': { if (!ExMapping.TryGetValue(noteData.type, out var def)) { return; } noteData.type = def; return; } case 'h': { if (!HoldMapping.TryGetValue(noteData.type, out var def)) { return; } noteData.type = def; return; } case '@': forceNormal = true; return; case '?': forceTapless = true; return; case '!': forceTapless = true; return; case '$': { if (!StarMapping.TryGetValue(noteData.type, out var def)) { return; } noteData.type = def; return; } default: throw new UnsupportedSyntaxException(token.line, token.character); } } private static void ReadDuration(NotesReferences refs, BPMChangeData timing, in Token token, ref NoteData note) { if (HoldMapping.TryGetValue(note.type, out var def)) { note.type = def; } var hashIndex = token.lexeme.IndexOf('#'); if (hashIndex == 0) { if (!float.TryParse(token.lexeme.Substring(1), NumberStyles.Any, CultureInfo.InvariantCulture, out var explicitValue)) throw new UnexpectedCharacterException(token.line, token.character + 1, "0-9 or \".\""); note.end = note.time + ParserUtilities.NotesTimeFromBars(refs, explicitValue / timing.SecondsPerBar()); return; } if (hashIndex != -1) { // The [BPM#a:b] syntax doesn't really make sense for holds? Unless we're adding // a BPM change event just for this hold. I am not bothering. throw new UnsupportedSyntaxException(token.line, token.character); } var separatorIndex = token.lexeme.IndexOf(':'); if (!float.TryParse(token.lexeme.Substring(0, separatorIndex), NumberStyles.Any, CultureInfo.InvariantCulture, out var denominator)) throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\""); if (!float.TryParse(token.lexeme.Substring(separatorIndex + 1), NumberStyles.Any, CultureInfo.InvariantCulture, out var nominator)) throw new UnexpectedCharacterException(token.line, token.character + separatorIndex + 1, "0-9 or \".\""); note.end = note.time + ParserUtilities.NotesTimeFromBars(refs, nominator / denominator); } }