Skip to content

Commit

Permalink
Rework unit tests again to allow testing all event types, and to test…
Browse files Browse the repository at this point in the history
… all instrument tracks (YARC-Official#20)

* Make SongObjects cloneable and ToString-able

* Rework parse behavior tests again to use ChartObjects and MoonSongs as their sources
.mid generation was rewritten along the way

* Generate and verify all instruments, not just specific ones

* Fix out-of-bounds error when retrieving Harmony 3 charts
Also removed the try/catch from GetChart so issues like this aren't masked away

* Fix .mid time signature denominators not being parsed correctly

* Handle special phrases and local events in chart generation

* Parse in tremolo/trill lanes on Pro Guitar

* Don't automate setting of event tick positions
  • Loading branch information
TheNathannator authored Jun 13, 2023
1 parent b070f08 commit 73695b2
Show file tree
Hide file tree
Showing 16 changed files with 867 additions and 298 deletions.
197 changes: 131 additions & 66 deletions YARG.Core.UnitTests/Parsing/ParseBehaviorTests.Chart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,105 +58,170 @@ public class ChartParseBehaviorTests
{ GameMode.GHLGuitar, GhlGuitarNoteLookup },
};

private static void GenerateSection(StringBuilder builder, List<MoonNote> data, MoonInstrument instrument, Difficulty difficulty)
private static readonly Dictionary<SpecialPhrase.Type, int> SpecialPhraseLookup = new()
{
string instrumentName = InstrumentToNameLookup[instrument];
{ SpecialPhrase.Type.Starpower, PHRASE_STARPOWER },
{ SpecialPhrase.Type.Versus_Player1, PHRASE_VERSUS_PLAYER_1 },
{ SpecialPhrase.Type.Versus_Player2, PHRASE_VERSUS_PLAYER_2 },
{ SpecialPhrase.Type.TremoloLane, PHRASE_TREMOLO_LANE },
{ SpecialPhrase.Type.TrillLane, PHRASE_TRILL_LANE },
{ SpecialPhrase.Type.ProDrums_Activation, PHRASE_DRUM_FILL },
};

private static void GenerateSongSection(MoonSong sourceSong, StringBuilder builder)
{
builder.Append($"{SECTION_SONG}\n{{\n");
builder.Append($" Resolution = {sourceSong.resolution}");
builder.Append("}\n");
}

private static void GenerateSyncSection(MoonSong sourceSong, StringBuilder builder)
{
builder.Append($"{SECTION_SYNC_TRACK}\n{{\n");
foreach (var sync in sourceSong.syncTrack)
{
switch (sync)
{
case BPM bpm:
builder.Append($" {bpm.tick} = B {bpm.value}");
break;
case TimeSignature ts:
builder.Append($" {ts.tick} = TS {ts.numerator} {(int)Math.Log2(ts.denominator)}");
break;
}
}
builder.Append("}\n");
}

private static void GenerateEventsSection(MoonSong sourceSong, StringBuilder builder)
{
builder.Append($"{SECTION_EVENTS}\n{{\n");
foreach (var text in sourceSong.eventsAndSections)
{
builder.Append($" {text.tick} = E \"{text.title}\"");
}
builder.Append("}\n");
}

private static void GenerateInstrumentSection(MoonSong sourceSong, StringBuilder builder, MoonInstrument instrument, Difficulty difficulty)
{
// Skip unsupported instruments
var gameMode = MoonSong.InstumentToChartGameMode(instrument);
if (!InstrumentToNoteLookupLookup.ContainsKey(gameMode))
return;

var chart = sourceSong.GetChart(instrument, difficulty);

string instrumentName = InstrumentToNameLookup[instrument];
string difficultyName = DifficultyToNameLookup[difficulty];
builder.Append($"[{difficultyName}{instrumentName}]\n{{\n");

List<ChartObject> eventsToRemove = new();
foreach (var chartObj in chart.chartObjects)
{
switch (chartObj)
{
case MoonNote note:
AppendNote(builder, note);
break;
case SpecialPhrase phrase:
// Drums-only phrases
if (gameMode is not GameMode.Drums && phrase.type is SpecialPhrase.Type.TremoloLane or
SpecialPhrase.Type.TrillLane or SpecialPhrase.Type.ProDrums_Activation)
{
eventsToRemove.Add(chartObj);
continue;
}
int phraseNumber = SpecialPhraseLookup[phrase.type];
builder.Append($" {phrase.tick} = S {phraseNumber} {phrase.length}\n");
break;
case ChartEvent text:
builder.Append($" {text.tick} = E {text.eventName}\n");
break;
}
}

foreach (var chartObj in eventsToRemove)
{
chart.Remove(chartObj);
}

builder.Append("}\n");
}

