mirror of
https://gitea.tendokyu.moe/beerpsi/sinmai-mods.git
synced 2025-02-14 09:42:35 +01:00
255 lines
9.4 KiB
C#
255 lines
9.4 KiB
C#
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<NotesTypeID, NotesTypeID.Def> 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<NotesTypeID, NotesTypeID.Def> 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<NotesTypeID, NotesTypeID.Def> 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<NotesTypeID, NotesTypeID.Def> 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);
|
|
}
|
|
} |