2024-05-27 10:59:40 +07:00

242 lines
8.6 KiB
C#

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<Token> sequence) : IDisposable
{
internal readonly IEnumerator<Token> TokenEnumerator = sequence.GetEnumerator();
internal BPMChangeData CurrentBpmChange;
internal NotesTime CurrentTime = new(0);
// References all notes between two TimeSteps
internal LinkedList<List<NoteData>> CurrentNoteDataCollections;
// References the current note between EACH dividers
internal List<NoteData> 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;
}
}