private static void AppendNote(StringBuilder builder, MoonNote note)
{
uint tick = note.tick;
var flags = note.flags;
var gameMode = note.gameMode;

bool canForce = gameMode is GameMode.Guitar or GameMode.GHLGuitar;
bool canTap = gameMode is GameMode.Guitar or GameMode.GHLGuitar;
bool canCymbal = gameMode is GameMode.Drums;
bool canDoubleKick = gameMode is GameMode.Drums;
bool canDynamics = gameMode is GameMode.Drums;

var noteLookup = InstrumentToNoteLookupLookup[gameMode];
for (int index = 0; index < data.Count; index++)
{
uint tick = RESOLUTION * (uint)index;
var note = data[index];
var flags = note.flags;

// Not technically necessary, but might as well lol
int rawNote = gameMode switch {
GameMode.Guitar => (int)note.guitarFret,
GameMode.GHLGuitar => (int)note.ghliveGuitarFret,
GameMode.ProGuitar => throw new NotSupportedException(".chart does not support Pro Guitar!"),
GameMode.Drums => (int)note.drumPad,
_ => note.rawNote
};

int chartNumber = noteLookup[rawNote];
if (canDoubleKick && (flags & Flags.DoubleKick) != 0)
chartNumber = NOTE_OFFSET_INSTRUMENT_PLUS;

builder.Append($" {tick} = N {chartNumber} {note.length}\n");
if (canForce && (flags & Flags.Forced) != 0)
builder.Append($" {tick} = N 5 0\n");
if (canTap && (flags & Flags.Tap) != 0)
builder.Append($" {tick} = N 6 0\n");
if (canCymbal && (flags & Flags.ProDrums_Cymbal) != 0)
builder.Append($" {tick} = N {NOTE_OFFSET_PRO_DRUMS + chartNumber} 0\n");
if (canDynamics && (flags & Flags.ProDrums_Accent) != 0)
builder.Append($" {tick} = N {NOTE_OFFSET_DRUMS_ACCENT + chartNumber} 0\n");
if (canDynamics && (flags & Flags.ProDrums_Ghost) != 0)
builder.Append($" {tick} = N {NOTE_OFFSET_DRUMS_GHOST + chartNumber} 0\n");
}
builder.Append("}\n");

// Not technically necessary, but might as well lol
int rawNote = gameMode switch {
GameMode.Guitar => (int)note.guitarFret,
GameMode.GHLGuitar => (int)note.ghliveGuitarFret,
GameMode.ProGuitar => throw new NotSupportedException(".chart does not support Pro Guitar!"),
GameMode.Drums => (int)note.drumPad,
_ => note.rawNote
};

int chartNumber = noteLookup[rawNote];
if (canDoubleKick && (flags & Flags.DoubleKick) != 0)
chartNumber = NOTE_OFFSET_INSTRUMENT_PLUS;

builder.Append($" {tick} = N {chartNumber} {note.length}\n");
if (canForce && (flags & Flags.Forced) != 0)
builder.Append($" {tick} = N 5 0\n");
if (canTap && (flags & Flags.Tap) != 0)
builder.Append($" {tick} = N 6 0\n");
if (canCymbal && (flags & Flags.ProDrums_Cymbal) != 0)
builder.Append($" {tick} = N {NOTE_OFFSET_PRO_DRUMS + chartNumber} 0\n");
if (canDynamics && (flags & Flags.ProDrums_Accent) != 0)
builder.Append($" {tick} = N {NOTE_OFFSET_DRUMS_ACCENT + chartNumber} 0\n");
if (canDynamics && (flags & Flags.ProDrums_Ghost) != 0)
builder.Append($" {tick} = N {NOTE_OFFSET_DRUMS_GHOST + chartNumber} 0\n");
}

private static string GenerateChartFile()
private static string GenerateChartFile(MoonSong sourceSong)
{
string header = $$"""
{{SECTION_SONG}}
{
Resolution = {{RESOLUTION}}
}
{{SECTION_SYNC_TRACK}}
var chartBuilder = new StringBuilder(5000);
GenerateSongSection(sourceSong, chartBuilder);
GenerateSyncSection(sourceSong, chartBuilder);
GenerateEventsSection(sourceSong, chartBuilder);
foreach (var instrument in EnumX<MoonInstrument>.Values)
{
foreach (var difficulty in EnumX<Difficulty>.Values)
{
{{RESOLUTION * 0}} = TS {{NUMERATOR}} {{DENOMINATOR_POW2}}
{{RESOLUTION * 0}} = B {{(int)(TEMPO * 1000)}}
GenerateInstrumentSection(sourceSong, chartBuilder, instrument, difficulty);
}
"""; // Trailing newline is deliberate

var chartBuilder = new StringBuilder(header, 1000);
foreach (var difficulty in EnumX<Difficulty>.Values)
{
GenerateSection(chartBuilder, GuitarNotes, MoonInstrument.Guitar, difficulty);
GenerateSection(chartBuilder, GhlGuitarNotes, MoonInstrument.GHLiveGuitar, difficulty);
GenerateSection(chartBuilder, DrumsNotes, MoonInstrument.Drums, difficulty);
}
return chartBuilder.ToString();
}

[TestCase]
public void GenerateAndParseChartFile()
{
string chartText = GenerateChartFile();
MoonSong song;
var sourceSong = GenerateSong();
string chartText = GenerateChartFile(sourceSong);
MoonSong parsedSong;
try
{
song = ChartReader.ReadChart(new StringReader(chartText));
parsedSong = ChartReader.ReadChart(new StringReader(chartText));
}
catch (Exception ex)
{
Assert.Fail($"Chart parsing threw an exception!\n{ex}");
return;
}

Assert.Multiple(() =>
{
VerifyMetadata(song);
VerifySync(song);
foreach (var difficulty in EnumX<Difficulty>.Values)
{
VerifyTrack(song, GuitarNotes, MoonInstrument.Guitar, difficulty);
VerifyTrack(song, GhlGuitarNotes, MoonInstrument.GHLiveGuitar, difficulty);
VerifyTrack(song, DrumsNotes, MoonInstrument.Drums, difficulty);
}
});
VerifySong(sourceSong, parsedSong, InstrumentToNoteLookupLookup.Keys);
}
}
}
Loading

0 comments on commit 73695b2

Please sign in to comment.