Files
ProjectQuint/Assets/ProjectQuint/Tests/Core/BmsParsingTests.cs
T

469 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using ProjectQuint.Core.Bms;
using ProjectQuint.Core.Charts;
using ProjectQuint.Core.Course;
using ProjectQuint.Core.Gauge;
using ProjectQuint.Core.Judgement;
using ProjectQuint.Core.Timing;
namespace ProjectQuint.Tests
{
public sealed class BmsParsingTests
{
[Test]
public void Parser_DecodesShiftJisJapaneseMetadata()
{
var directory = Path.Combine(Path.GetTempPath(), "ProjectQuint-ShiftJis-" + Guid.NewGuid().ToString("N"));
var path = Path.Combine(directory, "chart.bms");
Directory.CreateDirectory(directory);
try
{
var bytes = new List<byte>();
bytes.AddRange(Encoding.ASCII.GetBytes("#TITLE PUPA\r\n#ARTIST "));
bytes.AddRange(new byte[]
{
0x83, 0x82, 0x83, 0x8A, 0x83, 0x82, 0x83, 0x8A,
0x82, 0xA0, 0x82, 0xC2, 0x82, 0xB5,
});
bytes.AddRange(Encoding.ASCII.GetBytes("\r\n#BPM 202\r\n"));
File.WriteAllBytes(path, bytes.ToArray());
var metadata = BmsParser.ReadMetadataFile(path);
Assert.That(metadata.Title, Is.EqualTo("PUPA"));
Assert.That(metadata.Artist, Is.EqualTo("モリモリあつし"));
}
finally
{
if (Directory.Exists(directory))
Directory.Delete(directory, true);
}
}
[Test]
public void Parser_Base62PreservesCaseSensitiveObjectIdsAcrossAllTimelines()
{
const string source = @"
#TITLE Base62 Test
#BPM 120
#WAVAA upper.wav
#WAVAa lower.wav
#BMPBA upper.png
#BMPBa lower.png
#BPMCA 150
#BPMCa 180
#STOPDA 48
#STOPDa 96
#SCROLLEA 1.25
#SCROLLEa 0.5
#LNOBJ Aa
#00101:AAAa
#00104:BABa
#00108:CACa
#00109:DADa
#001SC:EAEa
#00211:AAAAAa
#RANDOM 1
#IF 1
#BASE 62
#ENDIF
#ENDRANDOM
";
var document = BmsParser.ParseText("base62.bms", source);
var compiled = BmsTimelineCompiler.Compile(document);
var chart = BmsPlayableChartConverter.Convert(compiled).Chart;
Assert.That(document.Metadata.ObjectBase, Is.EqualTo(62));
Assert.That(document.WavDefinitions, Has.Count.EqualTo(2));
Assert.That(document.WavDefinitions["AA"], Is.EqualTo("upper.wav"));
Assert.That(document.WavDefinitions["Aa"], Is.EqualTo("lower.wav"));
Assert.That(document.BmpDefinitions["BA"], Is.EqualTo("upper.png"));
Assert.That(document.BmpDefinitions["Ba"], Is.EqualTo("lower.png"));
Assert.That(document.ExtendedBpms.Keys, Is.EquivalentTo(new[] { "CA", "Ca" }));
Assert.That(document.Stops.Keys, Is.EquivalentTo(new[] { "DA", "Da" }));
Assert.That(document.Scrolls.Keys, Is.EquivalentTo(new[] { "EA", "Ea" }));
Assert.That(document.Metadata.LnObj, Is.EqualTo("Aa"));
Assert.That(chart.AudioResources, Has.Count.EqualTo(2));
Assert.That(chart.AudioResources["AA"], Is.EqualTo("upper.wav"));
Assert.That(chart.AudioResources["Aa"], Is.EqualTo("lower.wav"));
Assert.That(chart.BackgroundAudioEvents.Select(item => item.SoundId),
Is.EqualTo(new[] { "AA", "Aa" }));
Assert.That(chart.VisualEvents.Select(item => item.ResourceId),
Is.EqualTo(new[] { "BA", "Ba" }));
Assert.That(chart.Notes, Has.Count.EqualTo(2));
Assert.That(chart.Notes.Count(item => item.IsLong), Is.EqualTo(1));
Assert.That(chart.Notes.Single(item => item.IsLong).TailSoundId, Is.EqualTo("Aa"));
Assert.That(chart.Warnings.Any(item => item.StartsWith("Undefined", StringComparison.Ordinal)), Is.False);
}
[Test]
public void Parser_Base36NormalizesLowercaseObjectIdsEvenWhenBaseIsDeclaredLast()
{
const string source = @"
#BPM 120
#WAVAa hit.wav
#BMPbB image.png
#LNOBJ cC
#00111:AaAaCc
#00104:bB
#BASE 36
";
var document = BmsParser.ParseText("base36-lowercase.bms", source);
var chart = BmsTimelineCompiler.Compile(document);
Assert.That(document.Metadata.ObjectBase, Is.EqualTo(36));
Assert.That(document.WavDefinitions.Keys, Is.EqualTo(new[] { "AA" }));
Assert.That(document.BmpDefinitions.Keys, Is.EqualTo(new[] { "BB" }));
Assert.That(document.Metadata.LnObj, Is.EqualTo("CC"));
Assert.That(document.Events.All(item => item.ObjectId == item.ObjectId.ToUpperInvariant()), Is.True);
Assert.That(chart.Notes, Has.Count.EqualTo(2));
Assert.That(chart.Notes.Single(item => item.IsLong).TailSoundId, Is.EqualTo("CC"));
Assert.That(chart.BgaEvents.Single().ResourceId, Is.EqualTo("BB"));
}
[Test]
public void Parser_RejectsUnknownObjectBaseInsteadOfSilentlyCorruptingIds()
{
const string source = "#BASE 64\n#WAVAA hit.wav\n#00111:AA";
Assert.That(
() => BmsParser.ParseText("unsupported-base.bms", source),
Throws.TypeOf<FormatException>()
.With.Message.Contains("supports only #BASE 36 and #BASE 62"));
}
[Test]
public void Parser_RejectsObsoleteLnTypeTwoInsteadOfMisreadingLongNotes()
{
const string source = "#LNTYPE 2\n#BPM 120\n#00151:0001010100";
Assert.That(
() => BmsParser.ParseText("obsolete-mgq-ln.bms", source),
Throws.TypeOf<FormatException>()
.With.Message.Contains("Unsupported BMS #LNTYPE 2")
.And.Message.Contains("#LNTYPE 1 and #LNOBJ"));
}
[Test]
public void Parser_AcceptsSupportedLnTypeOne()
{
const string source = "#LNTYPE 1\n#BPM 120\n#00151:00010001";
var document = BmsParser.ParseText("standard-ln.bms", source);
var chart = BmsTimelineCompiler.Compile(document);
Assert.That(document.Metadata.LnType, Is.EqualTo(1));
Assert.That(chart.Notes.Count(note => note.IsLong), Is.EqualTo(1));
}
[Test]
public void Converter_PreservesScratchAsFixedLeftLane()
{
const string source = @"
#TITLE Smoke Test
#ARTIST ProjectQuint
#BPM 120
#WAV01 kick.wav
#00111:01
#00112:0001
#00116:01
";
var compiled = BmsTimelineCompiler.Compile(BmsParser.ParseText("smoke.bms", source));
var chart = BmsPlayableChartConverter.Convert(compiled).Chart;
Assert.That(chart.Metadata.Title, Is.EqualTo("Smoke Test"));
Assert.That(chart.Metadata.KeyCount, Is.EqualTo(5));
Assert.That(chart.Metadata.HasScratch, Is.True);
Assert.That(chart.Metadata.TotalLaneCount, Is.EqualTo(6));
Assert.That(chart.Notes, Has.Count.EqualTo(3));
Assert.That(chart.Notes[0].Lane, Is.EqualTo(0));
Assert.That(chart.Notes[0].IsScratch, Is.True);
Assert.That(chart.Notes[0].StartTime, Is.EqualTo(2.0).Within(0.000001));
Assert.That(chart.Notes, Has.Some.Property("Lane").EqualTo(1));
Assert.That(chart.Notes, Has.Some.Property("Lane").EqualTo(2));
Assert.That(chart.ScratchObjectCount, Is.EqualTo(1));
}
[Test]
public void Parser_PreservesDeclaredTotalForGaugeCalculation()
{
const string source = @"
#TITLE Gauge Test
#BPM 120
#TOTAL 470.5
#00111:01
";
var chart = BmsTimelineCompiler.Compile(BmsParser.ParseText("total.bms", source));
Assert.That(chart.Metadata.Total, Is.EqualTo(470.5).Within(0.000001));
}
[Test]
public void LaneModifier_MirrorReversesAllFiveLanes()
{
var laneMap = LanePermutation.Create(LaneModifier.Mirror);
Assert.That(laneMap, Is.EqualTo(new[] { 4, 3, 2, 1, 0 }));
}
[Test]
public void LaneModifier_MirrorKeepsScratchFixedAndReversesSevenKeys()
{
var laneMap = LanePermutation.Create(LaneModifier.Mirror, keyCount: 7, hasScratch: true);
Assert.That(laneMap, Is.EqualTo(new[] { 0, 7, 6, 5, 4, 3, 2, 1 }));
}
[Test]
public void ParserAndConverter_SupportOnePlusSevenAsEightGameplayLanes()
{
const string source = @"
#TITLE 1+7 Test
#BPM 120
#WAV01 hit.wav
#00111:01
#00112:01
#00113:01
#00114:01
#00115:01
#00118:01
#00119:01
#00116:01
";
var compiled = BmsTimelineCompiler.Compile(BmsParser.ParseText("one-plus-seven.bms", source));
var chart = BmsPlayableChartConverter.Convert(compiled).Chart;
Assert.That(chart.Metadata.KeyCount, Is.EqualTo(7));
Assert.That(chart.Metadata.HasScratch, Is.True);
Assert.That(chart.Metadata.TotalLaneCount, Is.EqualTo(8));
Assert.That(chart.Notes, Has.Count.EqualTo(8));
Assert.That(chart.Notes.Select(note => note.Lane), Is.EquivalentTo(Enumerable.Range(0, 8)));
Assert.That(chart.Notes.Single(note => note.IsScratch).Lane, Is.Zero);
}
[TestCase(7, false, "7K")]
[TestCase(7, true, "1+7K")]
[TestCase(5, false, "5K")]
[TestCase(5, true, "1+5K")]
public void KeyModeLabel_DistinguishesKeysFromScratchAndKeys(
int keyCount,
bool hasScratch,
string expected)
{
Assert.That(
ChartKeyMode.FormatLabel(keyCount, hasScratch),
Is.EqualTo(expected));
}
[TestCase("#00111:01\n#00112:01\n#00114:01\n#00115:01", 4)]
[TestCase("#00111:01\n#00112:01\n#00113:01\n#00114:01\n#00115:01", 5)]
[TestCase("#00111:01\n#00112:01\n#00113:01\n#00115:01\n#00118:01\n#00119:01", 6)]
[TestCase("#00111:01\n#00112:01\n#00113:01\n#00114:01\n#00115:01\n#00118:01\n#00119:01", 7)]
[TestCase("#00151:0102\n#00152:0102\n#00154:0102\n#00155:0102", 4)]
public void Parser_DetectsSupportedKeyLayouts(string events, int expectedKeyCount)
{
var document = BmsParser.ParseText(
"layout.bms",
"#BPM 120\n#WAV01 hit.wav\n#WAV02 tail.wav\n" + events);
var chart = BmsTimelineCompiler.Compile(document);
Assert.That(chart.Metadata.KeyCount, Is.EqualTo(expectedKeyCount));
Assert.That(chart.Notes.Select(note => note.Lane).Distinct().Count(), Is.EqualTo(expectedKeyCount));
}
[Test]
public void SixKeyContract_SkipsCenterChannelAndKeepsRightmostChannel()
{
var document = BmsParser.ParseText(
"six-key-with-center-stray.bms",
"#BPM 120\n" +
"#WAV01 one.wav\n#WAV02 two.wav\n#WAV03 three.wav\n" +
"#WAV04 stray.wav\n#WAV05 five.wav\n#WAV06 six.wav\n#WAV07 seven.wav\n" +
"#00111:01\n#00112:02\n#00113:03\n#00114:04\n" +
"#00115:05\n#00118:06\n#00119:07\n");
document.Metadata.KeyCount = 6;
var chart = BmsTimelineCompiler.Compile(document);
Assert.That(chart.Metadata.KeyCount, Is.EqualTo(6));
Assert.That(chart.Notes, Has.Count.EqualTo(6));
Assert.That(chart.Notes.Select(note => note.Lane),
Is.EquivalentTo(Enumerable.Range(0, 6)));
Assert.That(chart.Notes.Select(note => note.SoundId), Does.Contain("07"));
Assert.That(chart.Notes.Select(note => note.SoundId), Does.Not.Contain("04"));
}
[Test]
public void LaneModifier_RandomCreatesOneDeterministicPermutationPerPlay()
{
var first = LanePermutation.Create(LaneModifier.Random, randomSeed: 31415);
var second = LanePermutation.Create(LaneModifier.Random, randomSeed: 31415);
Assert.That(first, Is.EqualTo(second));
Assert.That(first, Is.EquivalentTo(new[] { 0, 1, 2, 3, 4 }));
Assert.That(first, Is.Not.EqualTo(new[] { 0, 1, 2, 3, 4 }));
}
[Test]
public void PlayableChartConverter_AppliesTheSameMappingWithoutMutatingSourceNotes()
{
var notes = Enumerable.Range(0, 5)
.Select(lane => new BmsNote { Id = lane, Lane = lane })
.ToArray();
var source = new BmsChart();
source.Notes.AddRange(notes);
var conversion = BmsPlayableChartConverter.Convert(source, LaneModifier.Random, randomSeed: 7);
for (var lane = 0; lane < notes.Length; lane++)
{
Assert.That(conversion.Chart.Notes.Single(note => note.Id == lane).Lane,
Is.EqualTo(conversion.LaneMap[lane]));
Assert.That(notes[lane].Lane, Is.EqualTo(lane));
}
}
[Test]
public void InvisibleBmsChannels_BecomeModifierAwareLaneSoundEvents()
{
var document = BmsParser.ParseText(
"invisible.bms",
"#BPM 120\n#WAV01 visible.wav\n#WAV02 empty.wav\n#00111:01\n#00231:02");
var source = BmsTimelineCompiler.Compile(document);
Assert.That(source.LaneSoundEvents, Has.Count.EqualTo(1));
Assert.That(source.LaneSoundEvents[0].Lane, Is.Zero);
Assert.That(source.LaneSoundEvents[0].SoundId, Is.EqualTo("02"));
var conversion = BmsPlayableChartConverter.Convert(source, LaneModifier.Mirror);
Assert.That(conversion.Chart.LaneSoundEvents, Has.Count.EqualTo(1));
Assert.That(conversion.Chart.LaneSoundEvents[0].Lane, Is.EqualTo(4));
Assert.That(conversion.Chart.LaneSoundEvents[0].SoundId, Is.EqualTo("02"));
}
[Test]
public void PlayableChartConverter_PreservesLongNoteIdentityTimingAndResourceTimelines()
{
var source = new BmsChart
{
Metadata = new BmsMetadata
{
ChartPath = "/songs/example/chart.bms",
Title = "Example",
Artist = "Composer",
StageFile = "stage.png",
Total = 420,
},
};
source.WavDefinitions["01"] = "head.wav";
source.BmpDefinitions["02"] = "movie.mp4";
source.Notes.Add(new BmsNote
{
Id = 42,
Lane = 1,
StartTime = 1.25,
EndTime = 3.5,
SoundId = "01",
TailSoundId = "09",
});
source.BackgroundSounds.Add(new BmsAudioEvent { Time = 0.5, SoundId = "01" });
source.BgaEvents.Add(new BmsBgaEvent
{
Time = 0.75,
ResourceId = "02",
Layer = BmsBgaLayer.Layer,
});
var chart = BmsPlayableChartConverter.Convert(source, LaneModifier.Mirror).Chart;
Assert.That(chart.Notes, Has.Count.EqualTo(1));
Assert.That(chart.Notes[0].Id, Is.EqualTo(42));
Assert.That(chart.Notes[0].Lane, Is.EqualTo(3));
Assert.That(chart.Notes[0].StartTime, Is.EqualTo(1.25));
Assert.That(chart.Notes[0].EndTime, Is.EqualTo(3.5));
Assert.That(chart.Notes[0].SoundId, Is.EqualTo("01"));
Assert.That(chart.Notes[0].TailSoundId, Is.EqualTo("09"));
Assert.That(chart.Metadata.SourcePath, Is.EqualTo("/songs/example/chart.bms"));
Assert.That(chart.Metadata.GaugeTotal, Is.EqualTo(420));
Assert.That(chart.AudioResources["01"], Is.EqualTo("head.wav"));
Assert.That(chart.VisualResources["02"], Is.EqualTo("movie.mp4"));
Assert.That(chart.BackgroundAudioEvents, Has.Count.EqualTo(1));
Assert.That(chart.VisualEvents[0].Layer, Is.EqualTo(PlayableVisualLayer.Overlay));
}
[Test]
public void PlayableChartConverter_PreservesScratchSoundAndDuration()
{
var source = new BmsChart
{
Metadata = new BmsMetadata { KeyCount = 5, HasScratch = true },
ScratchObjectCount = 1,
Duration = 102.0,
};
source.Notes.Add(new BmsNote
{
Id = 1,
Lane = 1,
StartTime = 1.0,
EndTime = 1.0,
SoundId = "KEY",
});
source.Notes.Add(new BmsNote
{
Id = 2,
Lane = 0,
StartTime = 100.0,
EndTime = 100.0,
SoundId = "SCRATCH",
IsScratch = true,
});
var chart = BmsPlayableChartConverter.Convert(source).Chart;
Assert.That(chart.Notes, Has.Count.EqualTo(2));
Assert.That(chart.Notes, Has.Some.Property("SoundId").EqualTo("KEY"));
Assert.That(chart.Notes, Has.Some.Property("SoundId").EqualTo("SCRATCH"));
Assert.That(chart.ScratchObjectCount, Is.EqualTo(1));
Assert.That(chart.Duration, Is.EqualTo(102.0));
Assert.That(source.Notes, Has.Count.EqualTo(2));
Assert.That(source.Duration, Is.EqualTo(102.0));
}
[Test]
public void ParserAndTimeline_CompileBmsScrollWithoutChangingJudgementTime()
{
const string source = @"
#TITLE Scroll Test
#BPM 120
#SCROLL01 0.5
#SCROLL02 2.0
#001SC:0102
#00111:0101
";
var document = BmsParser.ParseText("scroll.bms", source);
var chart = BmsTimelineCompiler.Compile(document);
Assert.That(document.Scrolls["01"], Is.EqualTo(0.5));
Assert.That(document.Scrolls["02"], Is.EqualTo(2.0));
Assert.That(chart.Notes[0].StartTime, Is.EqualTo(2.0).Within(0.000001));
Assert.That(chart.Notes[1].StartTime, Is.EqualTo(3.0).Within(0.000001));
Assert.That(chart.ScrollMap.PositionAtBeat(4), Is.EqualTo(4).Within(0.000001));
Assert.That(chart.ScrollMap.PositionAtBeat(6), Is.EqualTo(5).Within(0.000001));
Assert.That(chart.ScrollMap.PositionAtBeat(8), Is.EqualTo(9).Within(0.000001));
}
}
}