mirror of
https://gitea.tendokyu.moe/beerpsi/sinmai-mods.git
synced 2025-02-17 19:09:21 +01:00
242 lines
8.6 KiB
C#
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;
|
|
}
|
|
} |