diff --git a/TestConsole/CacheLoader.cs b/TestConsole/CacheLoader.cs index e0d4dc0d4..bc5367916 100644 --- a/TestConsole/CacheLoader.cs +++ b/TestConsole/CacheLoader.cs @@ -31,7 +31,7 @@ public static SongCache LoadCache() throw new Exception($"Please add at least one song directory to {SongDirsPath}"); var task = Task.Run(() => CacheHandler.RunScan( - fast: false, + tryQuickScan: false, SongCachePath, BadSongsPath, MULTITHREADING, diff --git a/YARG.Core.Benchmarks/Parsing/DotChartParsingBenchmarks.cs b/YARG.Core.Benchmarks/Parsing/DotChartParsingBenchmarks.cs index 590fa7de3..e53306e09 100644 --- a/YARG.Core.Benchmarks/Parsing/DotChartParsingBenchmarks.cs +++ b/YARG.Core.Benchmarks/Parsing/DotChartParsingBenchmarks.cs @@ -9,7 +9,6 @@ namespace YARG.Core.Benchmarks // [SimpleJob(RunStrategy.ColdStart, targetCount: 25, invocationCount: 1)] public class DotChartParsingBenchmarks { - private ParseSettings settings = ParseSettings.Default; private string chartText; [GlobalSetup] @@ -25,7 +24,7 @@ public void Initialize() [Benchmark] public SongChart ChartParsing() { - return SongChart.FromDotChart(settings, chartText); + return SongChart.FromDotChart(in ParseSettings.Default_Chart, chartText); } } } diff --git a/YARG.Core.Benchmarks/Parsing/MidiParsingBenchmarks.cs b/YARG.Core.Benchmarks/Parsing/MidiParsingBenchmarks.cs index 578712db4..869e83171 100644 --- a/YARG.Core.Benchmarks/Parsing/MidiParsingBenchmarks.cs +++ b/YARG.Core.Benchmarks/Parsing/MidiParsingBenchmarks.cs @@ -9,8 +9,6 @@ namespace YARG.Core.Benchmarks // [SimpleJob(RunStrategy.ColdStart, targetCount: 25, invocationCount: 1)] public class MidiParsingBenchmarks { - - private ParseSettings settings = ParseSettings.Default; private MidiFile midi; [GlobalSetup] @@ -26,7 +24,7 @@ public void Initialize() [Benchmark] public SongChart ChartParsing() { - return SongChart.FromMidi(settings, midi); + return SongChart.FromMidi(in ParseSettings.Default_Midi, midi); } } } diff --git a/YARG.Core.UnitTests/Engine/DrumEngineTester.cs b/YARG.Core.UnitTests/Engine/DrumEngineTester.cs index 20b3b7a1b..94d316c91 100644 --- a/YARG.Core.UnitTests/Engine/DrumEngineTester.cs +++ b/YARG.Core.UnitTests/Engine/DrumEngineTester.cs @@ -20,8 +20,6 @@ public class DrumEngineTester private string? _chartsDirectory; - private readonly ParseSettings _settings = ParseSettings.Default; - [SetUp] public void Setup() { @@ -37,7 +35,7 @@ public void DrumSoloThatEndsInChord_ShouldWorkCorrectly() { var chartPath = Path.Combine(_chartsDirectory!, "drawntotheflame.mid"); var midi = MidiFile.Read(chartPath); - var chart = SongChart.FromMidi(_settings, midi); + var chart = SongChart.FromMidi(in ParseSettings.Default_Midi, midi); var notes = chart.ProDrums.GetDifficulty(Difficulty.Expert); var engine = new YargDrumsEngine(notes, chart.SyncTrack, _engineParams, true); @@ -56,7 +54,7 @@ public void DrumTrackWithKickDrumRemoved_ShouldWorkCorrectly() { var chartPath = Path.Combine(_chartsDirectory!, "drawntotheflame.mid"); var midi = MidiFile.Read(chartPath); - var chart = SongChart.FromMidi(_settings, midi); + var chart = SongChart.FromMidi(in ParseSettings.Default_Midi, midi); var notes = chart.ProDrums.GetDifficulty(Difficulty.Expert); notes.RemoveKickDrumNotes(); diff --git a/YARG.Core.UnitTests/Parsing/ParseBehaviorTests.Midi.cs b/YARG.Core.UnitTests/Parsing/ParseBehaviorTests.Midi.cs index 300ca712a..d5e8b9297 100644 --- a/YARG.Core.UnitTests/Parsing/ParseBehaviorTests.Midi.cs +++ b/YARG.Core.UnitTests/Parsing/ParseBehaviorTests.Midi.cs @@ -20,7 +20,7 @@ namespace YARG.Core.UnitTests.Parsing public class MidiParseBehaviorTests { private const uint SUSTAIN_CUTOFF_THRESHOLD = RESOLUTION / 3; - private static readonly uint HopoThreshold = (uint)GetHopoThreshold(ParseSettings.Default, RESOLUTION); + private const uint HOPO_THRESHOLD = (RESOLUTION / 3) + 1; private static readonly Dictionary InstrumentToNameLookup = new() { @@ -450,7 +450,7 @@ private static void GenerateNotesForDifficulty(MidiEventList events, if ((canForceStrum || canForceHopo) && (flags & Flags.Forced) != 0) { MoonNoteType type; - if (canForceHopo && lastStartDelta >= HopoThreshold) + if (canForceHopo && lastStartDelta >= HOPO_THRESHOLD) { type = MoonNoteType.Hopo; // Apply additional flag to match the parsed data diff --git a/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.Lyrics.cs b/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.Lyrics.cs index d607d58b1..1694546c2 100644 --- a/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.Lyrics.cs +++ b/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.Lyrics.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using MoonscraperChartEditor.Song; @@ -69,7 +69,7 @@ public void AddPhraseEvent(string text, uint tick) public LyricsTrack LoadLyrics() { var converter = new LyricConverter(_moonSong); - var maxTick = _moonSong.Charts.Max(x => x.events.LastOrDefault()?.tick + (uint)_moonSong.resolution ?? 0); + var maxTick = _moonSong.Charts.Max(x => x.events.LastOrDefault()?.tick + _moonSong.resolution ?? 0); TextEvents.ConvertToPhrases(_moonSong.events, converter, maxTick); return new LyricsTrack(converter.Phrases); } diff --git a/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.cs b/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.cs index ca0b5ef28..39ee25d77 100644 --- a/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.cs +++ b/YARG.Core/Chart/Loaders/MoonSong/MoonSongLoader.cs @@ -30,11 +30,8 @@ private delegate TNote CreateNoteDelegate(MoonNote moonNote, CurrentPhras private MoonSong.MoonInstrument _currentMoonInstrument; private MoonSong.Difficulty _currentMoonDifficulty; - public MoonSongLoader(MoonSong song, ParseSettings settings) + public MoonSongLoader(MoonSong song, in ParseSettings settings) { - if (settings.NoteSnapThreshold < 0) - settings.NoteSnapThreshold = 0; - _moonSong = song; _settings = settings; } @@ -333,7 +330,7 @@ private static bool IsEventInPhrase(MoonObject songObj, MoonPhrase phrase) if (phrase.length == 0) return songObj.tick == phrase.tick; - return songObj.tick >= phrase.tick && songObj.tick < (phrase.tick + phrase.length); + return phrase.tick <= songObj.tick && songObj.tick < (phrase.tick + phrase.length); } private static bool IsNoteClosestToEndOfPhrase(MoonSong song, MoonNote note, MoonPhrase phrase) @@ -366,7 +363,7 @@ private static bool IsNoteClosestToEndOfPhrase(MoonSong song, MoonNote note, Moo if (previousNote is null) { // This is the first note in the chart, check by distance - float tickThreshold = song.resolution / 3; // 1/12th note + uint tickThreshold = song.resolution / 3; // 1/12th note return Math.Abs((int) note.tick - endTick) < tickThreshold; } else if (note.tick >= endTick && previousNote.tick < endTick) diff --git a/YARG.Core/Chart/ParsingProperties.cs b/YARG.Core/Chart/ParsingProperties.cs index 85a6da9ae..362d56b6d 100644 --- a/YARG.Core/Chart/ParsingProperties.cs +++ b/YARG.Core/Chart/ParsingProperties.cs @@ -25,16 +25,31 @@ public struct ParseSettings public static readonly ParseSettings Default = new() { DrumsType = DrumsType.Unknown, + HopoThreshold = SETTING_DEFAULT, + SustainCutoffThreshold = SETTING_DEFAULT, + ChordHopoCancellation = false, + StarPowerNote = SETTING_DEFAULT, + NoteSnapThreshold = 0, + }; + public static readonly ParseSettings Default_Chart = new() + { + DrumsType = DrumsType.Unknown, HopoThreshold = SETTING_DEFAULT, - HopoFreq_FoF = SETTING_DEFAULT, - EighthNoteHopo = false, + SustainCutoffThreshold = 0, ChordHopoCancellation = false, + StarPowerNote = SETTING_DEFAULT, + NoteSnapThreshold = 0, + }; + public static readonly ParseSettings Default_Midi = new() + { + DrumsType = DrumsType.Unknown, + HopoThreshold = SETTING_DEFAULT, SustainCutoffThreshold = SETTING_DEFAULT, + ChordHopoCancellation = false, + StarPowerNote = 116, NoteSnapThreshold = 0, - - StarPowerNote = SETTING_DEFAULT, }; /// @@ -57,23 +72,6 @@ public struct ParseSettings /// public long HopoThreshold; - /// - /// The FoF HOPO threshold setting number to use. - /// - /// - /// Uses the hopofreq tag from song.ini files.
- /// 0 -> 1/24th note, 1 -> 1/16th note, 2 -> 1/12th note, 3 -> 1/8th note, 4 -> 1/6th note, 5 -> 1/4th note. - ///
- public int HopoFreq_FoF; - - /// - /// Set the HOPO threshold to a 1/8th note instead of a 1/12th note. - /// - /// - /// Uses the eighthnote_hopo tag from song.ini files. - /// - public bool EighthNoteHopo; - /// /// Skip marking single notes after chords as HOPOs /// if the single note shares a fret with the chord. @@ -105,43 +103,5 @@ public struct ParseSettings /// Defaults to 116. /// public int StarPowerNote; - - /// - /// Calculates the HOPO threshold to use from the various HOPO settings. - /// - public readonly float GetHopoThreshold(float resolution) - { - // Prefer in this order: - // 1. hopo_threshold - // 2. eighthnote_hopo - // 3. hopofreq - - if (HopoThreshold > 0) - { - return HopoThreshold; - } - else if (EighthNoteHopo) - { - return resolution / 2; - } - else if (HopoFreq_FoF >= 0) - { - int denominator = HopoFreq_FoF switch - { - 0 => 24, - 1 => 16, - 2 => 12, - 3 => 8, - 4 => 6, - 5 => 4, - _ => throw new NotImplementedException($"Unhandled hopofreq value {HopoFreq_FoF}!") - }; - return (resolution * 4) / denominator; - } - else - { - return resolution / 3; - } - } } } \ No newline at end of file diff --git a/YARG.Core/Extensions/StreamExtensions.cs b/YARG.Core/Extensions/StreamExtensions.cs index 4adcc594f..f734f5c51 100644 --- a/YARG.Core/Extensions/StreamExtensions.cs +++ b/YARG.Core/Extensions/StreamExtensions.cs @@ -33,7 +33,10 @@ public static TType Read(this Stream stream, Endianness endianness) public static bool ReadBoolean(this Stream stream) { byte b = (byte)stream.ReadByte(); - return Unsafe.As(ref b); + unsafe + { + return *(bool*)&b; + } } public static int Read7BitEncodedInt(this Stream stream) @@ -124,6 +127,59 @@ public static void Write(this Stream stream, TType value, Endianness endi } } + public static void Write(this Stream stream, bool value) + { + unsafe + { + stream.WriteByte(*(byte*) &value); + } + } + + public static void Write7BitEncodedInt(this Stream stream, int value) + { + // Write out an int 7 bits at a time. The high bit of the byte, + // when on, tells reader to continue reading more bytes. + uint v = (uint) value; // support negative numbers + while (v >= 0x80) + { + stream.WriteByte((byte) (v | 0x80)); + v >>= 7; + } + stream.WriteByte((byte) v); + } + + public static unsafe void Write(this Stream stream, string value) + { + if (value.Length == 0) + { + stream.WriteByte(0); + return; + } + + var buffer = stackalloc byte[value.Length * 4]; + fixed (char* chars = value) + { + int len = Encoding.UTF8.GetBytes(chars, value.Length, buffer, value.Length * 4); + stream.Write7BitEncodedInt(len); + stream.Write(new ReadOnlySpan(buffer, len)); + } + } + + public static void Write(this Stream stream, Color color) + { + stream.Write(color.ToArgb(), Endianness.Little); + } + + public static void Write(this Stream stream, Guid guid) + { + Span span = stackalloc byte[16]; + if (!guid.TryWriteBytes(span)) + { + throw new InvalidOperationException("Failed to write GUID bytes."); + } + stream.Write(span); + } + private static unsafe void CorrectByteOrder(byte* bytes, Endianness endianness) where TType : unmanaged, IComparable, IComparable, IConvertible, IEquatable, IFormattable { diff --git a/YARG.Core/IO/AbridgedFileInfo.cs b/YARG.Core/IO/AbridgedFileInfo.cs index 275bc72dd..7833fd527 100644 --- a/YARG.Core/IO/AbridgedFileInfo.cs +++ b/YARG.Core/IO/AbridgedFileInfo.cs @@ -55,10 +55,10 @@ public AbridgedFileInfo(string filename, in DateTime lastUpdatedTime) LastUpdatedTime = lastUpdatedTime; } - public void Serialize(BinaryWriter writer) + public void Serialize(MemoryStream stream) { - writer.Write(FullName); - writer.Write(LastUpdatedTime.ToBinary()); + stream.Write(FullName); + stream.Write(LastUpdatedTime.ToBinary(), Endianness.Little); } public bool Exists() diff --git a/YARG.Core/IO/ConHandler/CONFile.cs b/YARG.Core/IO/ConHandler/CONFile.cs index 01cf1fb3a..d51b09660 100644 --- a/YARG.Core/IO/ConHandler/CONFile.cs +++ b/YARG.Core/IO/ConHandler/CONFile.cs @@ -6,7 +6,7 @@ namespace YARG.Core.IO { - public readonly struct CONFile + public static class CONFile { private static readonly FourCC CON_TAG = new('C', 'O', 'N', ' '); private static readonly FourCC LIVE_TAG = new('L', 'I', 'V', 'E'); @@ -22,41 +22,32 @@ public readonly struct CONFile private const int BYTES_PER_BLOCK = 0x1000; private const int SIZEOF_FILELISTING = 0x40; - private readonly List _filenames; - private readonly Dictionary _listings; - - private CONFile(List filenames, Dictionary listings) - { - _filenames = filenames; - _listings = listings; - } - - public string GetFilename(int index) - { - return _filenames[index]; - } - - public bool TryGetListing(string name, out CONFileListing listing) + public static bool TryGetListing(this List listings, string name, out CONFileListing listing) { - return _listings.TryGetValue(name, out listing); + foreach (var file in listings) + { + if (file.Filename == name) + { + listing = file; + return true; + } + } + listing = null!; + return false; } - public static CONFile? TryParseListings(AbridgedFileInfo info) + public static List? TryParseListings(in AbridgedFileInfo info, FileStream filestream) { - using var stream = InitStream_Internal(info.FullName); - if (stream == null) - return null; - Span int32Buffer = stackalloc byte[BYTES_32BIT]; - if (stream.Read(int32Buffer) != BYTES_32BIT) + if (filestream.Read(int32Buffer) != BYTES_32BIT) return null; var tag = new FourCC(int32Buffer); if (tag != CON_TAG && tag != LIVE_TAG && tag != PIRS_TAG) return null; - stream.Seek(METADATA_POSITION, SeekOrigin.Begin); - if (stream.Read(int32Buffer) != BYTES_32BIT) + filestream.Seek(METADATA_POSITION, SeekOrigin.Begin); + if (filestream.Read(int32Buffer) != BYTES_32BIT) return null; byte shift = 0; @@ -66,45 +57,40 @@ public bool TryGetListing(string name, out CONFileListing listing) if ((entryID + 0xFFF & 0xF000) >> 0xC != 0xB) shift = 1; - stream.Seek(FILETABLEBLOCKCOUNT_POSITION, SeekOrigin.Begin); - if (stream.Read(int32Buffer[..BYTES_16BIT]) != BYTES_16BIT) + filestream.Seek(FILETABLEBLOCKCOUNT_POSITION, SeekOrigin.Begin); + if (filestream.Read(int32Buffer[..BYTES_16BIT]) != BYTES_16BIT) return null; int length = BYTES_PER_BLOCK * (int32Buffer[0] | int32Buffer[1] << 8); - stream.Seek(FILETABLEFIRSTBLOCK_POSITION, SeekOrigin.Begin); - if (stream.Read(int32Buffer[..BYTES_24BIT]) != BYTES_24BIT) + filestream.Seek(FILETABLEFIRSTBLOCK_POSITION, SeekOrigin.Begin); + if (filestream.Read(int32Buffer[..BYTES_24BIT]) != BYTES_24BIT) return null; int firstBlock = int32Buffer[0] << 16 | int32Buffer[1] << 8 | int32Buffer[2]; - try { - var filenames = new List(); - var listings = new Dictionary(); + var listings = new List(); - using var conStream = new CONFileStream(stream, true, length, firstBlock, shift); - Span listingBuffer = stackalloc byte[SIZEOF_FILELISTING]; - while (conStream.Read(listingBuffer) == SIZEOF_FILELISTING && listingBuffer[0] != 0) + using var listingBuffer = CONFileStream.LoadFile(filestream, true, length, firstBlock, shift); + unsafe { - short pathIndex = (short) (listingBuffer[0x32] << 8 | listingBuffer[0x33]); - if (pathIndex >= filenames.Count) + var endPtr = listingBuffer.Ptr + length; + for (var currPtr = listingBuffer.Ptr; currPtr + SIZEOF_FILELISTING <= endPtr && currPtr[0] != 0; currPtr += SIZEOF_FILELISTING) { - YargLogger.LogFormatError("Error while parsing {0} - Filelisting blocks constructed out of spec", info.FullName); - return null; - } - - string filename = pathIndex >= 0 ? filenames[pathIndex] + "/" : string.Empty; - filename += Encoding.UTF8.GetString(listingBuffer[..0x28]).TrimEnd('\0'); - filenames.Add(filename); - - var flags = (CONFileListingFlag) listingBuffer[0x28]; - if ((flags & CONFileListingFlag.Directory) == 0) - { - listings[filename] = new CONFileListing(info, filename, pathIndex, flags, shift, listingBuffer); + short pathIndex = (short) (currPtr[0x32] << 8 | currPtr[0x33]); + if (pathIndex >= listings.Count) + { + YargLogger.LogFormatError("Error while parsing {0} - Filelisting blocks constructed out of spec", info.FullName); + return null; + } + + string filename = pathIndex >= 0 ? listings[pathIndex].Filename + "/" : string.Empty; + filename += Encoding.UTF8.GetString(currPtr, 0x28).TrimEnd('\0'); + listings.Add(new CONFileListing(info, filename, pathIndex, shift, currPtr)); } } - return new CONFile(filenames, listings); + return listings; } catch (Exception ex) { @@ -112,18 +98,5 @@ public bool TryGetListing(string name, out CONFileListing listing) return null; } } - - private static FileStream? InitStream_Internal(string filename) - { - try - { - return new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read); - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error loading {filename}"); - return null; - } - } } } diff --git a/YARG.Core/IO/ConHandler/CONFileListing.cs b/YARG.Core/IO/ConHandler/CONFileListing.cs index 27dc65d86..9d790a2ee 100644 --- a/YARG.Core/IO/ConHandler/CONFileListing.cs +++ b/YARG.Core/IO/ConHandler/CONFileListing.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; using System.IO; -using System.Text; using YARG.Core.Extensions; namespace YARG.Core.IO @@ -15,8 +13,6 @@ public enum CONFileListingFlag : byte public sealed class CONFileListing { - - private readonly int _shift; public readonly AbridgedFileInfo ConFile; @@ -28,7 +24,7 @@ public sealed class CONFileListing public readonly int Size; public readonly DateTime LastWrite; - public CONFileListing(AbridgedFileInfo conFile, string name, short pathIndex, CONFileListingFlag flags, int shift, ReadOnlySpan data) + public unsafe CONFileListing(AbridgedFileInfo conFile, string name, short pathIndex, int shift, byte* data) { _shift = shift; @@ -36,7 +32,7 @@ public CONFileListing(AbridgedFileInfo conFile, string name, short pathIndex, CO Filename = name; PathIndex = pathIndex; - Flags = flags; + Flags = (CONFileListingFlag) data[0x28]; NumBlocks = data[0x2B] << 16 | data[0x2A] << 8 | data[0x29]; FirstBlock = data[0x31] << 16 | data[0x30] << 8 | data[0x2F]; Size = data[0x34] << 24 | data[0x35] << 16 | data[0x36] << 8 | data[0x37]; diff --git a/YARG.Core/IO/DTA/DTAEntry.cs b/YARG.Core/IO/DTA/DTAEntry.cs new file mode 100644 index 000000000..a0c856950 --- /dev/null +++ b/YARG.Core/IO/DTA/DTAEntry.cs @@ -0,0 +1,310 @@ +using System.Text; +using YARG.Core.Song; + +namespace YARG.Core.IO +{ + public class DTAEntry + { + public string? Name; + public string? Artist; + public string? Album; + public string? Genre; + public string? Charter; + public string? Source; + public string? Playlist; + public int? YearAsNumber; + + public ulong? SongLength; + public uint? SongRating; // 1 = FF; 2 = SR; 3 = M; 4 = NR + + public long? PreviewStart; + public long? PreviewEnd; + public bool? IsMaster; + + public int? AlbumTrack; + + public string? SongID; + public uint? AnimTempo; + public string? DrumBank; + public string? VocalPercussionBank; + public uint? VocalSongScrollSpeed; + public bool? VocalGender; //true for male, false for female + //public bool HasAlbumArt; + //public bool IsFake; + public uint? VocalTonicNote; + public bool? SongTonality; // 0 = major, 1 = minor + public int? TuningOffsetCents; + public uint? VenueVersion; + + public string[]? Soloes; + public string[]? VideoVenues; + + public int[]? RealGuitarTuning; + public int[]? RealBassTuning; + + public RBAudio? Indices; + public int[]? CrowdChannels; + + public string? Location; + public float[]? Pans; + public float[]? Volumes; + public float[]? Cores; + public long? HopoThreshold; + public Encoding Encoding; + + public RBCONDifficulties Difficulties = RBCONDifficulties.Default; + + public DTAEntry(Encoding encoding) + { + Encoding = encoding; + } + + public DTAEntry(string nodename, in YARGTextContainer container) + { + Encoding = container.Encoding; + LoadData(nodename, container); + } + + public void LoadData(string nodename, YARGTextContainer container) + { + container.Encoding = Encoding; + while (YARGDTAReader.StartNode(ref container)) + { + string name = YARGDTAReader.GetNameOfNode(ref container, false); + switch (name) + { + case "name": Name = YARGDTAReader.ExtractText(ref container); break; + case "artist": Artist = YARGDTAReader.ExtractText(ref container); break; + case "master": IsMaster = YARGDTAReader.ExtractBoolean_FlippedDefault(ref container); break; + case "context": /*Context = YARGDTAReader.ExtractUInt32(ref container);*/ break; + case "song": + while (YARGDTAReader.StartNode(ref container)) + { + string descriptor = YARGDTAReader.GetNameOfNode(ref container, false); + switch (descriptor) + { + case "name": Location = YARGDTAReader.ExtractText(ref container); break; + case "tracks": + { + var indices = RBAudio.Empty; + while (YARGDTAReader.StartNode(ref container)) + { + while (YARGDTAReader.StartNode(ref container)) + { + switch (YARGDTAReader.GetNameOfNode(ref container, false)) + { + case "drum": indices.Drums = YARGDTAReader.ExtractArray_Int(ref container); break; + case "bass": indices.Bass = YARGDTAReader.ExtractArray_Int(ref container); break; + case "guitar": indices.Guitar = YARGDTAReader.ExtractArray_Int(ref container); break; + case "keys": indices.Keys = YARGDTAReader.ExtractArray_Int(ref container); break; + case "vocals": indices.Vocals = YARGDTAReader.ExtractArray_Int(ref container); break; + } + YARGDTAReader.EndNode(ref container); + } + YARGDTAReader.EndNode(ref container); + } + Indices = indices; + break; + } + case "crowd_channels": CrowdChannels = YARGDTAReader.ExtractArray_Int(ref container); break; + //case "vocal_parts": VocalParts = YARGDTAReader.ExtractUInt16(ref container); break; + case "pans": Pans = YARGDTAReader.ExtractArray_Float(ref container); break; + case "vols": Volumes = YARGDTAReader.ExtractArray_Float(ref container); break; + case "cores": Cores = YARGDTAReader.ExtractArray_Float(ref container); break; + case "hopo_threshold": HopoThreshold = YARGDTAReader.ExtractInt64(ref container); break; + } + YARGDTAReader.EndNode(ref container); + } + break; + case "song_vocals": while (YARGDTAReader.StartNode(ref container)) YARGDTAReader.EndNode(ref container); break; + case "song_scroll_speed": VocalSongScrollSpeed = YARGDTAReader.ExtractUInt32(ref container); break; + case "tuning_offset_cents": TuningOffsetCents = YARGDTAReader.ExtractInt32(ref container); break; + case "bank": VocalPercussionBank = YARGDTAReader.ExtractText(ref container); break; + case "anim_tempo": + { + string val = YARGDTAReader.ExtractText(ref container); + AnimTempo = val switch + { + "kTempoSlow" => 16, + "kTempoMedium" => 32, + "kTempoFast" => 64, + _ => uint.Parse(val) + }; + break; + } + case "preview": + PreviewStart = YARGDTAReader.ExtractInt64(ref container); + PreviewEnd = YARGDTAReader.ExtractInt64(ref container); + break; + case "rank": + while (YARGDTAReader.StartNode(ref container)) + { + string descriptor = YARGDTAReader.GetNameOfNode(ref container, false); + int diff = YARGDTAReader.ExtractInt32(ref container); + switch (descriptor) + { + case "drum": + case "drums": Difficulties.FourLaneDrums = (short) diff; break; + + case "guitar": Difficulties.FiveFretGuitar = (short) diff; break; + case "bass": Difficulties.FiveFretBass = (short) diff; break; + case "vocals": Difficulties.LeadVocals = (short) diff; break; + case "keys": Difficulties.Keys = (short) diff; break; + + case "realGuitar": + case "real_guitar": Difficulties.ProGuitar = (short) diff; break; + + case "realBass": + case "real_bass": Difficulties.ProBass = (short) diff; break; + + case "realKeys": + case "real_keys": Difficulties.ProKeys = (short) diff; break; + + case "realDrums": + case "real_drums": Difficulties.ProDrums = (short) diff; break; + + case "harmVocals": + case "vocal_harm": Difficulties.HarmonyVocals = (short) diff; break; + + case "band": Difficulties.Band = (short) diff; break; + } + YARGDTAReader.EndNode(ref container); + } + break; + case "solo": Soloes = YARGDTAReader.ExtractArray_String(ref container); break; + case "genre": Genre = YARGDTAReader.ExtractText(ref container); break; + case "decade": /*Decade = YARGDTAReader.ExtractText(ref container);*/ break; + case "vocal_gender": VocalGender = YARGDTAReader.ExtractText(ref container) == "male"; break; + case "format": /*Format = YARGDTAReader.ExtractUInt32(ref container);*/ break; + case "version": VenueVersion = YARGDTAReader.ExtractUInt32(ref container); break; + case "fake": /*IsFake = YARGDTAReader.ExtractText(ref container);*/ break; + case "downloaded": /*Downloaded = YARGDTAReader.ExtractText(ref container);*/ break; + case "game_origin": + { + string str = YARGDTAReader.ExtractText(ref container); + if ((str == "ugc" || str == "ugc_plus")) + { + if (!nodename.StartsWith("UGC_")) + Source = "customs"; + } + else if (str == "#ifdef") + { + string conditional = YARGDTAReader.ExtractText(ref container); + if (conditional == "CUSTOMSOURCE") + { + Source = YARGDTAReader.ExtractText(ref container); + } + else + { + Source = "customs"; + } + } + else + { + Source = str; + } + + //// if the source is any official RB game or its DLC, charter = Harmonix + //if (SongSources.GetSource(str).Type == SongSources.SourceType.RB) + //{ + // _charter = "Harmonix"; + //} + + //// if the source is meant for usage in TBRB, it's a master track + //// TODO: NEVER assume localized version contains "Beatles" + //if (SongSources.SourceToGameName(str).Contains("Beatles")) _isMaster = true; + break; + } + case "song_id": SongID = YARGDTAReader.ExtractText(ref container); break; + case "rating": SongRating = YARGDTAReader.ExtractUInt32(ref container); break; + case "short_version": /*ShortVersion = YARGDTAReader.ExtractUInt32(ref container);*/ break; + case "album_art": /*HasAlbumArt = YARGDTAReader.ExtractBoolean(ref container);*/ break; + case "year_released": + case "year_recorded": YearAsNumber = YARGDTAReader.ExtractInt32(ref container); break; + case "album_name": Album = YARGDTAReader.ExtractText(ref container); break; + case "album_track_number": AlbumTrack = YARGDTAReader.ExtractInt32(ref container); break; + case "pack_name": Playlist = YARGDTAReader.ExtractText(ref container); break; + case "base_points": /*BasePoints = YARGDTAReader.ExtractUInt32(ref container);*/ break; + case "band_fail_cue": /*BandFailCue = YARGDTAReader.ExtractText(ref container);*/ break; + case "drum_bank": DrumBank = YARGDTAReader.ExtractText(ref container); break; + case "song_length": SongLength = YARGDTAReader.ExtractUInt64(ref container); break; + case "sub_genre": /*Subgenre = YARGDTAReader.ExtractText(ref container);*/ break; + case "author": Charter = YARGDTAReader.ExtractText(ref container); break; + case "guide_pitch_volume": /*GuidePitchVolume = YARGDTAReader.ExtractFloat(ref container);*/ break; + case "encoding": + Encoding = YARGDTAReader.ExtractText(ref container).ToLower() switch + { + "latin1" => YARGTextReader.Latin1, + "utf-8" or + "utf8" => Encoding.UTF8, + _ => container.Encoding + }; + + var currEncoding = container.Encoding; + if (currEncoding != Encoding) + { + string Convert(string str) + { + byte[] bytes = currEncoding.GetBytes(str); + return Encoding.GetString(bytes); + } + + if (Name != null) + Name = Convert(Name); + if (Artist != null) + Artist = Convert(Artist); + if (Album != null) + Album = Convert(Album); + if (Genre != null) + Genre = Convert(Genre); + if (Charter != null) + Charter = Convert(Charter); + if (Source != null) + Source = Convert(Source); + + if (Playlist != null) + Playlist = Convert(Playlist); + container.Encoding = Encoding; + } + + break; + case "vocal_tonic_note": VocalTonicNote = YARGDTAReader.ExtractUInt32(ref container); break; + case "song_tonality": SongTonality = YARGDTAReader.ExtractBoolean(ref container); break; + case "alternate_path": /*AlternatePath = YARGDTAReader.ExtractBoolean(ref container);*/ break; + case "real_guitar_tuning": RealGuitarTuning = YARGDTAReader.ExtractArray_Int(ref container); break; + case "real_bass_tuning": RealBassTuning = YARGDTAReader.ExtractArray_Int(ref container); break; + case "video_venues": VideoVenues = YARGDTAReader.ExtractArray_String(ref container); break; + case "extra_authoring": + { + StringBuilder authors = new(); + foreach (string str in YARGDTAReader.ExtractArray_String(ref container)) + { + if (str != "disc_update") + { + if (authors.Length == 0 && Charter == SongMetadata.DEFAULT_CHARTER) + { + authors.Append(str); + } + else + { + if (authors.Length == 0) + authors.Append(Charter); + authors.Append(", " + str); + } + } + } + + if (authors.Length == 0) + { + authors.Append(Charter); + } + + Charter = authors.ToString(); + } + break; + } + YARGDTAReader.EndNode(ref container); + } + } + } +} diff --git a/YARG.Core/IO/YARGDTAReader.cs b/YARG.Core/IO/DTA/YARGDTAReader.cs similarity index 97% rename from YARG.Core/IO/YARGDTAReader.cs rename to YARG.Core/IO/DTA/YARGDTAReader.cs index de89755f7..5326eba14 100644 --- a/YARG.Core/IO/YARGDTAReader.cs +++ b/YARG.Core/IO/DTA/YARGDTAReader.cs @@ -1,30 +1,28 @@ using System; using System.Collections.Generic; -using System.IO; using System.Text; using YARG.Core.Extensions; using YARG.Core.Logging; namespace YARG.Core.IO { - public unsafe static class YARGDTAReader + public static unsafe class YARGDTAReader { - public static bool TryCreate(in FixedArray data, out YARGTextContainer container) + public static YARGTextContainer TryCreate(in FixedArray data) { if ((data[0] == 0xFF && data[1] == 0xFE) || (data[0] == 0xFE && data[1] == 0xFF)) { YargLogger.LogError("UTF-16 & UTF-32 are not supported for .dta files"); - container = default; - return false; + return YARGTextContainer.Null; } - container = new YARGTextContainer(in data, YARGTextReader.Latin1); + var container = new YARGTextContainer(in data, YARGTextReader.Latin1); if (data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) { container.Position += 3; container.Encoding = Encoding.UTF8; } - return true; + return container; } public static char SkipWhitespace(ref YARGTextContainer container) diff --git a/YARG.Core/IO/FixedArray.cs b/YARG.Core/IO/FixedArray.cs index 3a9986e24..89c5dd904 100644 --- a/YARG.Core/IO/FixedArray.cs +++ b/YARG.Core/IO/FixedArray.cs @@ -27,10 +27,7 @@ public unsafe struct FixedArray : IDisposable /// /// A indisposable default instance with a null pointer /// - public static readonly FixedArray Null = new() - { - _disposed = true - }; + public static readonly FixedArray Null = new(null, 0); /// /// Loads all of the given file's data into a FixedArray buffer @@ -144,7 +141,7 @@ private FixedArray(T* ptr, long length) { Ptr = ptr; Length = length; - _disposed = false; + _disposed = ptr == null; } public readonly Span Slice(long offset, long count) diff --git a/YARG.Core/IO/Ini/SongIniHandler.cs b/YARG.Core/IO/Ini/SongIniHandler.cs index 87b94d8c3..5b1bd530c 100644 --- a/YARG.Core/IO/Ini/SongIniHandler.cs +++ b/YARG.Core/IO/Ini/SongIniHandler.cs @@ -21,19 +21,20 @@ public static IniSection ReadSongIniFile(string iniPath) public static readonly Dictionary SONG_INI_MODIFIERS; #if DEBUG - private static readonly Dictionary _validations; + private static readonly Dictionary _types; + private static readonly Dictionary _outputs; public static void ThrowIfNot(string key) { - if (!SONG_INI_MODIFIERS.TryGetValue(key, out var mod)) + if (!_outputs.TryGetValue(key, out var modifierType)) { throw new ArgumentException($"Dev: {key} is not a valid modifier!"); } var typename = typeof(T).Name; - var type = _validations[typename]; - if (type != mod.Type - && (type == ModifierType.SortString) != (mod.Type == ModifierType.SortString_Chart) - && (type == ModifierType.String) != (mod.Type == ModifierType.String_Chart)) + var type = _types[typename]; + if (type != modifierType + && (type == ModifierType.SortString) != (modifierType == ModifierType.SortString_Chart) + && (type == ModifierType.String) != (modifierType == ModifierType.String_Chart)) { throw new ArgumentException($"Dev: Modifier {key} is not of type {typename}"); } @@ -45,10 +46,8 @@ static SongIniHandler() SONG_INI_MODIFIERS = new() { { "album", new("album", ModifierType.SortString) }, - { "Album", new("album", ModifierType.SortString_Chart) }, { "album_track", new("album_track", ModifierType.Int32) }, { "artist", new("artist", ModifierType.SortString) }, - { "Artist", new("artist", ModifierType.SortString_Chart) }, { "background", new("background", ModifierType.String) }, //{ "banner_link_a", new("banner_link_a", ModifierType.String) }, @@ -58,13 +57,16 @@ static SongIniHandler() //{ "cassettecolor", new("cassettecolor", ModifierType.UInt32) }, { "charter", new("charter", ModifierType.SortString) }, - { "Charter", new("charter", ModifierType.SortString_Chart) }, { "count", new("count", ModifierType.UInt32) }, { "cover", new("cover", ModifierType.String) }, + { "credit_written_by", new("credit_written_by", ModifierType.String) }, + { "credit_performed_by", new("credit_performed_by", ModifierType.String) }, + { "credit_courtesy_of", new("credit_courtesy_of", ModifierType.String) }, + { "credit_album_cover", new("credit_album_cover", ModifierType.String) }, + { "credit_license", new("credit_license", ModifierType.String) }, { "dance_type", new("dance_type", ModifierType.UInt32) }, { "delay", new("delay", ModifierType.Int64) }, - { "Difficulty", new("diff_band", ModifierType.Int32) }, { "diff_band", new("diff_band", ModifierType.Int32) }, { "diff_bass", new("diff_bass", ModifierType.Int32) }, { "diff_bass_real", new("diff_bass_real", ModifierType.Int32) }, @@ -99,7 +101,6 @@ static SongIniHandler() { "frets", new("frets", ModifierType.SortString) }, { "genre", new("genre", ModifierType.SortString) }, - { "Genre", new("genre", ModifierType.SortString_Chart) }, { "guitar_type", new("guitar_type", ModifierType.UInt32) }, { "hopo_frequency", new("hopo_frequency", ModifierType.Int64) }, @@ -115,27 +116,16 @@ static SongIniHandler() { "loading_phrase", new("loading_phrase", ModifierType.String) }, { "lyrics", new("lyrics", ModifierType.Bool) }, - { "credit_written_by", new("credit_written_by", ModifierType.String) }, - { "credit_performed_by", new("credit_performed_by", ModifierType.String) }, - { "credit_courtesy_of", new("credit_courtesy_of", ModifierType.String) }, - { "credit_album_cover", new("credit_album_cover", ModifierType.String) }, - { "credit_license", new("credit_license", ModifierType.String) }, - { "modchart", new("modchart", ModifierType.Bool) }, { "multiplier_note", new("multiplier_note", ModifierType.Int32) }, { "name", new("name", ModifierType.SortString) }, - { "Name", new("name", ModifierType.SortString_Chart) }, - - { "Offset", new("Offset", ModifierType.Double) }, { "playlist", new("playlist", ModifierType.SortString) }, { "playlist_track", new("playlist_track", ModifierType.Int32) }, { "preview", new("preview", ModifierType.Int64Array) }, { "preview_end_time", new("preview_end_time", ModifierType.Int64) }, - { "PreviewEnd", new("PreviewEnd", ModifierType.Double) }, { "preview_start_time", new("preview_start_time", ModifierType.Int64) }, - { "PreviewStart", new("PreviewStart", ModifierType.Double) }, { "pro_drum", new("pro_drums", ModifierType.Bool) }, { "pro_drums", new("pro_drums", ModifierType.Bool) }, @@ -178,7 +168,6 @@ static SongIniHandler() { "vocal_gender", new("vocal_gender", ModifierType.UInt32) }, { "year", new("year", ModifierType.String) }, - { "Year", new("Year", ModifierType.String) }, }; SONG_INI_DICTIONARY = new() @@ -187,7 +176,7 @@ static SongIniHandler() }; #if DEBUG - _validations = new() + _types = new() { { nameof(SortString), ModifierType.SortString }, { typeof(string).Name, ModifierType.String }, @@ -202,6 +191,18 @@ static SongIniHandler() { typeof(double).Name, ModifierType.Double }, { typeof(long[]).Name, ModifierType.Int64Array }, }; + + _outputs = new Dictionary(); + _outputs.EnsureCapacity(SONG_INI_MODIFIERS.Count + YARGChartFileReader.CHART_MODIFIERS.Count); + foreach (var node in SONG_INI_MODIFIERS.Values) + { + _outputs.TryAdd(node.OutputName, node.Type); + } + + foreach (var node in YARGChartFileReader.CHART_MODIFIERS.Values) + { + _outputs.TryAdd(node.OutputName, node.Type); + } #endif } } diff --git a/YARG.Core/IO/Midi/YARGMidiFile.cs b/YARG.Core/IO/Midi/YARGMidiFile.cs index 2af64ab1e..1f7d83c57 100644 --- a/YARG.Core/IO/Midi/YARGMidiFile.cs +++ b/YARG.Core/IO/Midi/YARGMidiFile.cs @@ -13,9 +13,9 @@ public sealed class YARGMidiFile : IEnumerable private static readonly FourCC TRACK_TAG = new('M', 'T', 'r', 'k'); private readonly Stream _stream; - private readonly ushort _format; - private readonly ushort _numTracks; - private readonly ushort _tickRate; + public readonly ushort Format; + public readonly ushort NumTracks; + public readonly ushort Resolution; private ushort _trackNumber = 0; public ushort TrackNumber => _trackNumber; @@ -32,20 +32,22 @@ public YARGMidiFile(Stream stream) throw new Exception("Midi Header not of sufficient length"); long next = stream.Position + length; - _format = stream.Read(Endianness.Big); - _numTracks = stream.Read(Endianness.Big); - _tickRate = stream.Read(Endianness.Big); + Format = stream.Read(Endianness.Big); + NumTracks = stream.Read(Endianness.Big); + Resolution = stream.Read(Endianness.Big); stream.Position = next; } public YARGMidiTrack? LoadNextTrack() { - if (_trackNumber == _numTracks || _stream.Position == _stream.Length) + if (_trackNumber == NumTracks || _stream.Position == _stream.Length) return null; _trackNumber++; if (!TRACK_TAG.Matches(_stream)) + { throw new Exception($"Midi Track Tag 'MTrk' not found for Track '{_trackNumber}'"); + } try { diff --git a/YARG.Core/IO/SngHandler/SngFile.cs b/YARG.Core/IO/SngHandler/SngFile.cs index 318f25f04..20d9ed535 100644 --- a/YARG.Core/IO/SngHandler/SngFile.cs +++ b/YARG.Core/IO/SngHandler/SngFile.cs @@ -63,11 +63,11 @@ IEnumerator IEnumerable.GetEnumerator() private const int BYTES_16BIT = 2; private static readonly byte[] SNGPKG = { (byte)'S', (byte) 'N', (byte) 'G', (byte)'P', (byte)'K', (byte)'G' }; - public static SngFile? TryLoadFromFile(AbridgedFileInfo file) + public static SngFile? TryLoadFromFile(in AbridgedFileInfo file) { try { - using var filestream = File.OpenRead(file.FullName); + using var filestream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); using var yargSongStream = YARGSongFileStream.TryLoad(filestream); if (yargSongStream != null) { diff --git a/YARG.Core/IO/TextReader/YARGTextContainer.cs b/YARG.Core/IO/TextReader/YARGTextContainer.cs index 25f5b7da2..dd69393f0 100644 --- a/YARG.Core/IO/TextReader/YARGTextContainer.cs +++ b/YARG.Core/IO/TextReader/YARGTextContainer.cs @@ -6,18 +6,17 @@ namespace YARG.Core.IO public unsafe struct YARGTextContainer where TChar : unmanaged, IConvertible { + public static readonly YARGTextContainer Null = new() + { + Encoding = null!, + Position = null, + }; + public readonly TChar* End; public Encoding Encoding; public TChar* Position; - public YARGTextContainer(in FixedArray buffer, Encoding encoding) - { - Position = buffer.Ptr; - End = buffer.Ptr + buffer.Length; - Encoding = encoding; - } - - public TChar CurrentValue + public readonly TChar CurrentValue { get { @@ -29,6 +28,15 @@ public TChar CurrentValue } } + public readonly bool IsActive => End != null; + + public YARGTextContainer(in FixedArray buffer, Encoding encoding) + { + Position = buffer.Ptr; + End = buffer.Ptr + buffer.Length; + Encoding = encoding; + } + public readonly bool IsCurrentCharacter(int cmp) { return Position->ToInt32(null).Equals(cmp); diff --git a/YARG.Core/IO/YARGChartFileReader.cs b/YARG.Core/IO/YARGChartFileReader.cs index 7c7d7959a..72518d286 100644 --- a/YARG.Core/IO/YARGChartFileReader.cs +++ b/YARG.Core/IO/YARGChartFileReader.cs @@ -217,6 +217,21 @@ public static unsafe bool TryParseEvent(ref YARGTextContainer cont return true; } + public static readonly Dictionary CHART_MODIFIERS = new() + { + { "Album", new("album", ModifierType.SortString_Chart) }, + { "Artist", new("artist", ModifierType.SortString_Chart) }, + { "Charter", new("charter", ModifierType.SortString_Chart) }, + { "Difficulty", new("diff_band", ModifierType.Int32) }, + { "Genre", new("genre", ModifierType.SortString_Chart) }, + { "Name", new("name", ModifierType.SortString_Chart) }, + { "Offset", new("delay_chart", ModifierType.Double) }, + { "PreviewEnd", new("previewEnd_chart", ModifierType.Double) }, + { "PreviewStart", new("previewStart_chart", ModifierType.Double) }, + { "Resolution", new("Resolution", ModifierType.Int64) }, + { "Year", new("year_chart", ModifierType.String_Chart) }, + }; + public unsafe static Dictionary> ExtractModifiers(ref YARGTextContainer container) where TChar : unmanaged, IEquatable, IConvertible { @@ -224,7 +239,7 @@ public unsafe static Dictionary> ExtractModifiers public static MoonSong ReadFromFile(string filepath) { - var settings = ParseSettings.Default; + var settings = ParseSettings.Default_Chart; return ReadFromFile(ref settings, filepath); } public static MoonSong ReadFromText(ReadOnlySpan chartText) { - var settings = ParseSettings.Default; + var settings = ParseSettings.Default_Chart; return ReadFromText(ref settings, chartText); } @@ -107,6 +107,7 @@ public static MoonSong ReadFromFile(ref ParseSettings settings, string filepath) } } + private const uint DEFAULT_RESOLUTION = 192; public static MoonSong ReadFromText(ref ParseSettings settings, ReadOnlySpan chartText) { int textIndex = 0; @@ -124,7 +125,15 @@ static void ExpectSection(ReadOnlySpan chartText, ref int textIndex, // Check for the [Song] section first explicitly, need the Resolution property up-front ExpectSection(chartText, ref textIndex, ChartIOHelper.SECTION_SONG, out var sectionBody); var song = SubmitDataSong(sectionBody); - ValidateAndApplySettings(song, ref settings); + + // With a 192 resolution, .chart has a HOPO threshold of 65 ticks, not 64, + // so we need to scale this factor to different resolutions (480 res = 162.5 threshold) + // This extra tick is meant for some slight leniency; .mid has it too, but it's applied + // after factoring in the resolution there, not before. + const uint THRESHOLD_AT_DEFAULT = 65; + song.hopoThreshold = settings.HopoThreshold > ParseSettings.SETTING_DEFAULT + ? (uint) settings.HopoThreshold + : (song.resolution * THRESHOLD_AT_DEFAULT) / DEFAULT_RESOLUTION; // Check for [SyncTrack] next, we need it for time conversions ExpectSection(chartText, ref textIndex, ChartIOHelper.SECTION_SYNC_TRACK, out sectionBody); @@ -255,6 +264,7 @@ private static void SubmitChartData(ref ParseSettings settings, MoonSong song, R private static MoonSong SubmitDataSong(AsciiTrimSplitter sectionLines) { + uint resolution = DEFAULT_RESOLUTION; foreach (var line in sectionLines) { var key = line.SplitOnceTrimmed('=', out var value); @@ -262,25 +272,11 @@ private static MoonSong SubmitDataSong(AsciiTrimSplitter sectionLines) if (key.Equals("Resolution", StringComparison.Ordinal)) { - uint resolution = (uint)FastInt32Parse(value); - return new MoonSong(resolution); + resolution = (uint)FastInt32Parse(value); + break; } } - - throw new InvalidDataException("No resolution was found in the chart data!"); - } - - private static void ValidateAndApplySettings(MoonSong song, ref ParseSettings settings) - { - // Apply HOPO threshold settings - song.hopoThreshold = ChartIOHelper.GetHopoThreshold(in settings, song.resolution); - - // Sustain cutoff threshold is not verified, sustains are not cut off by default in .chart - // SP note is not verified, as it is only relevant for .mid - // Note snap threshold is not verified, as the parser doesn't use it - - // Chord HOPO cancellation does not apply in .chart - settings.ChordHopoCancellation = false; + return new MoonSong(resolution); } private static void SubmitDataSync(MoonSong song, AsciiTrimSplitter sectionLines) diff --git a/YARG.Core/MoonscraperChartParser/IO/Midi/MidIOHelper.cs b/YARG.Core/MoonscraperChartParser/IO/Midi/MidIOHelper.cs index 7449242af..4c375d8f2 100644 --- a/YARG.Core/MoonscraperChartParser/IO/Midi/MidIOHelper.cs +++ b/YARG.Core/MoonscraperChartParser/IO/Midi/MidIOHelper.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2016-2020 Alexander Ong +// Copyright (c) 2016-2020 Alexander Ong // See LICENSE in project root for license information. using System.Collections.Generic; @@ -283,10 +283,5 @@ public static bool IsTextEvent(MidiEvent trackEvent, [NotNullWhen(true)] out Bas return false; } - - public static float GetHopoThreshold(ParseSettings settings, float resolution) - { - return settings.GetHopoThreshold(resolution) + 1; // +1 for a small bit of leniency - } } } diff --git a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs index 142c8feeb..5798c8f47 100644 --- a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs +++ b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs @@ -71,19 +71,19 @@ private struct EventProcessParams public static MoonSong ReadMidi(string path) { - var settings = ParseSettings.Default; + var settings = ParseSettings.Default_Midi; return ReadMidi(ref settings, path); } public static MoonSong ReadMidi(Stream stream) { - var settings = ParseSettings.Default; + var settings = ParseSettings.Default_Midi; return ReadMidi(ref settings, stream); } public static MoonSong ReadMidi(MidiFile midi) { - var settings = ParseSettings.Default; + var settings = ParseSettings.Default_Midi; return ReadMidi(ref settings, midi); } @@ -108,7 +108,18 @@ public static MoonSong ReadMidi(ref ParseSettings settings, MidiFile midi) var song = new MoonSong((uint)ticks.TicksPerQuarterNote); // Apply settings - ValidateAndApplySettings(song, ref settings); + song.hopoThreshold = settings.HopoThreshold > ParseSettings.SETTING_DEFAULT + ? (uint)settings.HopoThreshold + : (song.resolution / 3); + + if (settings.SustainCutoffThreshold <= ParseSettings.SETTING_DEFAULT) + { + settings.SustainCutoffThreshold = song.resolution / 3; + } + + // +1 for a small bit of leniency + song.hopoThreshold++; + settings.SustainCutoffThreshold++; // Read all bpm data in first. This will also allow song.TimeToTick to function properly. ReadSync(midi.GetTempoMap(), song); @@ -195,30 +206,6 @@ static void ReadProKeys(ref ParseSettings settings, TrackChunk track, MoonSong s } } - private static void ValidateAndApplySettings(MoonSong song, ref ParseSettings settings) - { - // Apply HOPO threshold settings - song.hopoThreshold = MidIOHelper.GetHopoThreshold(settings, song.resolution); - - // Verify sustain cutoff threshold - if (settings.SustainCutoffThreshold < 0) - { - // Default to 1/12th step + 1 - settings.SustainCutoffThreshold = (long) (song.resolution / 3) + 1; - } - else - { - // Limit minimum cutoff to 1 tick, non-sustain notes created by charting programs are 1 tick - settings.SustainCutoffThreshold = Math.Max(settings.SustainCutoffThreshold, 1); - } - - // SP note is not verified, as it being set is checked for by SP fixups - // Note snap threshold is also not verified, as the parser doesn't use it - - // Enable chord HOPO cancellation - settings.ChordHopoCancellation = true; - } - private static void ReadSync(TempoMap tempoMap, MoonSong song) { YargLogger.LogTrace("Reading sync track"); diff --git a/YARG.Core/MoonscraperChartParser/MoonSong.cs b/YARG.Core/MoonscraperChartParser/MoonSong.cs index c7e179c7a..7115b6229 100644 --- a/YARG.Core/MoonscraperChartParser/MoonSong.cs +++ b/YARG.Core/MoonscraperChartParser/MoonSong.cs @@ -11,8 +11,8 @@ namespace MoonscraperChartEditor.Song { internal class MoonSong { - public float resolution => syncTrack.Resolution; - public float hopoThreshold; + public uint resolution => syncTrack.Resolution; + public uint hopoThreshold; // Charts private readonly MoonChart[] charts; @@ -171,9 +171,9 @@ public bool Remove(MoonVenue venueEvent) return MoonObjectHelper.Remove(venueEvent, venue); } - public float ResolutionScaleRatio(float targetResoltion) + public float ResolutionScaleRatio(uint targetResoltion) { - return targetResoltion / resolution; + return (float)targetResoltion / resolution; } public static MoonChart.GameMode InstrumentToChartGameMode(MoonInstrument instrument) diff --git a/YARG.Core/Song/Cache/CONModification.cs b/YARG.Core/Song/Cache/CONModification.cs new file mode 100644 index 000000000..5c59e29f4 --- /dev/null +++ b/YARG.Core/Song/Cache/CONModification.cs @@ -0,0 +1,16 @@ +using YARG.Core.IO; + +namespace YARG.Core.Song.Cache +{ + public class CONModification + { + public bool Processed = false; + public AbridgedFileInfo? Midi; + public AbridgedFileInfo? Mogg; + public AbridgedFileInfo? Milo; + public AbridgedFileInfo? Image; + public DTAEntry? UpdateDTA; + public DTAEntry? UpgradeDTA; + public RBProUpgrade? UpgradeNode; + } +} diff --git a/YARG.Core/Song/Cache/CacheGroups/CacheGroup.cs b/YARG.Core/Song/Cache/CacheGroups/CacheGroup.cs index 841808693..dfda91002 100644 --- a/YARG.Core/Song/Cache/CacheGroups/CacheGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/CacheGroup.cs @@ -1,36 +1,35 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using System.Text; +using YARG.Core.Extensions; namespace YARG.Core.Song.Cache { - public interface ICacheGroup - where TEntry : SongEntry + public interface ICacheGroup { public int Count { get; } - public ReadOnlyMemory SerializeEntries(Dictionary nodes); + public void SerializeEntries(MemoryStream stream, Dictionary nodes); public bool TryRemoveEntry(SongEntry entryToRemove); - public static void SerializeGroups(List groups, BinaryWriter writer, Dictionary nodes) - where TGroup : ICacheGroup + public static void SerializeGroups(List groups, FileStream fileStream, Dictionary nodes) + where TGroup : ICacheGroup { - var spans = new ReadOnlyMemory[groups.Count]; + var streams = new MemoryStream[groups.Count]; int length = 4; for (int i = 0; i < groups.Count; i++) { - spans[i] = groups[i].SerializeEntries(nodes); - length += sizeof(int) + spans[i].Length; + streams[i] = new MemoryStream(); + groups[i].SerializeEntries(streams[i], nodes); + length += sizeof(int) + (int) streams[i].Length; } - writer.Write(length); - writer.Write(groups.Count); - for (int i = 0; i < groups.Count; i++) + fileStream.Write(length, Endianness.Little); + fileStream.Write(streams.Length, Endianness.Little); + for (int i = 0; i < streams.Length; i++) { - var span = spans[i].Span; - writer.Write(span.Length); - writer.Write(span); + using var stream = streams[i]; + fileStream.Write((int) stream.Length, Endianness.Little); + fileStream.Write(stream.GetBuffer(), 0, (int) stream.Length); } } } diff --git a/YARG.Core/Song/Cache/CacheGroups/ConGroup.cs b/YARG.Core/Song/Cache/CacheGroups/ConGroup.cs index c7040a8b5..4d48fdbd9 100644 --- a/YARG.Core/Song/Cache/CacheGroups/ConGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/ConGroup.cs @@ -1,39 +1,75 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using YARG.Core.Extensions; using YARG.Core.IO; namespace YARG.Core.Song.Cache { - public abstract class CONGroup : ICacheGroup + public abstract class CONGroup : ICacheGroup + where TEntry : RBCONEntry { - protected readonly Dictionary> entries = new(); - - private int _count; - public int Count { get { lock (entries) return _count; } } + protected readonly Dictionary> entries = new(); + public readonly string Location; + public readonly AbridgedFileInfo Info; public readonly string DefaultPlaylist; + public readonly FixedArray SongDTAData; - public abstract string Location { get; } + public int Count + { + get + { + int count = 0; + foreach (var node in entries.Values) + { + count += node.Count; + } + return count; + } + } - protected CONGroup(string defaultPlaylist) + protected CONGroup(in FixedArray songDTAData, string location, in AbridgedFileInfo info, string defaultPlaylist) { + SongDTAData = songDTAData; + Location = location; + Info = info; DefaultPlaylist = defaultPlaylist; } - public abstract void ReadEntry(string nodeName, int index, Dictionary, RBProUpgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings); - public abstract ReadOnlyMemory SerializeEntries(Dictionary nodes); + public abstract void ReadEntry(string nodeName, int index, RBProUpgrade upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings); - public void AddEntry(string name, int index, RBCONEntry entry) + public void SerializeEntries(MemoryStream groupStream, Dictionary nodes) { - SortedDictionary dict; + groupStream.Write(Location); + groupStream.Write(Info.LastUpdatedTime.ToBinary(), Endianness.Little); + groupStream.Write(Count, Endianness.Little); + + using var entryStream = new MemoryStream(); + foreach (var entryList in entries) + { + foreach (var entry in entryList.Value) + { + groupStream.Write(entryList.Key); + groupStream.Write(entry.Key, Endianness.Little); + + entryStream.SetLength(0); + entry.Value.Serialize(entryStream, nodes[entry.Value]); + + groupStream.Write((int) entryStream.Length, Endianness.Little); + groupStream.Write(entryStream.GetBuffer(), 0, (int) entryStream.Length); + } + } + } + + public void AddEntry(string name, int index, TEntry entry) + { + SortedDictionary dict; lock (entries) { if (!entries.TryGetValue(name, out dict)) { - entries.Add(name, dict = new SortedDictionary()); + entries.Add(name, dict = new SortedDictionary()); } - ++_count; } lock (dict) @@ -48,8 +84,6 @@ public bool RemoveEntries(string name) { if (!entries.Remove(name, out var dict)) return false; - - _count -= dict.Count; } return true; } @@ -64,11 +98,10 @@ public void RemoveEntry(string name, int index) { entries.Remove(name); } - --_count; } } - public bool TryGetEntry(string name, int index, out RBCONEntry? entry) + public bool TryGetEntry(string name, int index, out TEntry? entry) { entry = null; lock (entries) @@ -92,37 +125,11 @@ public bool TryRemoveEntry(SongEntry entryToRemove) { entries.Remove(dict.Key); } - --_count; return true; } } } return false; } - - protected void Serialize(BinaryWriter writer, ref Dictionary nodes) - { - writer.Write(_count); - foreach (var entryList in entries) - { - foreach (var entry in entryList.Value) - { - writer.Write(entryList.Key); - writer.Write(entry.Key); - - byte[] data = SerializeEntry(entry.Value, nodes[entry.Value]); - writer.Write(data.Length); - writer.Write(data); - } - } - } - - private static byte[] SerializeEntry(RBCONEntry entry, CategoryCacheWriteNode node) - { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - entry.Serialize(writer, node); - return ms.ToArray(); - } } } diff --git a/YARG.Core/Song/Cache/CacheGroups/IModificationGroup.cs b/YARG.Core/Song/Cache/CacheGroups/IModificationGroup.cs index cd2b62660..4e2f2e118 100644 --- a/YARG.Core/Song/Cache/CacheGroups/IModificationGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/IModificationGroup.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using System.Text; +using YARG.Core.Extensions; namespace YARG.Core.Song.Cache { public interface IModificationGroup { - public ReadOnlyMemory SerializeModifications(); + public void SerializeModifications(MemoryStream stream); - public static void SerializeGroups(List groups, BinaryWriter writer) + public static void SerializeGroups(List groups, FileStream fileStream) where TGroup : IModificationGroup { - var spans = new ReadOnlyMemory[groups.Count]; + var streams = new MemoryStream[groups.Count]; int length = sizeof(int); for (int i = 0; i < groups.Count; i++) { - spans[i] = groups[i].SerializeModifications(); - length += sizeof(int) + spans[i].Length; + var stream = streams[i] = new MemoryStream(); + groups[i].SerializeModifications(stream); + length += sizeof(int) + (int) stream.Length; } - writer.Write(length); - writer.Write(groups.Count); + fileStream.Write(length, Endianness.Little); + fileStream.Write(streams.Length, Endianness.Little); for (int i = 0; i < groups.Count; i++) { - var span = spans[i].Span; - writer.Write(span.Length); - writer.Write(span); + using var stream = streams[i]; + fileStream.Write((int) stream.Length, Endianness.Little); + fileStream.Write(stream.GetBuffer(), 0, (int) stream.Length); } } } diff --git a/YARG.Core/Song/Cache/CacheGroups/IniGroup.cs b/YARG.Core/Song/Cache/CacheGroups/IniGroup.cs index 15ef32ff4..5dc3d03c6 100644 --- a/YARG.Core/Song/Cache/CacheGroups/IniGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/IniGroup.cs @@ -1,19 +1,26 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using YARG.Core.Extensions; namespace YARG.Core.Song.Cache { - public sealed class IniGroup : ICacheGroup + public sealed class IniGroup : ICacheGroup { public readonly string Directory; public readonly Dictionary> entries = new(); - public readonly object iniLock = new(); - private int _count; - - public string Location => Directory; - public int Count => _count; + public int Count + { + get + { + int count = 0; + foreach (var node in entries.Values) + { + count += node.Count; + } + return count; + } + } public IniGroup(string directory) { @@ -24,13 +31,12 @@ public void AddEntry(IniSubEntry entry) { var hash = entry.Hash; List list; - lock (iniLock) + lock (entries) { if (!entries.TryGetValue(hash, out list)) { entries.Add(hash, list = new List()); } - ++_count; } lock (list) @@ -51,30 +57,38 @@ public bool TryRemoveEntry(SongEntry entryToRemove) { entries.Remove(entryToRemove.Hash); } - --_count; return true; } } return false; } - public ReadOnlyMemory SerializeEntries(Dictionary nodes) + public void SerializeEntries(MemoryStream groupStream, Dictionary nodes) { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); + groupStream.Write(Directory); + groupStream.Write(Count, Endianness.Little); - writer.Write(Location); - writer.Write(_count); + using MemoryStream entryStream = new(); foreach (var shared in entries) { foreach (var entry in shared.Value) { - var buffer = entry.Serialize(nodes[entry], Location); - writer.Write(buffer.Length); - writer.Write(buffer); + entryStream.SetLength(0); + + // Validation block + entryStream.Write(entry.SubType == EntryType.Sng); + string relativePath = Path.GetRelativePath(Directory, entry.Location); + if (relativePath == ".") + { + relativePath = string.Empty; + } + entryStream.Write(relativePath); + entry.Serialize(entryStream, nodes[entry]); + + groupStream.Write((int) entryStream.Length, Endianness.Little); + groupStream.Write(entryStream.GetBuffer(), 0, (int) entryStream.Length); } } - return new ReadOnlyMemory(ms.GetBuffer(), 0, (int) ms.Length); } } } diff --git a/YARG.Core/Song/Cache/CacheGroups/LockedList.cs b/YARG.Core/Song/Cache/CacheGroups/LockedList.cs deleted file mode 100644 index d4b28cee8..000000000 --- a/YARG.Core/Song/Cache/CacheGroups/LockedList.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace YARG.Core.Song.Cache -{ - public class LockedList - { - public readonly object Lock = new(); - public readonly List Values = new(); - - public void Add(T value) - { - lock (Lock) - Values.Add(value); - } - } -} diff --git a/YARG.Core/Song/Cache/CacheGroups/PackedCONGroup.cs b/YARG.Core/Song/Cache/CacheGroups/PackedCONGroup.cs index accc76f53..d766632e9 100644 --- a/YARG.Core/Song/Cache/CacheGroups/PackedCONGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/PackedCONGroup.cs @@ -1,129 +1,49 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using YARG.Core.Extensions; using YARG.Core.IO; -using YARG.Core.Logging; namespace YARG.Core.Song.Cache { - public sealed class PackedCONGroup : CONGroup, IUpgradeGroup + public sealed class PackedCONGroup : CONGroup, IModificationGroup { - public readonly AbridgedFileInfo Info; - public readonly CONFile ConFile; - public readonly CONFileListing? SongDTA; - public readonly CONFileListing? UpgradeDta; - public Stream? Stream; + public readonly List Listings; + public readonly Dictionary>> SongNodes = new(); + public readonly Dictionary Container, PackedRBProUpgrade Upgrade)> Upgrades = new(); + public readonly FixedArray UpgradeDTAData; + public FileStream Stream; - private FixedArray _songDTAData = FixedArray.Null; - private FixedArray _upgradeDTAData = FixedArray.Null; - - public override string Location => Info.FullName; - public Dictionary Upgrades { get; } = new(); - - public PackedCONGroup(CONFile conFile, AbridgedFileInfo info, string defaultPlaylist) - : base(defaultPlaylist) + public PackedCONGroup(List listings, FixedArray songData, FixedArray upgradeData, + Dictionary>> songNodes, Dictionary, PackedRBProUpgrade)> upgrades, + in AbridgedFileInfo info, string defaultPlaylist) + : base(in songData, info.FullName, in info, defaultPlaylist) { - const string SONGSFILEPATH = "songs/songs.dta"; - const string UPGRADESFILEPATH = "songs_upgrades/upgrades.dta"; - - Info = info; - ConFile = conFile; - conFile.TryGetListing(UPGRADESFILEPATH, out UpgradeDta); - conFile.TryGetListing(SONGSFILEPATH, out SongDTA); + Listings = listings; + SongNodes = songNodes; + Upgrades = upgrades; + UpgradeDTAData = upgradeData; + Stream = null!; } - public override void ReadEntry(string nodeName, int index, Dictionary, RBProUpgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + public override void ReadEntry(string nodeName, int index, RBProUpgrade upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { - var song = PackedRBCONEntry.TryLoadFromCache(in ConFile, nodeName, upgrades, stream, strings); + var song = PackedRBCONEntry.TryLoadFromCache(Listings, nodeName, upgrade, stream, strings); if (song != null) { AddEntry(nodeName, index, song); } } - public override ReadOnlyMemory SerializeEntries(Dictionary nodes) - { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - - writer.Write(Location); - writer.Write(SongDTA!.LastWrite.ToBinary()); - Serialize(writer, ref nodes); - return new ReadOnlyMemory(ms.GetBuffer(), 0, (int)ms.Length); - } - - public void AddUpgrade(string name, RBProUpgrade upgrade) { lock (Upgrades) Upgrades[name] = upgrade; } - - public bool LoadUpgrades(out YARGTextContainer container) - { - if (UpgradeDta == null) - { - container = default; - return false; - } - - try - { - Stream = new FileStream(Info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); - _upgradeDTAData = UpgradeDta.LoadAllBytes(Stream); - return YARGDTAReader.TryCreate(_upgradeDTAData, out container); - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while loading {UpgradeDta.Filename}"); - container = default; - return false; - } - } - - public bool LoadSongs(out YARGTextContainer container) + public void SerializeModifications(MemoryStream stream) { - if (SongDTA == null) + stream.Write(Location); + stream.Write(Info.LastUpdatedTime.ToBinary(), Endianness.Little); + stream.Write(Upgrades.Count, Endianness.Little); + foreach (var node in Upgrades) { - container = default; - return false; + stream.Write(node.Key); + node.Value.Upgrade.WriteToCache(stream); } - - try - { - Stream ??= new FileStream(Info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); - _songDTAData = SongDTA.LoadAllBytes(Stream); - return YARGDTAReader.TryCreate(_songDTAData, out container); - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while loading {SongDTA.Filename}"); - container = default; - return false; - } - } - - public ReadOnlyMemory SerializeModifications() - { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - - writer.Write(Location); - writer.Write(Info.LastUpdatedTime.ToBinary()); - writer.Write(UpgradeDta!.LastWrite.ToBinary()); - writer.Write(Upgrades.Count); - foreach (var upgrade in Upgrades) - { - writer.Write(upgrade.Key); - upgrade.Value.WriteToCache(writer); - } - return new ReadOnlyMemory(ms.GetBuffer(), 0, (int)ms.Length); - } - - public void DisposeStreamAndSongDTA() - { - Stream?.Dispose(); - _songDTAData.Dispose(); - } - - public void DisposeUpgradeDTA() - { - _upgradeDTAData.Dispose(); } } } diff --git a/YARG.Core/Song/Cache/CacheGroups/UnpackedCONGroup.cs b/YARG.Core/Song/Cache/CacheGroups/UnpackedCONGroup.cs index 737fe118a..244f6cf84 100644 --- a/YARG.Core/Song/Cache/CacheGroups/UnpackedCONGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/UnpackedCONGroup.cs @@ -6,61 +6,23 @@ namespace YARG.Core.Song.Cache { - public sealed class UnpackedCONGroup : CONGroup, IDisposable + public sealed class UnpackedCONGroup : CONGroup { - public readonly AbridgedFileInfo DTA; - private FixedArray _fileData = FixedArray.Null; + public readonly Dictionary>> SongNodes = new(); - public override string Location { get; } - - public UnpackedCONGroup(string directory, FileInfo dta, string defaultPlaylist) - : base(defaultPlaylist) + public UnpackedCONGroup(in FixedArray data, Dictionary>> songNodes, string directory, in AbridgedFileInfo dta, string defaultPlaylist) + : base(in data, directory, in dta, defaultPlaylist) { - Location = directory; - DTA = new AbridgedFileInfo(dta); + SongNodes = songNodes; } - public bool LoadDTA(out YARGTextContainer container) + public override void ReadEntry(string nodeName, int index, RBProUpgrade upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { - try - { - _fileData = FixedArray.Load(DTA.FullName); - return YARGDTAReader.TryCreate(_fileData, out container); - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while loading {DTA.FullName}"); - container = default; - return false; - } - } - - public override void ReadEntry(string nodeName, int index, Dictionary, RBProUpgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings) - { - var song = UnpackedRBCONEntry.TryLoadFromCache(Location, DTA, nodeName, upgrades, stream, strings); + var song = UnpackedRBCONEntry.TryLoadFromCache(Location, in Info, nodeName, upgrade, stream, strings); if (song != null) { AddEntry(nodeName, index, song); } } - - public override ReadOnlyMemory SerializeEntries(Dictionary nodes) - { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - - writer.Write(Location); - writer.Write(DTA.LastUpdatedTime.ToBinary()); - Serialize(writer, ref nodes); - return new ReadOnlyMemory(ms.GetBuffer(), 0, (int)ms.Length); - } - - public void Dispose() - { - if (_fileData.IsAllocated) - { - _fileData.Dispose(); - } - } } } diff --git a/YARG.Core/Song/Cache/CacheGroups/UpdateGroup.cs b/YARG.Core/Song/Cache/CacheGroups/UpdateGroup.cs index 578c11f6e..6ee432bec 100644 --- a/YARG.Core/Song/Cache/CacheGroups/UpdateGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/UpdateGroup.cs @@ -6,87 +6,68 @@ namespace YARG.Core.Song.Cache { - public sealed class UpdateGroup : IModificationGroup, IDisposable + public sealed class UpdateGroup : IModificationGroup { - public readonly DirectoryInfo Directory; + public readonly string Directory; public readonly DateTime DTALastWrite; public readonly Dictionary Updates = new(); + public readonly FixedArray DTAData; - private readonly FixedArray _dtaData; - - public UpdateGroup(DirectoryInfo directory, DateTime dtaLastUpdate, in FixedArray dtaData) + public UpdateGroup(string directory, DateTime lastWrite, FixedArray data, Dictionary updates) { + DTAData = data; Directory = directory; - DTALastWrite = dtaLastUpdate; - _dtaData = dtaData; + DTALastWrite = lastWrite; + Updates = updates; } - public ReadOnlyMemory SerializeModifications() + public void SerializeModifications(MemoryStream stream) { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - - writer.Write(Directory.FullName); - writer.Write(DTALastWrite.ToBinary()); - writer.Write(Updates.Count); + stream.Write(Directory); + stream.Write(DTALastWrite.ToBinary(), Endianness.Little); + stream.Write(Updates.Count, Endianness.Little); foreach (var (name, update) in Updates) { - writer.Write(name); - update.Serialize(writer); + stream.Write(name); + update.Serialize(stream); } - return new ReadOnlyMemory(ms.GetBuffer(), 0, (int)ms.Length); - } - - public void Dispose() - { - _dtaData.Dispose(); } } public class SongUpdate { - private readonly List> _containers; - - public readonly string BaseDirectory; + public readonly List> Containers; public readonly AbridgedFileInfo? Midi; public readonly AbridgedFileInfo? Mogg; public readonly AbridgedFileInfo? Milo; public readonly AbridgedFileInfo? Image; - public YARGTextContainer[] Containers => _containers.ToArray(); - - internal SongUpdate(string directory, AbridgedFileInfo? midi, AbridgedFileInfo? mogg, AbridgedFileInfo? milo, AbridgedFileInfo? image) + internal SongUpdate(in AbridgedFileInfo? midi, in AbridgedFileInfo? mogg, in AbridgedFileInfo? milo, in AbridgedFileInfo? image) { - _containers = new(); - BaseDirectory = directory; + Containers = new(); Midi = midi; Mogg = mogg; Milo = milo; Image = image; } - public void Add(in YARGTextContainer container) + public void Serialize(MemoryStream stream) { - _containers.Add(container); - } - - public void Serialize(BinaryWriter writer) - { - WriteInfo(Midi, writer); - WriteInfo(Mogg, writer); - WriteInfo(Milo, writer); - WriteInfo(Image, writer); + WriteInfo(Midi, stream); + WriteInfo(Mogg, stream); + WriteInfo(Milo, stream); + WriteInfo(Image, stream); - static void WriteInfo(in AbridgedFileInfo? info, BinaryWriter writer) + static void WriteInfo(in AbridgedFileInfo? info, MemoryStream stream) { if (info != null) { - writer.Write(true); - writer.Write(info.Value.LastUpdatedTime.ToBinary()); + stream.Write(true); + stream.Write(info.Value.LastUpdatedTime.ToBinary(), Endianness.Little); } else { - writer.Write(false); + stream.Write(false); } } } @@ -117,19 +98,13 @@ public bool Validate(UnmanagedMemoryStream stream) static bool CheckInfo(in AbridgedFileInfo? info, UnmanagedMemoryStream stream) { - if (stream.ReadBoolean()) + if (!stream.ReadBoolean()) { - var lastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); - if (info == null || info.Value.LastUpdatedTime != lastWrite) - { - return false; - } + return info == null; } - else if (info != null) - { - return false; - } - return true; + + var lastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); + return info != null && info.Value.LastUpdatedTime == lastWrite; } } diff --git a/YARG.Core/Song/Cache/CacheGroups/UpgradeGroup.cs b/YARG.Core/Song/Cache/CacheGroups/UpgradeGroup.cs index 88c5efb79..af08e7b6f 100644 --- a/YARG.Core/Song/Cache/CacheGroups/UpgradeGroup.cs +++ b/YARG.Core/Song/Cache/CacheGroups/UpgradeGroup.cs @@ -1,49 +1,36 @@ using System; using System.Collections.Generic; using System.IO; +using YARG.Core.Extensions; using YARG.Core.IO; namespace YARG.Core.Song.Cache { - public interface IUpgradeGroup : IModificationGroup + public sealed class UpgradeGroup : IModificationGroup { - public Dictionary Upgrades { get; } - } - - public sealed class UpgradeGroup : IUpgradeGroup, IDisposable - { - private readonly string _directory; - private readonly DateTime _dtaLastUpdate; - private readonly FixedArray _dtaData; - - public Dictionary Upgrades { get; } = new(); + public readonly string Directory; + public readonly DateTime DTALastWrite; + public readonly Dictionary Container, UnpackedRBProUpgrade Upgrade)> Upgrades; + public readonly FixedArray DTAData; - public UpgradeGroup(string directory, DateTime dtaLastUpdate, in FixedArray dtaData) + public UpgradeGroup(string directory, DateTime lastWrite, in FixedArray data, Dictionary Node, UnpackedRBProUpgrade Upgrade)> upgrades) { - _directory = directory; - _dtaLastUpdate = dtaLastUpdate; - _dtaData = dtaData; + Directory = directory; + DTALastWrite = lastWrite; + Upgrades = upgrades; + DTAData = data; } - public ReadOnlyMemory SerializeModifications() + public void SerializeModifications(MemoryStream stream) { - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - - writer.Write(_directory); - writer.Write(_dtaLastUpdate.ToBinary()); - writer.Write(Upgrades.Count); - foreach (var upgrade in Upgrades) + stream.Write(Directory); + stream.Write(DTALastWrite.ToBinary(), Endianness.Little); + stream.Write(Upgrades.Count, Endianness.Little); + foreach (var node in Upgrades) { - writer.Write(upgrade.Key); - upgrade.Value.WriteToCache(writer); + stream.Write(node.Key); + node.Value.Upgrade.WriteToCache(stream); } - return new ReadOnlyMemory(ms.GetBuffer(), 0, (int)ms.Length); - } - - public void Dispose() - { - _dtaData.Dispose(); } } } diff --git a/YARG.Core/Song/Cache/CacheHandler.Parallel.cs b/YARG.Core/Song/Cache/CacheHandler.Parallel.cs index 95e42dfd8..1f72302a3 100644 --- a/YARG.Core/Song/Cache/CacheHandler.Parallel.cs +++ b/YARG.Core/Song/Cache/CacheHandler.Parallel.cs @@ -30,11 +30,19 @@ protected override void FindNewEntries() var group = conGroups[i]; conActions[i] = () => { - if (group.LoadSongs(out var container)) + using var stream = group.Stream = new FileStream(group.Info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); + Parallel.ForEach(group.SongNodes, node => { - ScanCONGroup(group, ref container, ScanPackedCONNode); - } - group.DisposeStreamAndSongDTA(); + for (int j = 0; j < node.Value.Count; ++j) + { + var container = node.Value[j]; + unsafe + { + ScanCONNode(group, node.Key, j, container, &PackedRBCONEntry.ProcessNewEntry); + } + } + }); + group.SongDTAData.Dispose(); }; } @@ -43,19 +51,22 @@ protected override void FindNewEntries() var group = extractedConGroups[i]; conActions[conGroups.Count + i] = () => { - if (group.LoadDTA(out var container)) + Parallel.ForEach(group.SongNodes, node => { - ScanCONGroup(group, ref container, ScanUnpackedCONNode); - } - group.Dispose(); + for (int j = 0; j < node.Value.Count; ++j) + { + var container = node.Value[j]; + unsafe + { + ScanCONNode(group, node.Key, j, container, &UnpackedRBCONEntry.ProcessNewEntry); + } + } + }); + group.SongDTAData.Dispose(); }; } Parallel.ForEach(conActions, action => action()); - foreach (var group in conGroups) - { - group.DisposeUpgradeDTA(); - } } protected override void TraverseDirectory(in FileCollection collection, IniGroup group, PlaylistTracker tracker) @@ -168,26 +179,6 @@ protected override void Deserialize_Quick(UnmanagedMemoryStream stream) } } - protected override void AddUpdate(string name, DateTime dtaLastWrite, SongUpdate update) - { - lock (updates) - { - if (!updates.TryGetValue(name, out var list)) - { - updates.Add(name, list = new()); - } - list.Add(dtaLastWrite, update); - } - } - - protected override void AddUpgrade(string name, in YARGTextContainer container, RBProUpgrade upgrade) - { - lock (upgrades) - { - upgrades[name] = new(container, upgrade); - } - } - protected override void AddPackedCONGroup(PackedCONGroup group) { lock (conGroups) @@ -228,6 +219,17 @@ protected override void AddCollectionToCache(in FileCollection collection) } } + protected override void AddCacheUpgrade(string name, RBProUpgrade upgrade) + { + lock (cacheUpgrades) + { + if (!cacheUpgrades.TryGetValue(name, out var currUpgrade) || currUpgrade.LastUpdatedTime < upgrade.LastUpdatedTime) + { + cacheUpgrades[name] = upgrade; + } + } + } + protected override void RemoveCONEntry(string shortname) { lock (conGroups) @@ -253,48 +255,26 @@ protected override void RemoveCONEntry(string shortname) } } - protected override bool CanAddUpgrade(string shortname, DateTime lastUpdated) + protected override CONModification GetModification(string name) { - lock (upgradeGroups) + CONModification modification; + lock (conModifications) { - return CanAddUpgrade(upgradeGroups, shortname, lastUpdated) ?? false; - } - } - - protected override bool CanAddUpgrade_CONInclusive(string shortname, DateTime lastUpdated) - { - lock (conGroups) - { - var result = CanAddUpgrade(conGroups, shortname, lastUpdated); - if (result != null) + if (!conModifications.TryGetValue(name, out modification)) { - return (bool) result; + conModifications.Add(name, modification = new CONModification()); } } - lock (upgradeGroups) - { - return CanAddUpgrade(upgradeGroups, shortname, lastUpdated) ?? false; - } - } - - protected override Dictionary> MapUpdateFiles(in FileCollection collection) - { - Dictionary> mapping = new(); - Parallel.ForEach(collection.SubDirectories, dir => + lock (modification) { - var infos = new Dictionary(); - foreach (var file in dir.Value.EnumerateFiles("*", SearchOption.AllDirectories)) + if (!modification.Processed) { - infos[file.Name] = file; + InitModification(modification, name); + modification.Processed = true; } - - lock (mapping) - { - mapping.Add(dir.Key, infos); - } - }); - return mapping; + } + return modification; } protected override bool FindOrMarkDirectory(string directory) @@ -337,33 +317,6 @@ protected override void AddInvalidSong(string name) } } - protected override void CleanupDuplicates() - { - Parallel.ForEach(duplicatesToRemove, entry => - { - lock (iniGroups) - { - if (TryRemove(iniGroups, entry)) - { - return; - } - } - - lock (conGroups) - { - if (TryRemove(conGroups, entry)) - { - return; - } - } - - lock (extractedConGroups) - { - TryRemove(extractedConGroups, entry); - } - }); - } - private sealed class ParallelExceptionTracker : Exception { private readonly object _lock = new object(); @@ -408,33 +361,6 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } - private void ScanCONGroup(TGroup group, ref YARGTextContainer container, Action> func) - where TGroup : CONGroup - { - try - { - var slices = new List<(string Name, int Index, YARGTextContainer Container)>(); - Dictionary indices = new(); - while (YARGDTAReader.StartNode(ref container)) - { - string name = YARGDTAReader.GetNameOfNode(ref container, true); - if (indices.TryGetValue(name, out int index)) - { - ++index; - } - indices[name] = index; - - slices.Add((name, index, container)); - YARGDTAReader.EndNode(ref container); - } - Parallel.ForEach(slices, slice => func(group, slice.Name, slice.Index, slice.Container)); - } - catch (Exception e) - { - YargLogger.LogException(e, $"Error while scanning CON group {group.Location}!"); - } - } - private readonly struct CacheEnumerable : IEnumerable { private readonly UnmanagedMemoryStream _stream; @@ -540,7 +466,7 @@ private void ReadPackedCONGroup(UnmanagedMemoryStream stream, CategoryCacheStrin var group = ReadCONGroupHeader(stream); if (group != null) { - ReadCONGroup(group, stream, strings, tracker); + ReadCONGroup(group, stream, strings, tracker); } } @@ -549,12 +475,13 @@ private void ReadUnpackedCONGroup(UnmanagedMemoryStream stream, CategoryCacheStr var group = ReadExtractedCONGroupHeader(stream); if (group != null) { - ReadCONGroup(group, stream, strings, tracker); + ReadCONGroup(group, stream, strings, tracker); } } - private void ReadCONGroup(TGroup group, UnmanagedMemoryStream stream, CategoryCacheStrings strings, ParallelExceptionTracker tracker) - where TGroup : CONGroup + private void ReadCONGroup(TGroup group, UnmanagedMemoryStream stream, CategoryCacheStrings strings, ParallelExceptionTracker tracker) + where TGroup : CONGroup + where TEntry : RBCONEntry { var enumerable = new CacheEnumerable<(string Name, int Index, UnmanagedMemoryStream Stream)?>(stream, tracker, () => { @@ -575,7 +502,8 @@ private void ReadCONGroup(TGroup group, UnmanagedMemoryStream stream, Ca // Error catching must be done per-thread try { - group.ReadEntry(value.Name, value.Index, upgrades, value.Stream, strings); + cacheUpgrades.TryGetValue(value.Name, out var upgrade); + group.ReadEntry(value.Name, value.Index, upgrade, value.Stream, strings); } catch (Exception ex) { @@ -608,10 +536,7 @@ private void QuickReadIniGroup(UnmanagedMemoryStream stream, CategoryCacheString private void QuickReadCONGroup(UnmanagedMemoryStream stream, CategoryCacheStrings strings, ParallelExceptionTracker tracker) { - var group = QuickReadCONGroupHeader(stream); - if (group == null) - return; - + var listings = QuickReadCONGroupHeader(stream); var enumerable = new CacheEnumerable<(string Name, UnmanagedMemoryStream Stream)>(stream, tracker, () => { string name = stream.ReadString(); @@ -626,7 +551,8 @@ private void QuickReadCONGroup(UnmanagedMemoryStream stream, CategoryCacheString { try { - AddEntry(PackedRBCONEntry.LoadFromCache_Quick(in group.ConFile, slice.Name, upgrades, slice.Stream, strings)); + cacheUpgrades.TryGetValue(slice.Name, out var upgrade); + AddEntry(PackedRBCONEntry.LoadFromCache_Quick(listings, slice.Name, upgrade, slice.Stream, strings)); } catch (Exception ex) { @@ -654,7 +580,8 @@ private void QuickReadExtractedCONGroup(UnmanagedMemoryStream stream, CategoryCa { try { - AddEntry(UnpackedRBCONEntry.LoadFromCache_Quick(directory, dta, slice.Name, upgrades, slice.Stream, strings)); + cacheUpgrades.TryGetValue(slice.Name, out var upgrade); + AddEntry(UnpackedRBCONEntry.LoadFromCache_Quick(directory, dta, upgrade, slice.Stream, strings)); } catch (Exception ex) { diff --git a/YARG.Core/Song/Cache/CacheHandler.Sequential.cs b/YARG.Core/Song/Cache/CacheHandler.Sequential.cs index b0416b2ed..2c1096939 100644 --- a/YARG.Core/Song/Cache/CacheHandler.Sequential.cs +++ b/YARG.Core/Song/Cache/CacheHandler.Sequential.cs @@ -23,25 +23,35 @@ protected override void FindNewEntries() foreach (var group in conGroups) { - if (group.LoadSongs(out var container)) + using var stream = group.Stream = new FileStream(group.Info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); + foreach (var node in group.SongNodes) { - ScanCONGroup(group, ref container, ScanPackedCONNode); + for (int j = 0; j < node.Value.Count; ++j) + { + var container = node.Value[j]; + unsafe + { + ScanCONNode(group, node.Key, j, container, &PackedRBCONEntry.ProcessNewEntry); + } + } } - group.DisposeStreamAndSongDTA(); + group.SongDTAData.Dispose(); } foreach (var group in extractedConGroups) { - if (group.LoadDTA(out var container)) + foreach (var node in group.SongNodes) { - ScanCONGroup(group, ref container, ScanUnpackedCONNode); + for (int j = 0; j < node.Value.Count; ++j) + { + var container = node.Value[j]; + unsafe + { + ScanCONNode(group, node.Key, j, container, &UnpackedRBCONEntry.ProcessNewEntry); + } + } } - group.Dispose(); - } - - foreach (var group in conGroups) - { - group.DisposeUpgradeDTA(); + group.SongDTAData.Dispose(); } } @@ -105,20 +115,6 @@ protected override void Deserialize_Quick(UnmanagedMemoryStream stream) RunEntryTasks(stream, strings, QuickReadExtractedCONGroup); } - protected override void AddUpdate(string name, DateTime dtaLastWrite, SongUpdate update) - { - if (!updates.TryGetValue(name, out var list)) - { - updates.Add(name, list = new()); - } - list.Add(dtaLastWrite, update); - } - - protected override void AddUpgrade(string name, in YARGTextContainer container, RBProUpgrade upgrade) - { - upgrades[name] = new(container, upgrade); - } - protected override void AddPackedCONGroup(PackedCONGroup group) { conGroups.Add(group); @@ -144,6 +140,14 @@ protected override void AddCollectionToCache(in FileCollection collection) collectionCache.Add(collection.Directory.FullName, collection); } + protected override void AddCacheUpgrade(string name, RBProUpgrade upgrade) + { + if (!cacheUpgrades.TryGetValue(name, out var currUpgrade) || currUpgrade.LastUpdatedTime < upgrade.LastUpdatedTime) + { + cacheUpgrades[name] = upgrade; + } + } + protected override void RemoveCONEntry(string shortname) { foreach (var group in conGroups) @@ -163,34 +167,14 @@ protected override void RemoveCONEntry(string shortname) } } - protected override bool CanAddUpgrade(string shortname, DateTime lastUpdated) + protected override CONModification GetModification(string name) { - return CanAddUpgrade(upgradeGroups, shortname, lastUpdated) ?? false; - } - - protected override bool CanAddUpgrade_CONInclusive(string shortname, DateTime lastUpdated) - { - var result = CanAddUpgrade(conGroups, shortname, lastUpdated); - if (result != null) + if (!conModifications.TryGetValue(name, out var modification)) { - return (bool) result; + conModifications.Add(name, modification = new CONModification()); + InitModification(modification, name); } - return CanAddUpgrade(upgradeGroups, shortname, lastUpdated) ?? false; - } - - protected override Dictionary> MapUpdateFiles(in FileCollection collection) - { - Dictionary> mapping = new(); - foreach (var dir in collection.SubDirectories) - { - var infos = new Dictionary(); - foreach (var file in dir.Value.EnumerateFiles("*", SearchOption.AllDirectories)) - { - infos[file.Name] = file; - } - mapping[dir.Key] = infos; - } - return mapping; + return modification; } protected override PackedCONGroup? FindCONGroup(string filename) @@ -198,49 +182,6 @@ protected override Dictionary> MapUpdateFil return conGroups.Find(node => node.Location == filename); } - protected override void CleanupDuplicates() - { - foreach (var entry in duplicatesToRemove) - { - if (TryRemove(iniGroups, entry)) - { - continue; - } - - if (TryRemove(conGroups, entry)) - { - continue; - } - - TryRemove(extractedConGroups, entry); - } - } - - private void ScanCONGroup(TGroup group, ref YARGTextContainer container, Action> func) - where TGroup : CONGroup - { - try - { - Dictionary indices = new(); - while (YARGDTAReader.StartNode(ref container)) - { - string name = YARGDTAReader.GetNameOfNode(ref container, true); - if (indices.TryGetValue(name, out int index)) - { - ++index; - } - indices[name] = index; - - func(group, name, index, container); - YARGDTAReader.EndNode(ref container); - } - } - catch (Exception e) - { - YargLogger.LogException(e, $"Error while scanning CON group {group.Location}!"); - } - } - private void ReadIniGroup(UnmanagedMemoryStream stream, CategoryCacheStrings strings) { string directory = stream.ReadString(); @@ -262,18 +203,52 @@ private void ReadIniGroup(UnmanagedMemoryStream stream, CategoryCacheStrings str private void ReadPackedCONGroup(UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var group = ReadCONGroupHeader(stream); - if (group != null) + if (group == null) + { + return; + } + + int count = stream.Read(Endianness.Little); + for (int i = 0; i < count; ++i) { - ReadCONGroup(stream, (string name, int index, UnmanagedMemoryStream slice) => group.ReadEntry(name, index, upgrades, slice, strings)); + string name = stream.ReadString(); + int index = stream.Read(Endianness.Little); + int length = stream.Read(Endianness.Little); + if (invalidSongsInCache.Contains(name)) + { + stream.Position += length; + continue; + } + + var entryStream = stream.Slice(length); + cacheUpgrades.TryGetValue(name, out var upgrade); + group.ReadEntry(name, index, upgrade, entryStream, strings); } } private void ReadUnpackedCONGroup(UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var group = ReadExtractedCONGroupHeader(stream); - if (group != null) + if (group == null) { - ReadCONGroup(stream, (string name, int index, UnmanagedMemoryStream slice) => group.ReadEntry(name, index, upgrades, slice, strings)); + return; + } + + int count = stream.Read(Endianness.Little); + for (int i = 0; i < count; ++i) + { + string name = stream.ReadString(); + int index = stream.Read(Endianness.Little); + int length = stream.Read(Endianness.Little); + if (invalidSongsInCache.Contains(name)) + { + stream.Position += length; + continue; + } + + var entryStream = stream.Slice(length); + cacheUpgrades.TryGetValue(name, out var upgrade); + group.ReadEntry(name, index, upgrade, entryStream, strings); } } @@ -291,10 +266,7 @@ private void QuickReadIniGroup(UnmanagedMemoryStream stream, CategoryCacheString private void QuickReadCONGroup(UnmanagedMemoryStream stream, CategoryCacheStrings strings) { - var group = QuickReadCONGroupHeader(stream); - if (group == null) - return; - + var listings = QuickReadCONGroupHeader(stream); int count = stream.Read(Endianness.Little); for (int i = 0; i < count; ++i) { @@ -304,7 +276,8 @@ private void QuickReadCONGroup(UnmanagedMemoryStream stream, CategoryCacheString int length = stream.Read(Endianness.Little); var slice = stream.Slice(length); - AddEntry(PackedRBCONEntry.LoadFromCache_Quick(in group.ConFile, name, upgrades, slice, strings)); + cacheUpgrades.TryGetValue(name, out var upgrade); + AddEntry(PackedRBCONEntry.LoadFromCache_Quick(listings, name, upgrade, slice, strings)); } } @@ -322,7 +295,8 @@ private void QuickReadExtractedCONGroup(UnmanagedMemoryStream stream, CategoryCa int length = stream.Read(Endianness.Little); var slice = stream.Slice(length); - AddEntry(UnpackedRBCONEntry.LoadFromCache_Quick(directory, dta, name, upgrades, slice, strings)); + cacheUpgrades.TryGetValue(name, out var upgrade); + AddEntry(UnpackedRBCONEntry.LoadFromCache_Quick(directory, dta, upgrade, slice, strings)); } } diff --git a/YARG.Core/Song/Cache/CacheHandler.cs b/YARG.Core/Song/Cache/CacheHandler.cs index 23d2d00fa..930d64db6 100644 --- a/YARG.Core/Song/Cache/CacheHandler.cs +++ b/YARG.Core/Song/Cache/CacheHandler.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using YARG.Core.Audio; using YARG.Core.Extensions; using YARG.Core.IO; @@ -34,41 +33,46 @@ public abstract class CacheHandler /// Format is YY_MM_DD_RR: Y = year, M = month, D = day, R = revision (reset across dates, only increment /// if multiple cache version changes happen in a single day). /// - public const int CACHE_VERSION = 24_09_24_01; + public const int CACHE_VERSION = 24_10_02_01; public static ScanProgressTracker Progress => _progress; private static ScanProgressTracker _progress; - public static SongCache RunScan(bool fast, string cacheLocation, string badSongsLocation, bool multithreading, bool allowDuplicates, bool fullDirectoryPlaylists, List baseDirectories) + public static SongCache RunScan(bool tryQuickScan, string cacheLocation, string badSongsLocation, bool multithreading, bool allowDuplicates, bool fullDirectoryPlaylists, List baseDirectories) { CacheHandler handler = multithreading ? new ParallelCacheHandler(baseDirectories, allowDuplicates, fullDirectoryPlaylists) : new SequentialCacheHandler(baseDirectories, allowDuplicates, fullDirectoryPlaylists); + // Some ini entry items won't come with the song length defined in the .ini file. + // In those instances, we'll need to attempt to load the audio files that accompany the chart + // to evaluate the length directly. + // This toggle simply keeps those generated mixers from spamming the logs on creation. GlobalAudioHandler.LogMixerStatus = false; try { - if (!fast || !QuickScan(handler, cacheLocation)) - FullScan(handler, !fast, cacheLocation, badSongsLocation); + // Quick scans only fail if they parse zero entries (which could be the result of a few things) + if (!tryQuickScan || !QuickScan(handler, cacheLocation)) + { + // If a quick scan failed, there's no point to re-reading it in the full scan + FullScan(handler, !tryQuickScan, cacheLocation, badSongsLocation); + } } catch (Exception ex) { YargLogger.LogException(ex, "Unknown error while running song scan!"); } - - foreach (var group in handler.upgradeGroups) - { - group.Dispose(); - } - - foreach (var group in handler.updateGroups) - { - group.Dispose(); - } GlobalAudioHandler.LogMixerStatus = true; return handler.cache; } + /// + /// Reads the entries from a cache file - performing very few validation checks on the entries contained within + /// for the sole purpose of speeding through to gameplay. + /// + /// A parallel or sequential handler + /// File path of the cache + /// Whether the scan sucessfully parsed entries private static bool QuickScan(CacheHandler handler, string cacheLocation) { try @@ -99,6 +103,18 @@ private static bool QuickScan(CacheHandler handler, string cacheLocation) return true; } + /// + /// Runs a full scan process for a user's library. + /// Firstly, it attempts to read entries from a cache file - performing all validation checks necessary + /// to ensure that the player can immediately play whatever comes off the cache. + /// Secondly, we traverse the user's filesystem starting from their provided base directory nodes for any entries + /// that were not found from the cache or required re-evaluating. + /// Finally, we write the results of the scan back to a cache file and, if necessary, a badsongs.txt file containing the failures. + /// + /// A parallel or sequential handler + /// A flag communicating whether to perform the cache read (false only from failed quick scans) + /// File path of the cache + /// File path of the badsongs.txt private static void FullScan(CacheHandler handler, bool loadCache, string cacheLocation, string badSongsLocation) { if (loadCache) @@ -121,6 +137,10 @@ private static void FullScan(CacheHandler handler, bool loadCache, string cacheL _progress.Stage = ScanStage.LoadingSongs; handler.FindNewEntries(); + // CON, Upgrade, and Update groups hold onto the DTA data in memory. + // Once all entries are processed, they are no longer useful to us, so we dispose of them here. + handler.DisposeLeftoverData(); + _progress.Stage = ScanStage.CleaningDuplicates; handler.CleanupDuplicates(); @@ -165,9 +185,8 @@ private static void FullScan(CacheHandler handler, bool loadCache, string cacheL protected readonly List upgradeGroups = new(); protected readonly List conGroups = new(); protected readonly List extractedConGroups = new(); - - protected readonly Dictionary> updates = new(); - protected readonly Dictionary, RBProUpgrade)> upgrades = new(); + protected readonly Dictionary conModifications = new(); + protected readonly Dictionary cacheUpgrades = new(); protected readonly HashSet preScannedDirectories = new(); protected readonly HashSet preScannedFiles = new(); @@ -197,28 +216,96 @@ protected CacheHandler(List baseDirectories, bool allowDuplicates, bool } } + /// + /// Sorts entries + /// protected abstract void SortEntries(); - protected abstract void AddUpdate(string name, DateTime dtaLastWrite, SongUpdate update); - protected abstract void AddUpgrade(string name, in YARGTextContainer container, RBProUpgrade upgrade); + + /// + /// Adds a instance to the shared list of packed con groups. + /// protected abstract void AddPackedCONGroup(PackedCONGroup group); + + /// + /// Adds a instance to the shared list of unpacked con groups. + /// protected abstract void AddUnpackedCONGroup(UnpackedCONGroup group); + + /// + /// Adds a instance to the shared list of update groups. + /// protected abstract void AddUpdateGroup(UpdateGroup group); + + /// + /// Adds a instance to the shared list of upgrade groups. + /// protected abstract void AddUpgradeGroup(UpgradeGroup group); + + /// + /// Removes all the entries present in all packed and unpacked con groups that have a matching DTA node name + /// protected abstract void RemoveCONEntry(string shortname); - protected abstract bool CanAddUpgrade(string shortname, DateTime lastUpdated); - protected abstract bool CanAddUpgrade_CONInclusive(string shortname, DateTime lastUpdated); - protected abstract Dictionary> MapUpdateFiles(in FileCollection collection); + /// + /// Grabs or constructs a node containing all the updates or upgrades that can applied to any DTA entries + /// that have a name matching the one provided. + /// + /// The name of the DTA node for the entry + /// The node with the update and upgrade information + protected abstract CONModification GetModification(string name); + + /// + /// Performs the traversal of the filesystem in search of new entries to add to a user's library + /// protected abstract void FindNewEntries(); + + /// + /// Splits a collection into the tasks that traverse subdirectories or scan subfiles during the "New Entries" process + /// + /// The collection containing the sub directories and/or files + /// The group aligning to one of the base directories provided by the user + /// A tracker used to apply provide entries with default playlists protected abstract void TraverseDirectory(in FileCollection collection, IniGroup group, PlaylistTracker tracker); + /// + /// Deserializes a cache file into the separate song entries with all validation checks + /// + /// The stream containging the cache file data protected abstract void Deserialize(UnmanagedMemoryStream stream); + + /// + /// Deserializes a cache file into the separate song entries with minimal validations + /// + /// The stream containging the cache file data protected abstract void Deserialize_Quick(UnmanagedMemoryStream stream); + + /// + /// Adds a collection constructed during the full deserialization to a cache for use during + /// the full scan "New Entries" step. This skips the need to re-process a directory's list of files + /// where applicable. + /// protected abstract void AddCollectionToCache(in FileCollection collection); + + /// + /// Returns a CON group if the upgradeCON deserialization step already generated a group with the same filename + /// + /// The file path of the CON to potentially load (if not already) + /// A pre-loaded CON group on success find; otherwise protected abstract PackedCONGroup? FindCONGroup(string filename); - protected abstract void CleanupDuplicates(); + /// + /// Upgrade nodes need to exist before CON entries can be processed. We therefore place all valid upgrades from cache + /// into a list for quick access. + /// + /// The DTA node name for the upgrade + /// The upgrade node to add + protected abstract void AddCacheUpgrade(string name, RBProUpgrade upgrade); + /// + /// Attempts to mark a directory as "processed" + /// + /// The directory to mark + /// if the directory was not previously marked protected virtual bool FindOrMarkDirectory(string directory) { if (!preScannedDirectories.Add(directory)) @@ -228,16 +315,35 @@ protected virtual bool FindOrMarkDirectory(string directory) _progress.NumScannedDirectories++; return true; } + + /// + /// Attempts to mark a file as "processed" + /// + /// The file to mark + /// if the file was not previously marked protected virtual bool FindOrMarkFile(string file) { return preScannedFiles.Add(file); } + + /// + /// Adds an instance of a bad song + /// + /// The file that produced the error + /// The error produced protected virtual void AddToBadSongs(string filePath, ScanResult err) { badSongs.Add(filePath, err); _progress.BadSongCount++; } + /// + /// Attempts to add a new entry to current list. If duplicates are allowed, this will always return true. + /// If they are disallowed, then this will only succeed if the entry is not a duplicate or if it + /// takes precedence over the entry currently in its place (based on a variety of factors) + /// + /// The entry to add + /// Whether the song was accepted into the list protected virtual bool AddEntry(SongEntry entry) { var hash = entry.Hash; @@ -265,11 +371,20 @@ protected virtual bool AddEntry(SongEntry entry) return true; } + /// + /// Marks a CON song with the DTA name as invalid for addition from the cache + /// + /// The DTA name to mark protected virtual void AddInvalidSong(string name) { invalidSongsInCache.Add(name); } + /// + /// Grabs the iniGroup that parents the provided path, if one exists + /// + /// The absolute file path + /// The applicable group if found; otherwise protected IniGroup? GetBaseIniGroup(string path) { foreach (var group in iniGroups) @@ -284,6 +399,64 @@ protected virtual void AddInvalidSong(string name) return null; } + /// + /// Disposes all DTA FixedArray data present in upgrade and update nodes. + /// The songDTA arrays will already have been disposed of before reaching this point. + /// + private void DisposeLeftoverData() + { + foreach (var group in conGroups) + { + group.UpgradeDTAData.Dispose(); + } + + foreach (var group in upgradeGroups) + { + group.DTAData.Dispose(); + } + + foreach (var group in updateGroups) + { + group.DTAData.Dispose(); + } + } + + /// + /// Goes through all the groups that contain song entries to remove specific instances of duplicates + /// + private void CleanupDuplicates() + { + foreach (var entry in duplicatesToRemove) + { + if (!TryRemove(iniGroups, entry) && !TryRemove(conGroups, entry)) + { + TryRemove(extractedConGroups, entry); + } + } + } + + private static bool TryRemove(List groups, SongEntry entry) + where TGroup : ICacheGroup + { + for (int i = 0; i < groups.Count; ++i) + { + var group = groups[i]; + if (group.TryRemoveEntry(entry)) + { + if (group.Count == 0) + { + groups.RemoveAt(i); + } + return true; + } + } + return false; + } + + /// + /// Writes all bad song instances to a badsongs.txt file for the user + /// + /// The path for the file private void WriteBadSongs(string badSongsLocation) { using var stream = new FileStream(badSongsLocation, FileMode.Create, FileAccess.Write); @@ -325,15 +498,9 @@ private void WriteBadSongs(string badSongsLocation) case ScanResult.UnsupportedEncryption: writer.WriteLine("Mogg file uses unsupported encryption"); break; - case ScanResult.MissingMidi: + case ScanResult.MissingCONMidi: writer.WriteLine("Midi file queried for found missing"); break; - case ScanResult.MissingUpdateMidi: - writer.WriteLine("Update Midi file queried for found missing"); - break; - case ScanResult.MissingUpgradeMidi: - writer.WriteLine("Upgrade Midi file queried for found missing"); - break; case ScanResult.IniNotDownloaded: writer.WriteLine("Ini file not fully downloaded - try again once it completes"); break; @@ -362,215 +529,19 @@ private void WriteBadSongs(string badSongsLocation) writer.WriteLine("Loose chart files halted all traversal into the subdirectories at this location."); writer.WriteLine("To fix, if desired, place the loose chart files in a separate dedicated folder."); break; + case ScanResult.InvalidResolution: + writer.WriteLine("This chart uses an invalid resolution (or possibly contains it in an improper format, if .chart)"); + break; + case ScanResult.InvalidResolution_Update: + writer.WriteLine("The midi chart update file applicable with this chart has an invalid resolution of zero"); + break; + case ScanResult.InvalidResolution_Upgrade: + writer.WriteLine("The midi pro guitar upgrade file applicable with this chart has an invalid resolution of zero"); + break; } writer.WriteLine(); } } - - private UpgradeGroup? CreateUpgradeGroup(in FileCollection collection, FileInfo dta, bool removeEntries) - { - var fileData = FixedArray.Null; - YARGTextContainer container; - try - { - fileData = FixedArray.Load(dta.FullName); - if (!YARGDTAReader.TryCreate(fileData, out container)) - { - fileData.Dispose(); - return null; - } - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while loading {dta.FullName}"); - fileData.Dispose(); - return null; - } - - var group = new UpgradeGroup(collection.Directory.FullName, dta.LastWriteTime, fileData); - try - { - while (YARGDTAReader.StartNode(ref container)) - { - string name = YARGDTAReader.GetNameOfNode(ref container, true); - if (collection.Subfiles.TryGetValue($"{name.ToLower()}_plus.mid", out var info) - && CanAddUpgrade(name, info.LastWriteTime)) - { - var abridged = new AbridgedFileInfo(info, false); - var upgrade = new UnpackedRBProUpgrade(abridged); - group.Upgrades[name] = upgrade; - AddUpgrade(name, container, upgrade); - - if (removeEntries) - { - RemoveCONEntry(name); - } - } - YARGDTAReader.EndNode(ref container); - } - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while scanning CON upgrade folder {collection.Directory.FullName}!"); - } - - if (group.Upgrades.Count == 0) - { - YargLogger.LogFormatWarning("{0} .dta file possibly malformed", collection.Directory.FullName); - return null; - } - return group; - } - - private UpdateGroup? CreateUpdateGroup(in FileCollection collection, FileInfo dta, bool removeEntries) - { - var fileData = FixedArray.Null; - YARGTextContainer container; - try - { - fileData = FixedArray.Load(dta.FullName); - if (!YARGDTAReader.TryCreate(fileData, out container)) - { - fileData.Dispose(); - return null; - } - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while loading {dta.FullName}"); - fileData.Dispose(); - return null; - } - - var group = new UpdateGroup(collection.Directory, dta.LastWriteTime, fileData); - try - { - var mapping = MapUpdateFiles(in collection); - while (YARGDTAReader.StartNode(ref container)) - { - string name = YARGDTAReader.GetNameOfNode(ref container, true); - if (!group.Updates.TryGetValue(name, out var update)) - { - AbridgedFileInfo? midi = null; - AbridgedFileInfo? mogg = null; - AbridgedFileInfo? milo = null; - AbridgedFileInfo? image = null; - - string subname = name.ToLowerInvariant(); - if (mapping.TryGetValue(subname, out var files)) - { - if (files.TryGetValue(subname + "_update.mid", out var file)) - { - midi = new AbridgedFileInfo(file, false); - } - if (files.TryGetValue(subname + "_update.mogg", out file)) - { - mogg = new AbridgedFileInfo(file, false); - } - if (files.TryGetValue(subname + ".milo_xbox", out file)) - { - milo = new AbridgedFileInfo(file, false); - } - if (files.TryGetValue(subname + "_keep.png_xbox", out file)) - { - image = new AbridgedFileInfo(file, false); - } - } - - group.Updates.Add(name, update = new SongUpdate(collection.Directory.FullName, midi, mogg, milo, image)); - AddUpdate(name, dta.LastWriteTime, update); - if (removeEntries) - { - RemoveCONEntry(name); - } - } - update.Add(in container); - YARGDTAReader.EndNode(ref container); - } - - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while scanning CON update folder {collection.Directory.FullName}!"); - } - - if (group.Updates.Count == 0) - { - YargLogger.LogFormatWarning("{0} .dta file possibly malformed", collection.Directory.FullName); - fileData.Dispose(); - return null; - } - return group; - } - - private bool TryParseUpgrades(string filename, PackedCONGroup group) - { - if (!group.LoadUpgrades(out var container)) - { - return false; - } - - try - { - while (YARGDTAReader.StartNode(ref container)) - { - string name = YARGDTAReader.GetNameOfNode(ref container, true); - if (group.ConFile.TryGetListing($"songs_upgrades/{name}_plus.mid", out var listing) - && CanAddUpgrade_CONInclusive(name, listing.LastWrite)) - { - var upgrade = new PackedRBProUpgrade(listing, listing.LastWrite); - group.Upgrades[name] = upgrade; - AddUpgrade(name, container, upgrade); - RemoveCONEntry(name); - } - - YARGDTAReader.EndNode(ref container); - } - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error while scanning CON upgrades - {filename}!"); - } - return group.Upgrades.Count > 0; - } - - protected static bool? CanAddUpgrade(List groups, string shortname, DateTime lastUpdated) - where TGroup : IUpgradeGroup - { - foreach (var group in groups) - { - var upgrades = group.Upgrades; - if (upgrades.TryGetValue(shortname, out var currUpgrade)) - { - if (currUpgrade.LastUpdatedTime >= lastUpdated) - { - return false; - } - upgrades.Remove(shortname); - return true; - } - } - return null; - } - - protected static bool TryRemove(List groups, SongEntry entry) - where TGroup : ICacheGroup - where TEntry : SongEntry - { - for (int i = 0; i < groups.Count; ++i) - { - var group = groups[i]; - if (group.TryRemoveEntry(entry)) - { - if (group.Count == 0) - { - groups.RemoveAt(i); - } - return true; - } - } - return false; - } #endregion #region Scanning @@ -578,6 +549,8 @@ protected static bool TryRemove(List groups, SongEntry e protected readonly struct PlaylistTracker { private readonly bool _fullDirectoryFlag; + // We use `null` as the default state to grant two levels of subdirectories before + // supplying directories as the actual playlist (null -> empty -> directory) private readonly string? _playlist; public string Playlist => !string.IsNullOrEmpty(_playlist) ? _playlist : "Unknown Playlist"; @@ -603,6 +576,15 @@ public PlaylistTracker Append(string directory) protected const string SONGUPDATES_DTA = "songs_updates.dta"; protected const string SONGUPGRADES_DTA = "upgrades.dta"; + /// + /// Checks for the presence of files pertaining to an unpacked ini entry or whether the directory + /// is to be used for CON updates, upgrades, or extracted CON song entries. + /// If none of those, this will further traverse through any of the subdirectories present in this directory + /// and process all the subfiles for potential CONs or SNGs. + /// + /// The directory instance to load and scan through + /// The group aligning to one of the base directories provided by the user + /// A tracker used to apply provide entries with default playlists protected void ScanDirectory(DirectoryInfo directory, IniGroup group, PlaylistTracker tracker) { try @@ -612,13 +594,19 @@ protected void ScanDirectory(DirectoryInfo directory, IniGroup group, PlaylistTr return; } + // An update, upgrade, or unpacked con group might've failed during the cache load. + // In certain conditions, the collections that we would otherwise use for those would instead be in this cache if (!collectionCache.TryGetValue(directory.FullName, out var collection)) { collection = new FileCollection(directory); } + // If we discover any combo of valid unpacked ini entry files in this directory, + // we will traverse none of the subdirectories present in this scope if (ScanIniEntry(in collection, group, tracker.Playlist)) { + // However, the presence subdirectories could mean that the user didn't properly + // organize their collection. So as a service, we warn them in the badsongs.txt. if (collection.SubDirectories.Count > 0) { AddToBadSongs(directory.FullName, ScanResult.LooseChart_Warning); @@ -628,16 +616,21 @@ protected void ScanDirectory(DirectoryInfo directory, IniGroup group, PlaylistTr switch (directory.Name) { + // FOR ALL OF THE CASES: a missing dta file means that we will treat the folder like any other subdirectory case "songs_updates": { if (collection.Subfiles.TryGetValue(SONGUPDATES_DTA, out var dta)) { - var updateGroup = CreateUpdateGroup(in collection, dta, true); + var updateGroup = CreateUpdateGroup(in collection, dta); if (updateGroup != null) { - AddUpdateGroup(updateGroup); - return; + // Ensures any con entries pulled from cache are removed for re-evaluation + foreach (var node in updateGroup.Updates) + { + RemoveCONEntry(node.Key); + } } + return; } break; } @@ -645,12 +638,16 @@ protected void ScanDirectory(DirectoryInfo directory, IniGroup group, PlaylistTr { if (collection.Subfiles.TryGetValue(SONGUPGRADES_DTA, out var dta)) { - var upgradeGroup = CreateUpgradeGroup(in collection, dta, true); + var upgradeGroup = CreateUpgradeGroup(in collection, dta); if (upgradeGroup != null) { - AddUpgradeGroup(upgradeGroup); - return; + // Ensures any con entries pulled from cache are removed for re-evaluation + foreach (var node in upgradeGroup.Upgrades) + { + RemoveCONEntry(node.Key); + } } + return; } break; } @@ -658,14 +655,15 @@ protected void ScanDirectory(DirectoryInfo directory, IniGroup group, PlaylistTr { if (collection.Subfiles.TryGetValue(SONGS_DTA, out var dta)) { - var exConGroup = new UnpackedCONGroup(directory.FullName, dta, tracker.Playlist); - AddUnpackedCONGroup(exConGroup); + var _ = CreateUnpackedCONGroup(directory.FullName, dta, tracker.Playlist); return; } break; } } + TraverseDirectory(collection, group, tracker.Append(directory.Name)); + // Only possible on UNIX-based systems where file names are case-sensitive if (collection.ContainedDupes) { AddToBadSongs(collection.Directory.FullName, ScanResult.DuplicateFilesFound); @@ -682,7 +680,13 @@ protected void ScanDirectory(DirectoryInfo directory, IniGroup group, PlaylistTr } } - protected void ScanFile(FileInfo info, IniGroup group, in PlaylistTracker tracker) + /// + /// Attempts to process the provided file as either a CON or SNG + /// + /// The info for provided file + /// The group aligning to one of the base directories provided by the user + /// A tracker used to apply provide entries with default playlists + protected void ScanFile(FileInfo info, IniGroup group, in PlaylistTracker tracker) { string filename = info.FullName; try @@ -706,12 +710,14 @@ protected void ScanFile(FileInfo info, IniGroup group, in PlaylistTracker tracke } else { - var confile = CONFile.TryParseListings(abridged); - if (confile != null) + var conGroup = CreateCONGroup(in abridged, tracker.Playlist); + if (conGroup != null) { - var conGroup = new PackedCONGroup(confile.Value, abridged, tracker.Playlist); - TryParseUpgrades(info.FullName, conGroup); - AddPackedCONGroup(conGroup); + // Ensures any con entries pulled from cache are removed for re-evaluation + foreach (var node in conGroup.Upgrades) + { + RemoveCONEntry(node.Key); + } } } } @@ -727,20 +733,38 @@ protected void ScanFile(FileInfo info, IniGroup group, in PlaylistTracker tracke } } - protected void ScanPackedCONNode(PackedCONGroup group, string name, int index, YARGTextContainer node) + /// + /// A templated helper function used for scanning a new CON entry to the list + /// + /// The group type (shocker) + /// The entry type for the group (again... shocker) + /// The group that contains or will contain the entry + /// The DTA node name for the entry + /// The index for the specific node (for CON packs that contain songs that share the same DTA name FOR SOME FUCKING REASON) + /// The raw byte data for the entry's base DTA information + /// The function used to convert the DTA info and modifications to the desired entry type + protected unsafe void ScanCONNode(TGroup group, string name, int index, in YARGTextContainer node, delegate* func) + where TGroup : CONGroup + where TEntry : RBCONEntry { if (group.TryGetEntry(name, index, out var entry)) { if (!AddEntry(entry!)) + { group.RemoveEntry(name, index); + } } else { - var song = PackedRBCONEntry.ProcessNewEntry(group, name, node, updates, upgrades); + var dtaEntry = new DTAEntry(name, in node); + var modification = GetModification(name); + var song = func(group, name, dtaEntry, modification); if (song.Item2 != null) { if (AddEntry(song.Item2)) + { group.AddEntry(name, index, song.Item2); + } } else { @@ -749,46 +773,88 @@ protected void ScanPackedCONNode(PackedCONGroup group, string name, int index, Y } } - protected void ScanUnpackedCONNode(UnpackedCONGroup group, string name, int index, YARGTextContainer node) + /// + /// Loads the updates and upgrades that apply to con entries that share the same DTA node name + /// + /// The modification node to initialize + /// The DTA name of the node + protected void InitModification(CONModification modification, string name) { - if (group.TryGetEntry(name, index, out var entry)) + // To put the behavior simply: different folders mapping to the same nodes + // for like modification types is no bueno. Only the one with the most recent DTA + // write time will get utilized for entries with the current DTA name. + + var datetime = default(DateTime); + foreach (var group in updateGroups) { - if (!AddEntry(entry!)) - group.RemoveEntry(name, index); + if (group.Updates.TryGetValue(name, out var update)) + { + if (modification.UpdateDTA == null || datetime < group.DTALastWrite) + { + modification.UpdateDTA = new DTAEntry(update.Containers[0].Encoding); + foreach (var container in update.Containers) + { + modification.UpdateDTA.LoadData(name, container); + } + modification.Midi = update.Midi; + modification.Mogg = update.Mogg; + modification.Milo = update.Milo; + modification.Image = update.Image; + datetime = group.DTALastWrite; + } + } } - else + + foreach (var group in upgradeGroups) { - var song = UnpackedRBCONEntry.ProcessNewEntry(group, name, in node, updates, upgrades); - if (song.Item2 != null) + if (group.Upgrades.TryGetValue(name, out var node) && node.Upgrade != null) { - if (AddEntry(song.Item2)) - group.AddEntry(name, index, song.Item2); + if (modification.UpgradeDTA == null || datetime < group.DTALastWrite) + { + modification.UpgradeNode = node.Upgrade; + modification.UpgradeDTA = new DTAEntry(name, in node.Container); + datetime = group.DTALastWrite; + } } - else + } + + foreach (var group in conGroups) + { + if (group.Upgrades.TryGetValue(name, out var node) && node.Upgrade != null) { - AddToBadSongs(group.Location + $" - Node {name}", song.Item1); + if (modification.UpgradeDTA == null || datetime < group.Info.LastUpdatedTime) + { + modification.UpgradeNode = node.Upgrade; + modification.UpgradeDTA = new DTAEntry(name, in node.Container); + datetime = group.Info.LastUpdatedTime; + } } } } - private static readonly (string Name, ChartType Type)[] ChartTypes = - { - ("notes.mid", ChartType.Mid), - ("notes.midi", ChartType.Midi), - ("notes.chart", ChartType.Chart) - }; - + /// + /// Searches for a ".ini" and any .mid or .chart file to possibly extract as a song entry. + /// If found, even if we can't extract an entry from them, we should perform no further directory traversal. + /// + /// The collection containing the subfiles to search from + /// The group aligning to one of the base directories provided by the user + /// The default directory-based playlist to use for any successful entry + /// Whether files pertaining to an unpacked ini entry were discovered private bool ScanIniEntry(in FileCollection collection, IniGroup group, string defaultPlaylist) { int i = collection.Subfiles.TryGetValue("song.ini", out var ini) ? 0 : 2; while (i < 3) { - if (!collection.Subfiles.TryGetValue(ChartTypes[i].Name, out var chart)) + if (!collection.Subfiles.TryGetValue(IniSubEntry.CHART_FILE_TYPES[i].Filename, out var chart)) { ++i; continue; } + // Can't play a song without any audio can you? + // + // Note though that this is purely a pre-add check. + // We will not invalidate an entry from cache if the user removes the audio after the fact. if (!collection.ContainsAudio()) { AddToBadSongs(chart.FullName, ScanResult.NoAudio); @@ -797,8 +863,7 @@ private bool ScanIniEntry(in FileCollection collection, IniGroup group, string d try { - var node = new IniChartNode(chart, ChartTypes[i].Type); - var entry = UnpackedIniEntry.ProcessNewEntry(collection.Directory.FullName, in node, ini, defaultPlaylist); + var entry = UnpackedIniEntry.ProcessNewEntry(collection.Directory.FullName, chart, IniSubEntry.CHART_FILE_TYPES[i].Format, ini, defaultPlaylist); if (entry.Item2 == null) { AddToBadSongs(chart.FullName, entry.Item1); @@ -823,12 +888,18 @@ private bool ScanIniEntry(in FileCollection collection, IniGroup group, string d return false; } + /// + /// Searches for any .mid or .chart file to possibly extract as a song entry. + /// + /// The sngfile to search through + /// The group aligning to one of the base directories provided by the user + /// The default directory-based playlist to use for any successful entry private void ScanSngFile(SngFile sngFile, IniGroup group, string defaultPlaylist) { int i = sngFile.Metadata.Count > 0 ? 0 : 2; while (i < 3) { - if (!sngFile.TryGetValue(ChartTypes[i].Name, out var chart)) + if (!sngFile.TryGetValue(IniSubEntry.CHART_FILE_TYPES[i].Filename, out var chart)) { ++i; continue; @@ -842,8 +913,7 @@ private void ScanSngFile(SngFile sngFile, IniGroup group, string defaultPlaylist try { - var node = new IniChartNode(chart, ChartTypes[i].Type); - var entry = SngEntry.ProcessNewEntry(sngFile, in node, defaultPlaylist); + var entry = SngEntry.ProcessNewEntry(sngFile, chart, IniSubEntry.CHART_FILE_TYPES[i].Format, defaultPlaylist); if (entry.Item2 == null) { AddToBadSongs(sngFile.Info.FullName, entry.Item1); @@ -879,6 +949,13 @@ private void ScanSngFile(SngFile sngFile, IniGroup group, string defaultPlaylist /// private const int MIN_CACHEFILESIZE = 93; + /// + /// Attempts to laod the cache file's data into a FixedArray. This will fail if an error is thrown, + /// the cache is outdated, or if the the "full playlist" toggle mismatches. + /// + /// File location for the cache + /// Toggle for the display style of directory-based playlists + /// A FixedArray instance pointing to a buffer of the cache file's data, or .Null if invalid private static FixedArray LoadCacheToMemory(string cacheLocation, bool fullDirectoryPlaylists) { FileInfo info = new(cacheLocation); @@ -903,22 +980,26 @@ private static FixedArray LoadCacheToMemory(string cacheLocation, bool ful return FixedArray.ReadRemainder(stream); } + /// + /// Serializes the cache to a file, duhhhhhhh + /// + /// Location to save to private void Serialize(string cacheLocation) { - using var writer = new BinaryWriter(new FileStream(cacheLocation, FileMode.Create, FileAccess.Write)); + using var filestream = new FileStream(cacheLocation, FileMode.Create, FileAccess.Write); Dictionary nodes = new(); - writer.Write(CACHE_VERSION); - writer.Write(fullDirectoryPlaylists); + filestream.Write(CACHE_VERSION, Endianness.Little); + filestream.Write(fullDirectoryPlaylists); - CategoryWriter.WriteToCache(writer, cache.Titles, SongAttribute.Name, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Artists, SongAttribute.Artist, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Albums, SongAttribute.Album, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Genres, SongAttribute.Genre, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Years, SongAttribute.Year, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Charters, SongAttribute.Charter, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Playlists, SongAttribute.Playlist, ref nodes); - CategoryWriter.WriteToCache(writer, cache.Sources, SongAttribute.Source, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Titles, SongAttribute.Name, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Artists, SongAttribute.Artist, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Albums, SongAttribute.Album, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Genres, SongAttribute.Genre, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Years, SongAttribute.Year, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Charters, SongAttribute.Charter, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Playlists, SongAttribute.Playlist, ref nodes); + CategoryWriter.WriteToCache(filestream, cache.Sources, SongAttribute.Source, ref nodes); List upgradeCons = new(); List entryCons = new(); @@ -931,16 +1012,25 @@ private void Serialize(string cacheLocation) entryCons.Add(group); } - ICacheGroup.SerializeGroups(iniGroups, writer, nodes); - IModificationGroup.SerializeGroups(updateGroups, writer); - IModificationGroup.SerializeGroups(upgradeGroups, writer); - IModificationGroup.SerializeGroups(upgradeCons, writer); - ICacheGroup.SerializeGroups(entryCons, writer, nodes); - ICacheGroup.SerializeGroups(extractedConGroups, writer, nodes); + ICacheGroup.SerializeGroups(iniGroups, filestream, nodes); + IModificationGroup.SerializeGroups(updateGroups, filestream); + IModificationGroup.SerializeGroups(upgradeGroups, filestream); + IModificationGroup.SerializeGroups(upgradeCons, filestream); + ICacheGroup.SerializeGroups(entryCons, filestream, nodes); + ICacheGroup.SerializeGroups(extractedConGroups, filestream, nodes); } + /// + /// Reads a ini-based entry from the cache, with all validation steps + /// + /// Group mapping to the *user's* base directory + /// String of the base directory *written in the cache* + /// Stream containing the entry data + /// Container of the main metadata arrays protected void ReadIniEntry(IniGroup group, string directory, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { + // An ini entry can be either unpacked (.ini) or packed (.sng). + // This boolean variable in the cache communicates to us which type it is. bool isSngEntry = stream.ReadBoolean(); string fullname = Path.Combine(directory, stream.ReadString()); @@ -948,14 +1038,24 @@ protected void ReadIniEntry(IniGroup group, string directory, UnmanagedMemoryStr ? ReadSngEntry(fullname, stream, strings) : ReadUnpackedIniEntry(fullname, stream, strings); + // If the "duplicates" toggle is set to false, regardless of what's within the cache, + // we will only accept non-duplicates if (entry != null && AddEntry(entry)) { group.AddEntry(entry); } } + /// + /// Reads a ini-based entry from the cache, with very few validation steps + /// + /// String of the base directory *written in the cache* + /// Stream containing the entry data + /// Container of the main metadata arrays protected void QuickReadIniEntry(string directory, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { + // An ini entry can be either unpacked (.ini) or packed (.sng). + // This boolean variable in the cache communicates to us which type it is. bool isSngEntry = stream.ReadBoolean(); string fullname = Path.Combine(directory, stream.ReadString()); @@ -965,6 +1065,8 @@ protected void QuickReadIniEntry(string directory, UnmanagedMemoryStream stream, if (entry != null) { + // If the "duplicates" toggle is set to false, regardless of what's within the cache, + // we will only accept non-duplicates AddEntry(entry); } else @@ -973,6 +1075,12 @@ protected void QuickReadIniEntry(string directory, UnmanagedMemoryStream stream, } } + /// + /// Reads a section of the cache containing a list of updates to apply from a specific directory, + /// performing validations on each update node. If an update node from the cache is invalidated, it will mark + /// any RBCON entry nodes that share its DTA name as invalid, forcing re-evaluation. + /// + /// The stream containing the list of updates protected void ReadUpdateDirectory(UnmanagedMemoryStream stream) { string directory = stream.ReadString(); @@ -994,41 +1102,46 @@ protected void ReadUpdateDirectory(UnmanagedMemoryStream stream) var collection = new FileCollection(dirInfo); if (!collection.Subfiles.TryGetValue(SONGUPDATES_DTA, out var dta)) { + // We don't *mark* the directory to allow the "New Entries" process + // to access this collection collectionCache.Add(directory, collection); goto Invalidate; } - var group = CreateUpdateGroup(collection, dta, false); - if (group == null) - { - goto Invalidate; - } - - AddUpdateGroup(group); FindOrMarkDirectory(directory); - if (group.DTALastWrite != dtaLastWritten) - { - goto Invalidate; - } - - for (int i = 0; i < count; i++) + // Will add the update group to the shared list on success + var group = CreateUpdateGroup(in collection, dta); + if (group != null && group.DTALastWrite == dtaLastWritten) { - string name = stream.ReadString(); - if (group.Updates.TryGetValue(name, out var update)) + // We need to compare what we have on the filesystem against what's written one by one + var updates = new Dictionary(group.Updates); + for (int i = 0; i < count; i++) { - if (!update.Validate(stream)) + string name = stream.ReadString(); + // `Remove` returns true if the node was present + if (updates.Remove(name, out var update)) + { + // Validates midi, mogg, image, and milo write dates + if (!update.Validate(stream)) + { + AddInvalidSong(name); + } + } + else { AddInvalidSong(name); + SongUpdate.SkipRead(stream); } } - else + + // Anything left in the dictionary may require invalidation of cached entries + foreach (var leftover in updates.Keys) { - AddInvalidSong(name); - SongUpdate.SkipRead(stream); + AddInvalidSong(leftover); } + return; } - return; Invalidate: for (int i = 0; i < count; i++) @@ -1038,6 +1151,12 @@ protected void ReadUpdateDirectory(UnmanagedMemoryStream stream) } } + /// + /// Reads a section of the cache containing a list of upgrades to apply from a specific directory, + /// performing validations on each upgrade node. If an upgrade node from the cache is invalidated, it will mark + /// any RBCON entry nodes that share its DTA name as invalid, forcing re-evaluation. + /// + /// The stream containing the list of upgrades protected void ReadUpgradeDirectory(UnmanagedMemoryStream stream) { string directory = stream.ReadString(); @@ -1059,32 +1178,21 @@ protected void ReadUpgradeDirectory(UnmanagedMemoryStream stream) var collection = new FileCollection(dirInfo); if (!collection.Subfiles.TryGetValue(SONGUPGRADES_DTA, out var dta)) { + // We don't *mark* the directory to allow the "New Entries" process + // to access this collection collectionCache.Add(directory, collection); goto Invalidate; } FindOrMarkDirectory(directory); - var group = CreateUpgradeGroup(in collection, dta, false); - if (group == null) - { - goto Invalidate; - } - - AddUpgradeGroup(group); - if (dta.LastWriteTime != dtaLastWritten) - { - goto Invalidate; - } - - for (int i = 0; i < count; i++) + // Will add the upgrade group to the shared list on success + var group = CreateUpgradeGroup(in collection, dta); + if (group != null && dta.LastWriteTime == dtaLastWritten) { - string name = stream.ReadString(); - var lastUpdated = DateTime.FromBinary(stream.Read(Endianness.Little)); - if (!group.Upgrades.TryGetValue(name, out var upgrade) || upgrade!.LastUpdatedTime != lastUpdated) - AddInvalidSong(name); + ValidateUpgrades(group.Upgrades, count, stream); + return; } - return; Invalidate: for (int i = 0; i < count; i++) @@ -1094,42 +1202,30 @@ protected void ReadUpgradeDirectory(UnmanagedMemoryStream stream) } } + /// + /// Reads a section of the cache containing a list of upgrades to apply from a packed CON file, + /// performing validations on each upgrade node. If an upgrade node from the cache is invalidated, it will mark + /// any RBCON entry nodes that share its DTA name as invalid, forcing re-evaluation. + /// + /// The stream containing the list of upgrades protected void ReadUpgradeCON(UnmanagedMemoryStream stream) { string filename = stream.ReadString(); var conLastUpdated = DateTime.FromBinary(stream.Read(Endianness.Little)); - var dtaLastWritten = DateTime.FromBinary(stream.Read(Endianness.Little)); int count = stream.Read(Endianness.Little); var baseGroup = GetBaseIniGroup(filename); - if (baseGroup != null) + if (baseGroup == null) { - // Make playlist as the group is only made once - string playlist = ConstructPlaylist(filename, baseGroup.Directory); - var group = CreateCONGroup(filename, playlist); - if (group == null) - { - goto Invalidate; - } - - AddPackedCONGroup(group); + goto Invalidate; + } - if (TryParseUpgrades(filename, group) && group.UpgradeDta!.LastWrite == dtaLastWritten) - { - if (group.Info.LastUpdatedTime != conLastUpdated) - { - for (int i = 0; i < count; i++) - { - string name = stream.ReadString(); - var lastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); - if (group.Upgrades[name].LastUpdatedTime != lastWrite) - { - AddInvalidSong(name); - } - } - } - return; - } + // Will add the packed CON group to the shared list on success + var group = CreateCONGroup(filename, baseGroup.Directory); + if (group != null && group.Info.LastUpdatedTime == conLastUpdated) + { + ValidateUpgrades(group.Upgrades, count, stream); + return; } Invalidate: @@ -1140,46 +1236,79 @@ protected void ReadUpgradeCON(UnmanagedMemoryStream stream) } } - protected PackedCONGroup? ReadCONGroupHeader(UnmanagedMemoryStream stream) + /// + /// Helper function that runs validation on all the upgrade nodes present within either an upgrade directory or + /// upgrade CON section of a cache file. + /// + /// Type of upgrade node (packed or unpacked) + /// Dictionary containing the upgrade nodes freshly loaded from the source directory or CON + /// The number of upgrade nodes present in the cache file + /// Stream containing the upgrade node entries + private void ValidateUpgrades(Dictionary Container, TUpgrade Upgrade)> groupUpgrades, int count, UnmanagedMemoryStream stream) + where TUpgrade : RBProUpgrade { - string filename = stream.ReadString(); - var baseGroup = GetBaseIniGroup(filename); - if (baseGroup == null) + // All we need to compare are the last update times + var upgrades = new Dictionary(); + upgrades.EnsureCapacity(groupUpgrades.Count); + for (int i = 0; i < count; i++) { - return null; + string name = stream.ReadString(); + var lastUpdated = DateTime.FromBinary(stream.Read(Endianness.Little)); + upgrades.Add(name, lastUpdated); } - var dtaLastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); - var group = FindCONGroup(filename); - if (group == null) + foreach (var node in groupUpgrades) { - var info = new FileInfo(filename); - if (!info.Exists) + // `Remove` returns true if the node was present + if (upgrades.Remove(node.Key, out var dateTime) && node.Value.Upgrade.LastUpdatedTime == dateTime) { - return null; + // Upgrade nodes need to exist before adding CON entries, so we must have a separate list for + // all upgrade nodes processed from cache + AddCacheUpgrade(node.Key, node.Value.Upgrade); } - - FindOrMarkFile(filename); - - var abridged = new AbridgedFileInfo(info); - var confile = CONFile.TryParseListings(abridged); - if (confile == null) + else { - return null; + AddInvalidSong(node.Key); } + } - string playlist = ConstructPlaylist(filename, baseGroup.Directory); - group = new PackedCONGroup(confile.Value, abridged, playlist); - AddPackedCONGroup(group); + // Anything left in the dictionary may require invalidation of cached entries + foreach (var leftover in upgrades.Keys) + { + AddInvalidSong(leftover); } + } - if (group.SongDTA == null || group.SongDTA.LastWrite != dtaLastWrite) + /// + /// Attempts to load a PackedCONGroup instance from the validation data present in the cache. + /// Even if a group is generated however, if the last-update time in the cache does not match what we + /// receive from the filesystem, this function will not return that instance. Therefore, the caller can not parse + /// any of the accompanying CON entries that follow the validation data. + /// + /// Stream containing the CON validation information + /// The packed CON group on success; otherwise + protected PackedCONGroup? ReadCONGroupHeader(UnmanagedMemoryStream stream) + { + string filename = stream.ReadString(); + var baseGroup = GetBaseIniGroup(filename); + if (baseGroup == null) { return null; } - return group; + + var conLastUpdate = DateTime.FromBinary(stream.Read(Endianness.Little)); + var group = FindCONGroup(filename) ?? CreateCONGroup(filename, baseGroup.Directory); + return group != null && group.Info.LastUpdatedTime == conLastUpdate ? group : null; } + /// + /// Attempts to load a UnpackedCONGroup instance from the validation data present in the cache. + /// Even if a group is generated however, if the last-update time of the songs.dta file in the cache does not match what we + /// receive from the filesystem, this function will not return that instance. Therefore, the caller can not parse + /// any of the accompanying CON entries that follow the validation data. + /// + /// Stream containing the song.dta validation information + /// The unpacked CON group on success; otherwise protected UnpackedCONGroup? ReadExtractedCONGroupHeader(UnmanagedMemoryStream stream) { string directory = stream.ReadString(); @@ -1198,39 +1327,24 @@ protected void ReadUpgradeCON(UnmanagedMemoryStream stream) FindOrMarkDirectory(directory); string playlist = ConstructPlaylist(directory, baseGroup.Directory); - var group = new UnpackedCONGroup(directory, dtaInfo, playlist); - AddUnpackedCONGroup(group); - - if (dtaInfo.LastWriteTime != DateTime.FromBinary(stream.Read(Endianness.Little))) + var group = CreateUnpackedCONGroup(directory, dtaInfo, playlist); + if (group == null) { return null; } - return group; - } - protected void ReadCONGroup(UnmanagedMemoryStream stream, Action func) - { - int count = stream.Read(Endianness.Little); - for (int i = 0; i < count; ++i) - { - string name = stream.ReadString(); - int index = stream.Read(Endianness.Little); - int length = stream.Read(Endianness.Little); - if (invalidSongsInCache.Contains(name)) - { - stream.Position += length; - continue; - } - - var entryReader = stream.Slice(length); - func(name, index, entryReader); - } + var dtaLastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); + return dtaInfo.LastWriteTime == dtaLastWrite ? group : null; } + /// + /// Loads all the upgrade nodes present in the cache from an "upgrades folder" section + /// + /// Stream containing the data for a folder's upgrade nodes protected void QuickReadUpgradeDirectory(UnmanagedMemoryStream stream) { string directory = stream.ReadString(); - var dtaLastUpdated = DateTime.FromBinary(stream.Read(Endianness.Little)); + stream.Position += sizeof(long); // Can skip the last update time int count = stream.Read(Endianness.Little); for (int i = 0; i < count; i++) @@ -1239,58 +1353,60 @@ protected void QuickReadUpgradeDirectory(UnmanagedMemoryStream stream) string filename = Path.Combine(directory, $"{name}_plus.mid"); var info = new AbridgedFileInfo(filename, stream); - AddUpgrade(name, default, new UnpackedRBProUpgrade(info)); + // Upgrade nodes need to exist before adding CON entries, so we must have a separate list for + // all upgrade nodes processed from cache + AddCacheUpgrade(name, new UnpackedRBProUpgrade(info)); } } + /// + /// Loads all the upgrade nodes present in the cache from an "upgrade CON" section. + /// + /// Stream containing the data for a CON's upgrade nodes protected void QuickReadUpgradeCON(UnmanagedMemoryStream stream) { - string filename = stream.ReadString(); - stream.Position += 2 * SIZEOF_DATETIME; + var listings = QuickReadCONGroupHeader(stream); int count = stream.Read(Endianness.Little); - - var group = CreateCONGroup(filename, string.Empty); - if (group != null) - { - AddPackedCONGroup(group); - } - for (int i = 0; i < count; i++) { string name = stream.ReadString(); var lastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); var listing = default(CONFileListing); - group?.ConFile.TryGetListing($"songs_upgrades/{name}_plus.mid", out listing); - - var upgrade = new PackedRBProUpgrade(listing, lastWrite); - AddUpgrade(name, default, upgrade); + listings?.TryGetListing($"songs_upgrades/{name}_plus.mid", out listing); + // Upgrade nodes need to exist before adding CON entries, so we must have a separate list for + // all upgrade nodes processed from cache + AddCacheUpgrade(name, new PackedRBProUpgrade(listing, lastWrite)); } } - protected PackedCONGroup? QuickReadCONGroupHeader(UnmanagedMemoryStream stream) + /// + /// Attempts to load a list of CONFileListings from a CON file off the filesystem. + /// The listings that get loaded will utilize the last write information from the cache file, + /// rather than the last write info off the filesystem. + /// + /// Stream containing the CON validation information + /// A list of CONFileListings on success; otherwise + protected List? QuickReadCONGroupHeader(UnmanagedMemoryStream stream) { string filename = stream.ReadString(); - var dtaLastWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); - - var group = FindCONGroup(filename); - if (group == null) - { - group = CreateCONGroup(filename, string.Empty); - if (group == null) - { - return null; - } - - AddPackedCONGroup(group); - } - - if (group.SongDTA == null || group.SongDTA.LastWrite != dtaLastWrite) + var info = new AbridgedFileInfo(filename, stream); + if (!File.Exists(filename)) { return null; } - return group; + + using var filestream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, 1); + return CONFile.TryParseListings(in info, filestream); } + /// + /// Attempts to read an unpacked .ini entry from the cache - with all validation checks. + /// If successful, we mark the directory as "already processed" so that the "New Entries" step doesn't attempt to access it. + /// + /// The directory potentially containing an entry + /// The stream containing the entry's information + /// Container will the basic metadata arrays + /// An unpacked ini entry on success; otherwise private UnpackedIniEntry? ReadUnpackedIniEntry(string directory, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var entry = UnpackedIniEntry.TryLoadFromCache(directory, stream, strings); @@ -1298,11 +1414,18 @@ protected void QuickReadUpgradeCON(UnmanagedMemoryStream stream) { return null; } - FindOrMarkDirectory(directory); return entry; } + /// + /// Attempts to read an unpacked .sng entry from the cache - with all validation checks. + /// If successful, we mark the file as "already processed" so that the "New Entries" step doesn't attempt to access it. + /// + /// The file path to a potential entry + /// The stream containing the entry's information + /// Container will the basic metadata arrays + /// An packed sng entry on success; otherwise private SngEntry? ReadSngEntry(string fullname, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var entry = SngEntry.TryLoadFromCache(fullname, stream, strings); @@ -1314,23 +1437,275 @@ protected void QuickReadUpgradeCON(UnmanagedMemoryStream stream) return entry; } - private PackedCONGroup? CreateCONGroup(string filename, string defaultPlaylist) + /// + /// Creates an UpdateGroup... self-explanatory + /// + /// The collection of subdirectories and subfiles to locate updates from + /// The file info for the main DTA + /// An UpdateGroup instance on success; otherwise + private UpdateGroup? CreateUpdateGroup(in FileCollection collection, FileInfo dta) + { + UpdateGroup? group = null; + try + { + // We call `using` to ensure the proper disposal of data if an error occurs + using var data = FixedArray.Load(dta.FullName); + var updates = new Dictionary(); + var container = YARGDTAReader.TryCreate(data); + while (YARGDTAReader.StartNode(ref container)) + { + string name = YARGDTAReader.GetNameOfNode(ref container, true); + if (!updates.TryGetValue(name, out var update)) + { + // We only need to check for the files one time per-DTA name + AbridgedFileInfo? midi = null; + AbridgedFileInfo? mogg = null; + AbridgedFileInfo? milo = null; + AbridgedFileInfo? image = null; + + string subname = name.ToLowerInvariant(); + if (collection.SubDirectories.TryGetValue(subname, out var directory)) + { + string midiName = subname + "_update.mid"; + string moggName = subname + "_update.mogg"; + string miloName = subname + ".milo_xbox"; + string imageName = subname + "_keep.png_xbox"; + // Enumerating through the available files through the DirectoryInfo instance + // provides a speed boost over manual `File.Exists` checks + foreach (var file in directory.EnumerateFiles("*", SearchOption.AllDirectories)) + { + string filename = file.Name; + if (filename == midiName) + { + midi = new AbridgedFileInfo(file, false); + } + else if (filename == moggName) + { + mogg = new AbridgedFileInfo(file, false); + } + else if (filename == miloName) + { + milo = new AbridgedFileInfo(file, false); + } + else if (filename == imageName) + { + image = new AbridgedFileInfo(file, false); + } + } + } + updates.Add(name, update = new SongUpdate(in midi, in mogg, in milo, in image)); + } + // Updates may contain multiple entries for the same DTA name, so we must collect them all under + // the same node. However, we won't actually load the information unless later stages require it. + update.Containers.Add(container); + YARGDTAReader.EndNode(ref container); + } + + if (updates.Count > 0) + { + // We transfer ownership of the FixedArray data to give the group responsibility over the disposal. + // Otherwise, the `using` call would dispose of the data after the scope of this function. + group = new UpdateGroup(collection.Directory.FullName, dta.LastWriteTime, data.TransferOwnership(), updates); + AddUpdateGroup(group); + } + } + catch (Exception ex) + { + YargLogger.LogException(ex, $"Error while loading {dta.FullName}"); + } + return group; + } + + /// + /// Creates an UpgradeGroup... self-explanatory + /// + /// The collection of subdirectories and subfiles to locate upgrades from + /// The file info for the main DTA + /// An UpgradeGroup instance on success; otherwise + private UpgradeGroup? CreateUpgradeGroup(in FileCollection collection, FileInfo dta) + { + UpgradeGroup? group = null; + try + { + // We call `using` to ensure the proper disposal of data if an error occurs + using var data = FixedArray.Load(dta.FullName); + var upgrades = new Dictionary Container, UnpackedRBProUpgrade Upgrade)>(); + var container = YARGDTAReader.TryCreate(data); + while (YARGDTAReader.StartNode(ref container)) + { + string name = YARGDTAReader.GetNameOfNode(ref container, true); + var upgrade = default(UnpackedRBProUpgrade); + // If there is no upgrade file accompanying the DTA node, there's no point in adding the upgrade + if (collection.Subfiles.TryGetValue($"{name.ToLower()}_plus.mid", out var info)) + { + var abridged = new AbridgedFileInfo(info, false); + upgrade = new UnpackedRBProUpgrade(abridged); + upgrades[name] = (container, upgrade); + } + YARGDTAReader.EndNode(ref container); + } + + if (upgrades.Count > 0) + { + // We transfer ownership of the FixedArray data to give the group responsibility over the disposal. + // Otherwise, the `using` call would dispose of the data after the scope of this function. + group = new UpgradeGroup(collection.Directory.FullName, dta.LastWriteTime, data.TransferOwnership(), upgrades); + AddUpgradeGroup(group); + } + } + catch (Exception ex) + { + YargLogger.LogException(ex, $"Error while loading {dta.FullName}"); + } + return group; + } + + /// + /// Attempts to create a PackedCONGroup from the file at the provided path. + /// + /// The path for the file + /// One of the base directories provided by the user + /// A PackedCONGroup instance on success; otherwise + private PackedCONGroup? CreateCONGroup(string filename, string baseDirectory) { var info = new FileInfo(filename); if (!info.Exists) + { return null; + } FindOrMarkFile(filename); + string playlist = ConstructPlaylist(filename, baseDirectory); var abridged = new AbridgedFileInfo(info); - var confile = CONFile.TryParseListings(abridged); - if (confile == null) + return CreateCONGroup(in abridged, playlist); + } + + /// + /// Attempts to create a PackedCONGroup with the provided fileinfo. + /// + /// The file info for the possible CONFile + /// The playlist to use for any entries generated from the CON (if it is one) + /// A PackedCONGroup instance on success; otherwise + private PackedCONGroup? CreateCONGroup(in AbridgedFileInfo info, string defaultPlaylist) + { + const string SONGSFILEPATH = "songs/songs.dta"; + const string UPGRADESFILEPATH = "songs_upgrades/upgrades.dta"; + PackedCONGroup? group = null; + // Holds the file that caused an error in some form + string errorFile = string.Empty; + try { - return null; + using var stream = new FileStream(info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); + var listings = CONFile.TryParseListings(in info, stream); + if (listings == null) + { + return null; + } + + var songNodes = new Dictionary>>(); + // We call `using` to ensure the proper disposal of data if an error occurs + using var songDTAData = listings.TryGetListing(SONGSFILEPATH, out var songDTA) ? songDTA.LoadAllBytes(stream) : FixedArray.Null; + if (songDTAData.IsAllocated) + { + errorFile = SONGSFILEPATH; + var container = YARGDTAReader.TryCreate(songDTAData); + while (YARGDTAReader.StartNode(ref container)) + { + string name = YARGDTAReader.GetNameOfNode(ref container, true); + if (!songNodes.TryGetValue(name, out var list)) + { + songNodes.Add(name, list = new List>()); + } + list.Add(container); + YARGDTAReader.EndNode(ref container); + } + } + + var upgrades = new Dictionary Container, PackedRBProUpgrade Upgrade)>(); + // We call `using` to ensure the proper disposal of data if an error occurs + using var upgradeDTAData = listings.TryGetListing(UPGRADESFILEPATH, out var upgradeDta) ? upgradeDta.LoadAllBytes(stream) : FixedArray.Null; + if (upgradeDTAData.IsAllocated) + { + errorFile = UPGRADESFILEPATH; + var container = YARGDTAReader.TryCreate(upgradeDTAData); + while (YARGDTAReader.StartNode(ref container)) + { + string name = YARGDTAReader.GetNameOfNode(ref container, true); + if (listings.TryGetListing($"songs_upgrades/{name}_plus.mid", out var listing)) + { + var upgrade = new PackedRBProUpgrade(listing, listing.LastWrite); + upgrades[name] = (container, upgrade); + } + YARGDTAReader.EndNode(ref container); + } + } + + if (songNodes.Count > 0 || upgrades.Count > 0) + { + // We transfer ownership of the FixedArray data to give the group responsibility over the disposal. + // Otherwise, the `using` calls would dispose of the data after the scope of this function. + group = new PackedCONGroup(listings, songDTAData.TransferOwnership(), upgradeDTAData.TransferOwnership(), songNodes, upgrades, in info, defaultPlaylist); + AddPackedCONGroup(group); + } + } + catch (Exception ex) + { + YargLogger.LogException(ex, $"Error while loading {errorFile}"); } - return new PackedCONGroup(confile.Value, abridged, defaultPlaylist); + return group; } + /// + /// Attempts to create an UnpackedCONGroup from the file at the provided path. + /// + /// The directory containing the list of entry subdirectories and the main DTA + /// The info for the main DTA file + /// The playlist to use for any entries generated from the CON (if it is one) + /// An UnpackedCONGroup instance on success; otherwise + private UnpackedCONGroup? CreateUnpackedCONGroup(string directory, FileInfo dta, string defaultPlaylist) + { + try + { + using var songDTAData = FixedArray.Load(dta.FullName); + + var songNodes = new Dictionary>>(); + if (songDTAData.IsAllocated) + { + var container = YARGDTAReader.TryCreate(songDTAData); + while (YARGDTAReader.StartNode(ref container)) + { + string name = YARGDTAReader.GetNameOfNode(ref container, true); + if (!songNodes.TryGetValue(name, out var list)) + { + songNodes.Add(name, list = new List>()); + } + list.Add(container); + YARGDTAReader.EndNode(ref container); + } + } + + if (songNodes.Count > 0) + { + var abridged = new AbridgedFileInfo(dta); + var group = new UnpackedCONGroup(songDTAData.TransferOwnership(), songNodes, directory, in abridged, defaultPlaylist); + AddUnpackedCONGroup(group); + return group; + } + } + catch (Exception ex) + { + YargLogger.LogException(ex, $"Error while loading {dta.FullName}"); + } + return null; + } + + /// + /// Constructs a directory-based playlist based on the provided file name + /// + /// The path for the current file + /// One of the base directories provided by the user + /// The default playlist to potentially use private string ConstructPlaylist(string filename, string baseDirectory) { string directory = Path.GetDirectoryName(filename); @@ -1350,8 +1725,20 @@ private string ConstructPlaylist(string filename, string baseDirectory) internal static class UnmanagedStreamSlicer { + /// + /// Splice a section of the unmanaged stream into a separate instance. + /// Useful for parallelization and for potentially catching "end of stream" errors. + /// + /// The base stream to slice + /// The amount of the data to slice out from the base stream + /// The new stream containing the slice public static unsafe UnmanagedMemoryStream Slice(this UnmanagedMemoryStream stream, int length) { + if (stream.Position > stream.Length - length) + { + throw new EndOfStreamException(); + } + var newStream = new UnmanagedMemoryStream(stream.PositionPointer, length); stream.Position += length; return newStream; diff --git a/YARG.Core/Song/Cache/CacheNodes.cs b/YARG.Core/Song/Cache/CacheNodes.cs index e0b816d85..03036061f 100644 --- a/YARG.Core/Song/Cache/CacheNodes.cs +++ b/YARG.Core/Song/Cache/CacheNodes.cs @@ -2,7 +2,6 @@ using System.IO; using System.Threading.Tasks; using YARG.Core.Extensions; -using YARG.Core.IO; namespace YARG.Core.Song.Cache { diff --git a/YARG.Core/Song/Cache/FileCollection.cs b/YARG.Core/Song/Cache/FileCollection.cs index 02e61b63e..ccc32a2df 100644 --- a/YARG.Core/Song/Cache/FileCollection.cs +++ b/YARG.Core/Song/Cache/FileCollection.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using System.Text; namespace YARG.Core.Song.Cache { diff --git a/YARG.Core/Song/Cache/SongCategories.cs b/YARG.Core/Song/Cache/SongCategories.cs index 24eccc78e..784046344 100644 --- a/YARG.Core/Song/Cache/SongCategories.cs +++ b/YARG.Core/Song/Cache/SongCategories.cs @@ -1,7 +1,7 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; +using YARG.Core.Extensions; namespace YARG.Core.Song.Cache { @@ -74,7 +74,7 @@ public string GetKey(SongEntry entry) public EntryComparer Comparer => _COMPARER; public DateTime GetKey(SongEntry entry) { - return entry.GetAddTime().Date; + return entry.GetAddDate(); } } @@ -190,7 +190,7 @@ public static void Add(SongEntry entry, SortedDictionary(BinaryWriter fileWriter, SortedDictionary> sections, SongAttribute attribute, ref Dictionary nodes) + public static void WriteToCache(FileStream filestream, SortedDictionary> sections, SongAttribute attribute, ref Dictionary nodes) { List strings = new(); foreach (var element in sections) @@ -238,13 +238,14 @@ public static void WriteToCache(BinaryWriter fileWriter, SortedDictionary< } using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - writer.Write(strings.Count); + ms.Write(strings.Count, Endianness.Little); foreach (string str in strings) - writer.Write(str); + { + ms.Write(str); + } - fileWriter.Write((int) ms.Length); - ms.WriteTo(fileWriter.BaseStream); + filestream.Write((int) ms.Length, Endianness.Little); + filestream.Write(ms.GetBuffer(), 0, (int)ms.Length); } } } diff --git a/YARG.Core/Song/Entries/AvailableParts/AvailableParts.cs b/YARG.Core/Song/Entries/AvailableParts/AvailableParts.cs index 6f009c5e2..1a7b30bbe 100644 --- a/YARG.Core/Song/Entries/AvailableParts/AvailableParts.cs +++ b/YARG.Core/Song/Entries/AvailableParts/AvailableParts.cs @@ -1,10 +1,4 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using YARG.Core.Chart; -using YARG.Core.IO; -using YARG.Core.Song.Preparsers; namespace YARG.Core.Song { diff --git a/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs b/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs index dfb19fddc..251bdf5d2 100644 --- a/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs +++ b/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs @@ -1,6 +1,4 @@ -using Melanchall.DryWetMidi.Core; -using MoonscraperChartEditor.Song.IO; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -13,18 +11,6 @@ namespace YARG.Core.Song { - public readonly struct IniChartNode - { - public readonly T File; - public readonly ChartType Type; - - public IniChartNode(T file, ChartType type) - { - File = file; - Type = type; - } - } - public static class IniAudio { public static readonly string[] SupportedStems = { "song", "guitar", "bass", "rhythm", "keys", "vocals", "vocals_1", "vocals_2", "drums", "drums_1", "drums_2", "drums_3", "drums_4", "crowd", }; @@ -46,11 +32,11 @@ public static bool IsAudioFile(string file) public abstract class IniSubEntry : SongEntry { - public static readonly IniChartNode[] CHART_FILE_TYPES = + public static readonly (string Filename, ChartFormat Format)[] CHART_FILE_TYPES = { - new("notes.mid" , ChartType.Mid), - new("notes.midi" , ChartType.Midi), - new("notes.chart", ChartType.Chart), + ("notes.mid" , ChartFormat.Mid), + ("notes.midi" , ChartFormat.Midi), + ("notes.chart", ChartFormat.Chart), }; protected static readonly string[] ALBUMART_FILES; @@ -65,90 +51,64 @@ static IniSubEntry() } PREVIEW_FILES = new string[IniAudio.SupportedFormats.Length]; - for (int i = 0; i < PREVIEW_FILES.Length; i++ ) + for (int i = 0; i < PREVIEW_FILES.Length; i++) { PREVIEW_FILES[i] = "preview" + IniAudio.SupportedFormats[i]; } } - protected readonly string _background; - protected readonly string _video; - protected readonly string _cover; - public readonly bool Video_Loop; - - public abstract ChartType Type { get; } + public readonly string Background; + public readonly string Video; + public readonly string Cover; + + public override string Year { get; } + public override int YearAsNumber { get; } + public override bool LoopVideo { get; } - protected IniSubEntry(in AvailableParts parts, in HashWrapper hash, IniSection modifiers, string defaultPlaylist) - : base(in parts, in hash, modifiers, defaultPlaylist) + protected IniSubEntry(in SongMetadata metadata, in AvailableParts parts, in HashWrapper hash, in LoaderSettings settings, IniSection modifiers) + : base(in metadata, in parts, in hash, in settings) { - if (modifiers.TryGet("background", out _background)) + (Year, YearAsNumber) = ParseYear(Metadata.Year); + if (modifiers.TryGet("background", out Background)) { - string ext = Path.GetExtension(_background.Trim('\"')).ToLower(); - _background = IMAGE_EXTENSIONS.Contains(ext) ? _background.ToLowerInvariant() : string.Empty; + string ext = Path.GetExtension(Background.Trim('\"')).ToLower(); + Background = IMAGE_EXTENSIONS.Contains(ext) ? Background.ToLowerInvariant() : string.Empty; } - if (modifiers.TryGet("video", out _video)) + if (modifiers.TryGet("video", out Video)) { - string ext = Path.GetExtension(_video.Trim('\"')).ToLower(); - _video = VIDEO_EXTENSIONS.Contains(ext) ? _video.ToLowerInvariant() : string.Empty; + string ext = Path.GetExtension(Video.Trim('\"')).ToLower(); + Video = VIDEO_EXTENSIONS.Contains(ext) ? Video.ToLowerInvariant() : string.Empty; } - if (modifiers.TryGet("cover", out _cover)) + if (modifiers.TryGet("cover", out Cover)) { - string ext = Path.GetExtension(_cover.Trim('\"')).ToLower(); - _cover = IMAGE_EXTENSIONS.Contains(ext) ? _cover.ToLowerInvariant() : string.Empty; + string ext = Path.GetExtension(Cover.Trim('\"')).ToLower(); + Cover = IMAGE_EXTENSIONS.Contains(ext) ? Cover.ToLowerInvariant() : string.Empty; } - modifiers.TryGet("video_loop", out Video_Loop); + LoopVideo = modifiers.TryGet("video_loop", out bool loop) && loop; } protected IniSubEntry(UnmanagedMemoryStream stream, CategoryCacheStrings strings) : base(stream, strings) { - _background = stream.ReadString(); - _video = stream.ReadString(); - _cover = stream.ReadString(); - Video_Loop = stream.ReadBoolean(); - } - - protected abstract Stream? GetChartStream(); - - protected abstract void SerializeSubData(BinaryWriter writer); + YearAsNumber = stream.Read(Endianness.Little); + Background = stream.ReadString(); + Video = stream.ReadString(); + Cover = stream.ReadString(); + LoopVideo = stream.ReadBoolean(); - public ReadOnlySpan Serialize(CategoryCacheWriteNode node, string groupDirectory) - { - string relativePath = Path.GetRelativePath(groupDirectory, Location); - if (relativePath == ".") - relativePath = string.Empty; - - using MemoryStream ms = new(); - using BinaryWriter writer = new(ms); - - writer.Write(SubType == EntryType.Sng); - writer.Write(relativePath); - - SerializeSubData(writer); - SerializeMetadata(writer, node); - - writer.Write(_background); - writer.Write(_video); - writer.Write(_cover); - writer.Write(Video_Loop); - return new ReadOnlySpan(ms.GetBuffer(), 0, (int)ms.Length); + Year = YearAsNumber != int.MaxValue ? YearAsNumber.ToString() : Metadata.Year; } - public override SongChart? LoadChart() + public override void Serialize(MemoryStream stream, CategoryCacheWriteNode node) { - using var stream = GetChartStream(); - if (stream == null) - return null; - - if (Type != ChartType.Chart) - { - return SongChart.FromMidi(_parseSettings, MidFileLoader.LoadMidiFile(stream)); - } - - using var reader = new StreamReader(stream); - return SongChart.FromDotChart(_parseSettings, reader.ReadToEnd()); + base.Serialize(stream, node); + stream.Write(YearAsNumber, Endianness.Little); + stream.Write(Background); + stream.Write(Video); + stream.Write(Cover); + stream.Write(LoopVideo); } public override FixedArray LoadMiloData() @@ -156,7 +116,7 @@ public override FixedArray LoadMiloData() return FixedArray.Null; } - protected static (ScanResult Result, AvailableParts Parts) ScanIniChartFile(in FixedArray file, ChartType chartType, IniSection modifiers) + protected static (ScanResult Result, AvailableParts Parts, LoaderSettings Settings) ProcessChartFile(in FixedArray file, ChartFormat format, IniSection modifiers) { DrumPreparseHandler drums = new() { @@ -164,52 +124,199 @@ protected static (ScanResult Result, AvailableParts Parts) ScanIniChartFile(in F }; var parts = AvailableParts.Default; - if (chartType == ChartType.Chart) + var settings = default(LoaderSettings); + var results = default((ScanResult result, long resolution)); + if (format == ChartFormat.Chart) { if (YARGTextReader.IsUTF8(in file, out var byteContainer)) { - ParseDotChart(ref byteContainer, modifiers, ref parts, drums); + results = ParseDotChart(ref byteContainer, modifiers, ref parts, drums); } else { using var chars = YARGTextReader.ConvertToUTF16(in file, out var charContainer); if (chars.IsAllocated) { - ParseDotChart(ref charContainer, modifiers, ref parts, drums); + results = ParseDotChart(ref charContainer, modifiers, ref parts, drums); } else { using var ints = YARGTextReader.ConvertToUTF32(in file, out var intContainer); - ParseDotChart(ref intContainer, modifiers, ref parts, drums); + results = ParseDotChart(ref intContainer, modifiers, ref parts, drums); } } } else // if (chartType == ChartType.Mid || chartType == ChartType.Midi) // Uncomment for any future file type { - if (!ParseDotMidi(in file, modifiers, ref parts, drums)) - { - return (ScanResult.MultipleMidiTrackNames, parts); - } + results = ParseDotMidi(in file, modifiers, ref parts, drums); } - SetDrums(ref parts, drums); + if (results.result != ScanResult.Success) + { + return (results.result, parts, settings); + } + SetDrums(ref parts, drums); if (!CheckScanValidity(in parts)) - return (ScanResult.NoNotes, parts); + { + return (ScanResult.NoNotes, parts, settings); + } if (!modifiers.Contains("name")) - return (ScanResult.NoName, parts); + { + return (ScanResult.NoName, parts, settings); + } + + if (!modifiers.TryGet("hopo_frequency", out settings.HopoThreshold) || settings.HopoThreshold <= 0) + { + if (modifiers.TryGet("eighthnote_hopo", out bool eighthNoteHopo)) + { + settings.HopoThreshold = results.resolution / (eighthNoteHopo ? 2 : 3); + } + else if (modifiers.TryGet("hopofreq", out long hopoFreq)) + { + int denominator = hopoFreq switch + { + 0 => 24, + 1 => 16, + 2 => 12, + 3 => 8, + 4 => 6, + 5 => 4, + _ => throw new NotImplementedException($"Unhandled hopofreq value {hopoFreq}!") + }; + settings.HopoThreshold = 4 * results.resolution / denominator; + } + else + { + settings.HopoThreshold = results.resolution / 3; + } + + if (format == ChartFormat.Chart) + { + // With a 192 resolution, .chart has a HOPO threshold of 65 ticks, not 64, + // so we need to scale this factor to different resolutions (480 res = 162.5 threshold). + // Why?... idk, but I hate it. + const float DEFAULT_RESOLUTION = 192; + settings.HopoThreshold += (long) (results.resolution / DEFAULT_RESOLUTION); + } + } + + // .chart defaults to no sustain cutoff whatsoever if the ini does not define the value. + // Since a failed `TryGet` sets the value to zero, we would need no additional work unless it's .mid + if (!modifiers.TryGet("sustain_cutoff_threshold", out settings.SustainCutoffThreshold) && format != ChartFormat.Chart) + { + settings.SustainCutoffThreshold = results.resolution / 3; + } + + if (format == ChartFormat.Mid || format == ChartFormat.Midi) + { + if (!modifiers.TryGet("multiplier_note", out settings.OverdiveMidiNote) || settings.OverdiveMidiNote != 103) + { + settings.OverdiveMidiNote = 116; + } + } SetIntensities(modifiers, ref parts); - return (ScanResult.Success, parts); + return (ScanResult.Success, parts, settings); + } + + private static (string Parsed, int AsNumber) ParseYear(in string baseString) + { + string parsedString = baseString; + int number = int.MaxValue; + for (int i = 0; i <= baseString.Length - 4; ++i) + { + int pivot = i; + int tmpNumber = 0; + while (i < pivot + 4 && i < baseString.Length && char.IsDigit(baseString[i])) + { + tmpNumber = 10 * tmpNumber + baseString[i] - '0'; + ++i; + } + + if (i == pivot + 4) + { + parsedString = baseString[pivot..i]; + number = tmpNumber; + break; + } + } + return (parsedString, number); + } + + protected static bool TryGetRandomBackgroundImage(TEnumerable collection, out TValue? value) + where TEnumerable : IEnumerable> + { + // Choose a valid image background present in the folder at random + var images = new List(); + foreach (var format in SongEntry.IMAGE_EXTENSIONS) + { + var (_, image) = collection.FirstOrDefault(node => node.Key == "bg" + format); + if (image != null) + { + images.Add(image); + } + } + + foreach (var (shortname, image) in collection) + { + if (!shortname.StartsWith("background")) + { + continue; + } + + foreach (var format in SongEntry.IMAGE_EXTENSIONS) + { + if (shortname.EndsWith(format)) + { + images.Add(image); + break; + } + } + } + + if (images.Count == 0) + { + value = default!; + return false; + } + value = images[SongEntry.BACKROUND_RNG.Next(images.Count)]; + return true; + } + + protected static DrumsType ParseDrumsType(in AvailableParts parts) + { + if (parts.FourLaneDrums.SubTracks > 0) + { + return DrumsType.FourLane; + } + if (parts.FiveLaneDrums.SubTracks > 0) + { + return DrumsType.FiveLane; + } + return DrumsType.Unknown; } - private static void ParseDotChart(ref YARGTextContainer container, IniSection modifiers, ref AvailableParts parts, DrumPreparseHandler drums) + private static (ScanResult result, long resolution) ParseDotChart(ref YARGTextContainer container, IniSection modifiers, ref AvailableParts parts, DrumPreparseHandler drums) where TChar : unmanaged, IEquatable, IConvertible { + long resolution = 192; if (YARGChartFileReader.ValidateTrack(ref container, YARGChartFileReader.HEADERTRACK)) { var chartMods = YARGChartFileReader.ExtractModifiers(ref container); + if (chartMods.Remove("Resolution", out var resolutions)) + { + unsafe + { + var mod = resolutions[0]; + resolution = mod.Buffer[0]; + if (resolution < 1) + { + return (ScanResult.InvalidResolution, 0); + } + } + } modifiers.Append(chartMods); } @@ -225,20 +332,25 @@ private static void ParseDotChart(ref YARGTextContainer container, } if (drums.Type == DrumsType.Unknown && drums.ValidatedDiffs > 0) + { drums.Type = DrumsType.FourLane; + } + return (ScanResult.Success, resolution); } - - private static bool ParseDotMidi(in FixedArray file, IniSection modifiers, ref AvailableParts parts, DrumPreparseHandler drums) + private static (ScanResult result, long resolution) ParseDotMidi(in FixedArray file, IniSection modifiers, ref AvailableParts parts, DrumPreparseHandler drums) { bool usePro = !modifiers.TryGet("pro_drums", out bool proDrums) || proDrums; if (drums.Type == DrumsType.Unknown) { if (usePro) + { drums.Type = DrumsType.UnknownPro; + } } else if (drums.Type == DrumsType.FourLane && usePro) + { drums.Type = DrumsType.ProDrums; - + } return ParseMidi(in file, drums, ref parts); } @@ -253,16 +365,16 @@ private static unsafe bool TraverseChartTrack(ref YARGTextContainer ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretGuitar, &ChartPreparser.ValidateFiveFret), - Instrument.FiveFretBass => ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretBass, &ChartPreparser.ValidateFiveFret), - Instrument.FiveFretRhythm => ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretRhythm, &ChartPreparser.ValidateFiveFret), + Instrument.FiveFretGuitar => ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretGuitar, &ChartPreparser.ValidateFiveFret), + Instrument.FiveFretBass => ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretBass, &ChartPreparser.ValidateFiveFret), + Instrument.FiveFretRhythm => ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretRhythm, &ChartPreparser.ValidateFiveFret), Instrument.FiveFretCoopGuitar => ChartPreparser.Traverse(ref container, difficulty, ref parts.FiveFretCoopGuitar, &ChartPreparser.ValidateFiveFret), - Instrument.SixFretGuitar => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretGuitar, &ChartPreparser.ValidateSixFret), - Instrument.SixFretBass => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretBass, &ChartPreparser.ValidateSixFret), - Instrument.SixFretRhythm => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretRhythm, &ChartPreparser.ValidateSixFret), - Instrument.SixFretCoopGuitar => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretCoopGuitar, &ChartPreparser.ValidateSixFret), - Instrument.Keys => ChartPreparser.Traverse(ref container, difficulty, ref parts.Keys, &ChartPreparser.ValidateFiveFret), - Instrument.FourLaneDrums => drums.ParseChart(ref container, difficulty), + Instrument.SixFretGuitar => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretGuitar, &ChartPreparser.ValidateSixFret), + Instrument.SixFretBass => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretBass, &ChartPreparser.ValidateSixFret), + Instrument.SixFretRhythm => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretRhythm, &ChartPreparser.ValidateSixFret), + Instrument.SixFretCoopGuitar => ChartPreparser.Traverse(ref container, difficulty, ref parts.SixFretCoopGuitar, &ChartPreparser.ValidateSixFret), + Instrument.Keys => ChartPreparser.Traverse(ref container, difficulty, ref parts.Keys, &ChartPreparser.ValidateFiveFret), + Instrument.FourLaneDrums => drums.ParseChart(ref container, difficulty), _ => false, }; } @@ -417,44 +529,5 @@ private static void SetIntensities(IniSection modifiers, ref AvailableParts part } } } - - protected static bool TryGetRandomBackgroundImage(IEnumerable> collection, out T value) - { - // Choose a valid image background present in the folder at random - var images = new List(); - foreach (var format in IMAGE_EXTENSIONS) - { - var (_, image) = collection.FirstOrDefault(node => node.Key == "bg" + format); - if (image != null) - { - images.Add(image); - } - } - - foreach (var (shortname, image) in collection) - { - if (!shortname.StartsWith("background")) - { - continue; - } - - foreach (var format in IMAGE_EXTENSIONS) - { - if (shortname.EndsWith(format)) - { - images.Add(image); - break; - } - } - } - - if (images.Count == 0) - { - value = default!; - return false; - } - value = images[BACKROUND_RNG.Next(images.Count)]; - return true; - } } } diff --git a/YARG.Core/Song/Entries/Ini/SongEntry.Sng.cs b/YARG.Core/Song/Entries/Ini/SongEntry.Sng.cs index 9e57cbd8e..87b1f0002 100644 --- a/YARG.Core/Song/Entries/Ini/SongEntry.Sng.cs +++ b/YARG.Core/Song/Entries/Ini/SongEntry.Sng.cs @@ -1,7 +1,9 @@ -using System; +using MoonscraperChartEditor.Song.IO; +using System; using System.IO; using System.Linq; using YARG.Core.Audio; +using YARG.Core.Chart; using YARG.Core.Extensions; using YARG.Core.IO; using YARG.Core.IO.Ini; @@ -15,37 +17,88 @@ public sealed class SngEntry : IniSubEntry { private readonly uint _version; private readonly AbridgedFileInfo _sngInfo; - private readonly string _chartName; + private readonly ChartFormat _chartFormat; public override string Location => _sngInfo.FullName; public override string DirectoryActual => Path.GetDirectoryName(_sngInfo.FullName); - public override ChartType Type { get; } - public override DateTime GetAddTime() => _sngInfo.LastUpdatedTime; - + public override DateTime GetAddDate() => _sngInfo.LastUpdatedTime.Date; public override EntryType SubType => EntryType.Sng; + public override ulong SongLengthMilliseconds { get; } + + private SngEntry(SngFile sngFile, ChartFormat format, IniSection modifiers, in AvailableParts parts, in HashWrapper hash, in SongMetadata metadata, in LoaderSettings settings) + : base(in metadata, in parts, in hash, in settings, modifiers) + { + _version = sngFile.Version; + _sngInfo = sngFile.Info; + _chartFormat = format; + if (!modifiers.TryGet("song_length", out ulong songLength)) + { + using var mixer = LoadAudio(0, 0); + if (mixer != null) + { + songLength = (ulong) (mixer.Length * SongMetadata.MILLISECOND_FACTOR); + } + } + SongLengthMilliseconds = songLength; + } - protected override void SerializeSubData(BinaryWriter writer) + private SngEntry(uint version, in AbridgedFileInfo sngInfo, ChartFormat format, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + : base(stream, strings) { - writer.Write(_sngInfo.LastUpdatedTime.ToBinary()); - writer.Write(_version); - writer.Write((byte) Type); + _version = version; + _sngInfo = sngInfo; + _chartFormat = format; + SongLengthMilliseconds = stream.Read(Endianness.Little); } - protected override Stream? GetChartStream() + public override void Serialize(MemoryStream stream, CategoryCacheWriteNode node) + { + // Validation block + stream.Write(_sngInfo.LastUpdatedTime.ToBinary(), Endianness.Little); + stream.Write(_version, Endianness.Little); + stream.WriteByte((byte) _chartFormat); + + // Metadata block + base.Serialize(stream, node); + stream.Write(SongLengthMilliseconds, Endianness.Little); + } + + public override SongChart? LoadChart() { if (!_sngInfo.IsStillValid()) + { return null; + } - using var sngFile = SngFile.TryLoadFromFile(_sngInfo); + var sngFile = SngFile.TryLoadFromFile(_sngInfo); if (sngFile == null) + { return null; + } - return sngFile[_chartName].CreateStream(sngFile); + var parseSettings = new ParseSettings() + { + HopoThreshold = Settings.HopoThreshold, + SustainCutoffThreshold = Settings.SustainCutoffThreshold, + StarPowerNote = Settings.OverdiveMidiNote, + DrumsType = ParseDrumsType(in Parts), + ChordHopoCancellation = _chartFormat != ChartFormat.Chart + }; + + string file = CHART_FILE_TYPES[(int) _chartFormat].Filename; + using var stream = sngFile[file].CreateStream(sngFile); + if (_chartFormat == ChartFormat.Mid || _chartFormat == ChartFormat.Midi) + { + return SongChart.FromMidi(in parseSettings, MidFileLoader.LoadMidiFile(stream)); + } + + using var reader = new StreamReader(stream); + return SongChart.FromDotChart(in parseSettings, reader.ReadToEnd()); } public override StemMixer? LoadAudio(float speed, double volume, params SongStem[] ignoreStems) { - using var sngFile = SngFile.TryLoadFromFile(_sngInfo); + var sngFile = SngFile.TryLoadFromFile(_sngInfo); if (sngFile == null) { YargLogger.LogFormatError("Failed to load sng file {0}", _sngInfo.FullName); @@ -56,7 +109,7 @@ protected override void SerializeSubData(BinaryWriter writer) public override StemMixer? LoadPreviewAudio(float speed) { - using var sngFile = SngFile.TryLoadFromFile(_sngInfo); + var sngFile = SngFile.TryLoadFromFile(_sngInfo); if (sngFile == null) { YargLogger.LogFormatError("Failed to load sng file {0}", _sngInfo.FullName); @@ -85,18 +138,18 @@ protected override void SerializeSubData(BinaryWriter writer) public override YARGImage? LoadAlbumData() { - using var sngFile = SngFile.TryLoadFromFile(_sngInfo); + var sngFile = SngFile.TryLoadFromFile(_sngInfo); if (sngFile == null) return null; - if (!string.IsNullOrEmpty(_cover) && sngFile.TryGetValue(_video, out var cover)) + if (sngFile.TryGetValue(Cover, out var cover)) { var image = YARGImage.Load(in cover, sngFile); if (image != null) { return image; } - YargLogger.LogFormatError("SNG Image mapped to {0} failed to load", _video); + YargLogger.LogFormatError("SNG Image mapped to {0} failed to load", Cover); } foreach (string albumFile in ALBUMART_FILES) @@ -116,7 +169,7 @@ protected override void SerializeSubData(BinaryWriter writer) public override BackgroundResult? LoadBackground(BackgroundType options) { - using var sngFile = SngFile.TryLoadFromFile(_sngInfo); + var sngFile = SngFile.TryLoadFromFile(_sngInfo); if (sngFile == null) { return null; @@ -140,7 +193,7 @@ protected override void SerializeSubData(BinaryWriter writer) if ((options & BackgroundType.Video) > 0) { - if (!string.IsNullOrEmpty(_video) && sngFile.TryGetValue(_video, out var video)) + if (sngFile.TryGetValue(Video, out var video)) { var stream = video.CreateStream(sngFile); return new BackgroundResult(BackgroundType.Video, stream); @@ -172,8 +225,7 @@ protected override void SerializeSubData(BinaryWriter writer) if ((options & BackgroundType.Image) > 0) { - if ((!string.IsNullOrEmpty(_background) && sngFile.TryGetValue(_background, out var listing)) - || TryGetRandomBackgroundImage(sngFile, out listing)) + if (sngFile.TryGetValue(Background, out var listing) || TryGetRandomBackgroundImage(sngFile, out listing)) { var image = YARGImage.Load(in listing, sngFile); if (image != null) @@ -200,9 +252,14 @@ protected override void SerializeSubData(BinaryWriter writer) return null; } + public override FixedArray LoadMiloData() + { + return FixedArray.Null; + } + private StemMixer? CreateAudioMixer(float speed, double volume, SngFile sngFile, params SongStem[] ignoreStems) { - bool clampStemVolume = _metadata.Source.Str.ToLowerInvariant() == "yarg"; + bool clampStemVolume = Metadata.Source.Str.ToLowerInvariant() == "yarg"; var mixer = GlobalAudioHandler.CreateMixer(ToString(), speed, volume, clampStemVolume); if (mixer == null) { @@ -243,44 +300,18 @@ protected override void SerializeSubData(BinaryWriter writer) return mixer; } - private SngEntry(SngFile sngFile, in IniChartNode chart, in AvailableParts parts, in HashWrapper hash, IniSection modifiers, string defaultPlaylist) - : base(in parts, in hash, modifiers, defaultPlaylist) - { - _version = sngFile.Version; - _sngInfo = sngFile.Info; - _chartName = chart.File; - Type = chart.Type; - } - - private SngEntry(uint version, in AbridgedFileInfo sngInfo, in IniChartNode chart, UnmanagedMemoryStream stream, CategoryCacheStrings strings) - : base(stream, strings) + public static (ScanResult, SngEntry?) ProcessNewEntry(SngFile sng, in SngFileListing listing, ChartFormat format, string defaultPlaylist) { - _version = version; - _sngInfo = sngInfo; - _chartName = chart.File; - Type = chart.Type; - } - - public static (ScanResult, SngEntry?) ProcessNewEntry(SngFile sng, in IniChartNode chart, string defaultPlaylist) - { - using var file = chart.File.LoadAllBytes(sng); - var (result, parts) = ScanIniChartFile(in file, chart.Type, sng.Metadata); + using var file = listing.LoadAllBytes(sng); + var (result, parts, settings) = ProcessChartFile(file, format, sng.Metadata); if (result != ScanResult.Success) { return (result, null); } - var node = new IniChartNode(chart.File.Name, chart.Type); var hash = HashWrapper.Hash(file.ReadOnlySpan); - var entry = new SngEntry(sng, in node, in parts, in hash, sng.Metadata, defaultPlaylist); - if (!sng.Metadata.Contains("song_length")) - { - using var mixer = entry.LoadAudio(0, 0); - if (mixer != null) - { - entry.SongLengthSeconds = mixer.Length; - } - } + var metadata = new SongMetadata(sng.Metadata, defaultPlaylist); + var entry = new SngEntry(sng, format, sng.Metadata, in parts, in hash, in metadata, in settings); return (result, entry); } @@ -288,35 +319,28 @@ public static (ScanResult, SngEntry?) ProcessNewEntry(SngFile sng, in IniChartNo { var sngInfo = AbridgedFileInfo.TryParseInfo(filename, stream); if (sngInfo == null) + { return null; + } - using var sngFile = SngFile.TryLoadFromFile(sngInfo.Value); uint version = stream.Read(Endianness.Little); + var sngFile = SngFile.TryLoadFromFile(sngInfo.Value); if (sngFile == null || sngFile.Version != version) { // TODO: Implement Update-in-place functionality return null; } - byte chartTypeIndex = (byte)stream.ReadByte(); - if (chartTypeIndex >= CHART_FILE_TYPES.Length) - { - return null; - } - return new SngEntry(sngFile.Version, sngInfo.Value, CHART_FILE_TYPES[chartTypeIndex], stream, strings); + byte chartTypeIndex = (byte) stream.ReadByte(); + return chartTypeIndex < CHART_FILE_TYPES.Length ? new SngEntry(sngFile.Version, sngInfo.Value, (ChartFormat) chartTypeIndex, stream, strings) : null; } public static SngEntry? LoadFromCache_Quick(string filename, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var sngInfo = new AbridgedFileInfo(filename, stream); - uint version = stream.Read(Endianness.Little); - byte chartTypeIndex = (byte)stream.ReadByte(); - if (chartTypeIndex >= CHART_FILE_TYPES.Length) - { - return null; - } - return new SngEntry(version, sngInfo, CHART_FILE_TYPES[chartTypeIndex], stream, strings); + byte chartTypeIndex = (byte) stream.ReadByte(); + return chartTypeIndex < CHART_FILE_TYPES.Length ? new SngEntry(version, sngInfo, (ChartFormat) chartTypeIndex, stream, strings) : null; } } } diff --git a/YARG.Core/Song/Entries/Ini/SongEntry.UnpackedIni.cs b/YARG.Core/Song/Entries/Ini/SongEntry.UnpackedIni.cs index 7a9f3f1e2..804a9f49d 100644 --- a/YARG.Core/Song/Entries/Ini/SongEntry.UnpackedIni.cs +++ b/YARG.Core/Song/Entries/Ini/SongEntry.UnpackedIni.cs @@ -9,37 +9,102 @@ using System.Linq; using YARG.Core.Logging; using YARG.Core.Extensions; +using MoonscraperChartEditor.Song.IO; +using YARG.Core.Chart; namespace YARG.Core.Song { public sealed class UnpackedIniEntry : IniSubEntry { private readonly AbridgedFileInfo _chartFile; + private readonly ChartFormat _chartFormat; private readonly AbridgedFileInfo? _iniFile; public override string Location { get; } public override string DirectoryActual => Location; - public override ChartType Type { get; } - public override DateTime GetAddTime() => _chartFile.LastUpdatedTime; - + public override DateTime GetAddDate() => _chartFile.LastUpdatedTime.Date; public override EntryType SubType => EntryType.Ini; + public override ulong SongLengthMilliseconds { get; } + + private UnpackedIniEntry(string directory, FileInfo chartInfo, ChartFormat format, in AbridgedFileInfo? iniFile, IniSection modifiers, in AvailableParts parts, in HashWrapper hash, in SongMetadata metadata, in LoaderSettings settings) + : base(in metadata, in parts, in hash, in settings, modifiers) + { + Location = directory; + _chartFile = new AbridgedFileInfo(chartInfo); + _chartFormat = format; + _iniFile = iniFile; + + if (!modifiers.TryGet("song_length", out ulong songLength)) + { + using var mixer = LoadAudio(0, 0); + if (mixer != null) + { + songLength = (ulong) (mixer.Length * SongMetadata.MILLISECOND_FACTOR); + } + } + SongLengthMilliseconds = songLength; + } + + private UnpackedIniEntry(string directory, in AbridgedFileInfo chartInfo, ChartFormat format, in AbridgedFileInfo? iniFile, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + : base(stream, strings) + { + Location = directory; + _chartFile = chartInfo; + _chartFormat = format; + _iniFile = iniFile; + SongLengthMilliseconds = stream.Read(Endianness.Little); + } - protected override void SerializeSubData(BinaryWriter writer) + public override void Serialize(MemoryStream stream, CategoryCacheWriteNode node) { - writer.Write((byte) Type); - writer.Write(_chartFile.LastUpdatedTime.ToBinary()); + // Validation block + stream.WriteByte((byte) _chartFormat); + stream.Write(_chartFile.LastUpdatedTime.ToBinary(), Endianness.Little); + stream.Write(_iniFile != null); if (_iniFile != null) { - writer.Write(true); - writer.Write(_iniFile.Value.LastUpdatedTime.ToBinary()); + stream.Write(_iniFile.Value.LastUpdatedTime.ToBinary(), Endianness.Little); } - else - writer.Write(false); + + // Metadata block + base.Serialize(stream, node); + stream.Write(SongLengthMilliseconds, Endianness.Little); + } + + public override SongChart? LoadChart() + { + if (!_chartFile.IsStillValid()) + { + return null; + } + + if (_iniFile != null ? !_iniFile.Value.IsStillValid() : File.Exists(Path.Combine(Location, "song.ini"))) + { + return null; + } + + var parseSettings = new ParseSettings() + { + HopoThreshold = Settings.HopoThreshold, + SustainCutoffThreshold = Settings.SustainCutoffThreshold, + StarPowerNote = Settings.OverdiveMidiNote, + DrumsType = ParseDrumsType(in Parts), + ChordHopoCancellation = _chartFormat == ChartFormat.Chart + }; + + using var stream = new FileStream(_chartFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); + if (_chartFormat == ChartFormat.Mid || _chartFormat == ChartFormat.Midi) + { + return SongChart.FromMidi(in parseSettings, MidFileLoader.LoadMidiFile(stream)); + } + + using var reader = new StreamReader(stream); + return SongChart.FromDotChart(in parseSettings, reader.ReadToEnd()); } public override StemMixer? LoadAudio(float speed, double volume, params SongStem[] ignoreStems) { - bool clampStemVolume = _metadata.Source.Str.ToLowerInvariant() == "yarg"; + bool clampStemVolume = Metadata.Source.Str.ToLowerInvariant() == "yarg"; var mixer = GlobalAudioHandler.CreateMixer(ToString(), speed, volume, clampStemVolume); if (mixer == null) { @@ -97,7 +162,7 @@ protected override void SerializeSubData(BinaryWriter writer) public override YARGImage? LoadAlbumData() { var subFiles = GetSubFiles(); - if (!string.IsNullOrEmpty(_cover) && subFiles.TryGetValue(_cover, out var cover)) + if (!string.IsNullOrEmpty(Cover) && subFiles.TryGetValue(Cover, out var cover)) { var image = YARGImage.Load(cover); if (image != null) @@ -136,7 +201,7 @@ protected override void SerializeSubData(BinaryWriter writer) if ((options & BackgroundType.Video) > 0) { - if (!string.IsNullOrEmpty(_video) && subFiles.TryGetValue(_video, out var video)) + if (subFiles.TryGetValue(Video, out var video)) { var stream = File.OpenRead(video.FullName); return new BackgroundResult(BackgroundType.Video, stream); @@ -157,10 +222,9 @@ protected override void SerializeSubData(BinaryWriter writer) if ((options & BackgroundType.Image) > 0) { - if ((!string.IsNullOrEmpty(_background) && subFiles.TryGetValue(_background, out var file)) - || TryGetRandomBackgroundImage(subFiles, out file)) + if (subFiles.TryGetValue(Background, out var file) || TryGetRandomBackgroundImage(subFiles, out file)) { - var image = YARGImage.Load(file); + var image = YARGImage.Load(file!); if (image != null) { return new BackgroundResult(image); @@ -170,22 +234,9 @@ protected override void SerializeSubData(BinaryWriter writer) return null; } - protected override Stream? GetChartStream() + public override FixedArray LoadMiloData() { - if (!_chartFile.IsStillValid()) - return null; - - if (_iniFile != null) - { - if (!_iniFile.Value.IsStillValid()) - return null; - } - else if (File.Exists(Path.Combine(Location, "song.ini"))) - { - return null; - } - - return new FileStream(_chartFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1); + return FixedArray.Null; } private Dictionary GetSubFiles() @@ -202,25 +253,7 @@ private Dictionary GetSubFiles() return files; } - private UnpackedIniEntry(string directory, in IniChartNode chart, AbridgedFileInfo? iniFile, in AvailableParts parts, in HashWrapper hash, IniSection modifiers, string defaultPlaylist) - : base(in parts, in hash, modifiers, defaultPlaylist) - { - Location = directory; - Type = chart.Type; - _chartFile = chart.File; - _iniFile = iniFile; - } - - private UnpackedIniEntry(string directory, in IniChartNode chart, AbridgedFileInfo? iniFile, UnmanagedMemoryStream stream, CategoryCacheStrings strings) - : base(stream, strings) - { - Location = directory; - Type = chart.Type; - _chartFile = chart.File; - _iniFile = iniFile; - } - - public static (ScanResult, UnpackedIniEntry?) ProcessNewEntry(string chartDirectory, in IniChartNode chart, FileInfo? iniFile, string defaultPlaylist) + public static (ScanResult, UnpackedIniEntry?) ProcessNewEntry(string chartDirectory, FileInfo chartInfo, ChartFormat format, FileInfo? iniFile, string defaultPlaylist) { IniSection iniModifiers; AbridgedFileInfo? iniFileInfo = null; @@ -239,30 +272,21 @@ public static (ScanResult, UnpackedIniEntry?) ProcessNewEntry(string chartDirect iniModifiers = new(); } - if ((chart.File.Attributes & AbridgedFileInfo.RECALL_ON_DATA_ACCESS) > 0) + if ((chartInfo.Attributes & AbridgedFileInfo.RECALL_ON_DATA_ACCESS) > 0) { return (ScanResult.ChartNotDownloaded, null); } - using var file = FixedArray.Load(chart.File.FullName); - var (result, parts) = ScanIniChartFile(in file, chart.Type, iniModifiers); + using var file = FixedArray.Load(chartInfo.FullName); + var (result, parts, settings) = ProcessChartFile(file, format, iniModifiers); if (result != ScanResult.Success) { return (result, null); } - var abridged = new AbridgedFileInfo(chart.File); - var node = new IniChartNode(abridged, chart.Type); var hash = HashWrapper.Hash(file.ReadOnlySpan); - var entry = new UnpackedIniEntry(chartDirectory, in node, iniFileInfo, in parts, in hash, iniModifiers, defaultPlaylist); - if (!iniModifiers.Contains("song_length")) - { - using var mixer = entry.LoadAudio(0, 0); - if (mixer != null) - { - entry.SongLengthSeconds = mixer.Length; - } - } + var metadata = new SongMetadata(iniModifiers, defaultPlaylist); + var entry = new UnpackedIniEntry(chartDirectory, chartInfo, format, in iniFileInfo, iniModifiers, in parts, in hash, in metadata, in settings); return (result, entry); } @@ -275,7 +299,7 @@ public static (ScanResult, UnpackedIniEntry?) ProcessNewEntry(string chartDirect } var chart = CHART_FILE_TYPES[chartTypeIndex]; - var chartInfo = AbridgedFileInfo.TryParseInfo(Path.Combine(directory, chart.File), stream, true); + var chartInfo = AbridgedFileInfo.TryParseInfo(Path.Combine(directory, chart.Filename), stream); if (chartInfo == null) { return null; @@ -295,8 +319,7 @@ public static (ScanResult, UnpackedIniEntry?) ProcessNewEntry(string chartDirect { return null; } - var node = new IniChartNode(chartInfo.Value, chart.Type); - return new UnpackedIniEntry(directory, in node, iniInfo, stream, strings); + return new UnpackedIniEntry(directory, chartInfo.Value, chart.Format, in iniInfo, stream, strings); } public static UnpackedIniEntry? IniFromCache_Quick(string directory, UnmanagedMemoryStream stream, CategoryCacheStrings strings) @@ -308,14 +331,9 @@ public static (ScanResult, UnpackedIniEntry?) ProcessNewEntry(string chartDirect } var chart = CHART_FILE_TYPES[chartTypeIndex]; - var chartInfo = new AbridgedFileInfo(Path.Combine(directory, chart.File), stream); - AbridgedFileInfo? iniInfo = null; - if (stream.ReadBoolean()) - { - iniInfo = new AbridgedFileInfo(Path.Combine(directory, "song.ini"), stream); - } - var node = new IniChartNode(chartInfo, chart.Type); - return new UnpackedIniEntry(directory, in node, iniInfo, stream, strings); + var chartInfo = new AbridgedFileInfo(Path.Combine(directory, chart.Filename), stream); + AbridgedFileInfo? iniInfo = stream.ReadBoolean() ? new AbridgedFileInfo(Path.Combine(directory, "song.ini"), stream) : null; + return new UnpackedIniEntry(directory, chartInfo, chart.Format, in iniInfo, stream, strings); } } } diff --git a/YARG.Core/Song/Entries/RBCON/RBAudio.cs b/YARG.Core/Song/Entries/RBCON/RBAudio.cs index d483175d2..55dd22494 100644 --- a/YARG.Core/Song/Entries/RBCON/RBAudio.cs +++ b/YARG.Core/Song/Entries/RBCON/RBAudio.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Text; using YARG.Core.Extensions; namespace YARG.Core.Song @@ -39,26 +37,26 @@ public RBAudio(UnmanagedMemoryStream stream) Crowd = ReadArray(stream); } - public readonly void Serialize(BinaryWriter writer) + public readonly void Serialize(MemoryStream stream) { - WriteArray(Track, writer); - WriteArray(Drums, writer); - WriteArray(Bass, writer); - WriteArray(Guitar, writer); - WriteArray(Keys, writer); - WriteArray(Vocals, writer); - WriteArray(Crowd, writer); + WriteArray(Track, stream); + WriteArray(Drums, stream); + WriteArray(Bass, stream); + WriteArray(Guitar, stream); + WriteArray(Keys, stream); + WriteArray(Vocals, stream); + WriteArray(Crowd, stream); } - public static void WriteArray(in TType[] values, BinaryWriter writer) + public static void WriteArray(in TType[] values, MemoryStream stream) { - writer.Write(values.Length); + stream.Write(values.Length, Endianness.Little); unsafe { fixed (TType* ptr = values) { var span = new ReadOnlySpan(ptr, values.Length * sizeof(TType)); - writer.Write(span); + stream.Write(span); } } } diff --git a/YARG.Core/Song/Entries/RBCON/RBCONDifficulties.cs b/YARG.Core/Song/Entries/RBCON/RBCONDifficulties.cs index ce7a80166..75b82b53d 100644 --- a/YARG.Core/Song/Entries/RBCON/RBCONDifficulties.cs +++ b/YARG.Core/Song/Entries/RBCON/RBCONDifficulties.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using YARG.Core.Extensions; -using YARG.Core.IO; namespace YARG.Core.Song { diff --git a/YARG.Core/Song/Entries/RBCON/RBMetadata.cs b/YARG.Core/Song/Entries/RBCON/RBMetadata.cs index 6397dd126..0430898dc 100644 --- a/YARG.Core/Song/Entries/RBCON/RBMetadata.cs +++ b/YARG.Core/Song/Entries/RBCON/RBMetadata.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; +using System.IO; +using YARG.Core.Extensions; namespace YARG.Core.Song { @@ -41,5 +41,76 @@ public struct RBMetadata public RBAudio Indices; public RBAudio Panning; + + public RBMetadata(UnmanagedMemoryStream stream) + { + AnimTempo = stream.Read(Endianness.Little); + SongID = stream.ReadString(); + VocalPercussionBank = stream.ReadString(); + VocalSongScrollSpeed = stream.Read(Endianness.Little); + VocalGender = stream.ReadBoolean(); + VocalTonicNote = stream.Read(Endianness.Little); + SongTonality = stream.ReadBoolean(); + TuningOffsetCents = stream.Read(Endianness.Little); + VenueVersion = stream.Read(Endianness.Little); + DrumBank = stream.ReadString(); + + RealGuitarTuning = RBAudio.ReadArray(stream); + RealBassTuning = RBAudio.ReadArray(stream); + + Indices = new RBAudio(stream); + Panning = new RBAudio(stream); + + Soloes = ReadStringArray(stream); + VideoVenues = ReadStringArray(stream); + } + + public readonly void Serialize(MemoryStream stream) + { + stream.Write(AnimTempo, Endianness.Little); + stream.Write(SongID); + stream.Write(VocalPercussionBank); + stream.Write(VocalSongScrollSpeed, Endianness.Little); + stream.Write(VocalGender); + stream.Write(VocalTonicNote, Endianness.Little); + stream.Write(SongTonality); + stream.Write(TuningOffsetCents, Endianness.Little); + stream.Write(VenueVersion, Endianness.Little); + stream.Write(DrumBank); + + RBAudio.WriteArray(in RealGuitarTuning, stream); + RBAudio.WriteArray(in RealBassTuning, stream); + + Indices.Serialize(stream); + Panning.Serialize(stream); + + stream.Write(Soloes.Length, Endianness.Little); + for (int i = 0; i < Soloes.Length; ++i) + { + stream.Write(Soloes[i]); + } + + stream.Write(VideoVenues.Length, Endianness.Little); + for (int i = 0; i < VideoVenues.Length; ++i) + { + stream.Write(VideoVenues[i]); + } + } + + private static string[] ReadStringArray(UnmanagedMemoryStream stream) + { + int length = stream.Read(Endianness.Little); + if (length == 0) + { + return Array.Empty(); + } + + var strings = new string[length]; + for (int i = 0; i < length; ++i) + { + strings[i] = stream.ReadString(); + } + return strings; + } } } diff --git a/YARG.Core/Song/Entries/RBCON/RBProUpgrade.cs b/YARG.Core/Song/Entries/RBCON/RBProUpgrade.cs index 9d5ea4ed6..351b60011 100644 --- a/YARG.Core/Song/Entries/RBCON/RBProUpgrade.cs +++ b/YARG.Core/Song/Entries/RBCON/RBProUpgrade.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Text; +using YARG.Core.Extensions; using YARG.Core.IO; namespace YARG.Core.Song @@ -9,7 +8,7 @@ namespace YARG.Core.Song public abstract class RBProUpgrade { public abstract DateTime LastUpdatedTime { get; } - public abstract void WriteToCache(BinaryWriter writer); + public abstract void WriteToCache(MemoryStream stream); public abstract Stream? GetUpgradeMidiStream(); public abstract FixedArray LoadUpgradeMidi(); } @@ -28,9 +27,9 @@ public PackedRBProUpgrade(CONFileListing? listing, DateTime lastWrite) _lastUpdatedTime = listing?.LastWrite ?? lastWrite; } - public override void WriteToCache(BinaryWriter writer) + public override void WriteToCache(MemoryStream stream) { - writer.Write(_lastUpdatedTime.ToBinary()); + stream.Write(_lastUpdatedTime.ToBinary(), Endianness.Little); } public override Stream? GetUpgradeMidiStream() @@ -63,9 +62,9 @@ public UnpackedRBProUpgrade(in AbridgedFileInfo info) _midi = info; } - public override void WriteToCache(BinaryWriter writer) + public override void WriteToCache(MemoryStream stream) { - writer.Write(_midi.LastUpdatedTime.ToBinary()); + stream.Write(_midi.LastUpdatedTime.ToBinary(), Endianness.Little); } public override Stream? GetUpgradeMidiStream() diff --git a/YARG.Core/Song/Entries/RBCON/SongEntry.PackedRBCON.cs b/YARG.Core/Song/Entries/RBCON/SongEntry.PackedRBCON.cs index 2b5533c3f..e1da2e71e 100644 --- a/YARG.Core/Song/Entries/RBCON/SongEntry.PackedRBCON.cs +++ b/YARG.Core/Song/Entries/RBCON/SongEntry.PackedRBCON.cs @@ -6,7 +6,6 @@ using YARG.Core.IO; using YARG.Core.Venue; using System.Linq; -using YARG.Core.Logging; namespace YARG.Core.Song { @@ -23,37 +22,57 @@ public sealed class PackedRBCONEntry : RBCONEntry public override string DirectoryActual => Path.GetDirectoryName(_midiListing?.ConFile.FullName); public override EntryType SubType => EntryType.CON; - public static (ScanResult, PackedRBCONEntry?) ProcessNewEntry(PackedCONGroup group, string nodename, in YARGTextContainer container, Dictionary> updates, Dictionary, RBProUpgrade)> upgrades) + public static (ScanResult, PackedRBCONEntry?) ProcessNewEntry(PackedCONGroup group, string nodename, DTAEntry node, CONModification modification) { - try + var (dtaResult, info) = ProcessDTAs(nodename, node, modification); + if (dtaResult != ScanResult.Success) { - var song = new PackedRBCONEntry(group, nodename, in container, updates, upgrades); - if (song._midiListing == null) - { - YargLogger.LogFormatError("Required midi file for {0} - {1} was not located", group.Info.FullName, item2: nodename); - return (ScanResult.MissingMidi, null); - } + return (dtaResult, null); + } - var result = song.ParseRBCONMidi(group.Stream); - if (result != ScanResult.Success) - { - return (result, null); - } - return (result, song); + group.Listings.TryGetListing(info.Location + ".mogg", out var moggListing); + if (!IsMoggValid(in modification.Mogg, moggListing, group.Stream)) + { + return (ScanResult.MoggError, null); } - catch (Exception ex) + + if (!group.Listings.TryGetListing(info.Location + ".mid", out var midiListing)) { - YargLogger.LogException(ex, null); - return (ScanResult.DTAError, null); + return (ScanResult.MissingCONMidi, null); } + + using var mainMidi = midiListing.LoadAllBytes(group.Stream); + var (midiResult, hash) = ParseRBCONMidi(in mainMidi, modification, ref info); + if (midiResult != ScanResult.Success) + { + return (midiResult, null); + } + + if (!info.Location!.StartsWith($"songs/{nodename}")) + { + nodename = midiListing.Filename.Split('/')[1]; + } + + string genPath = $"songs/{nodename}/gen/{nodename}"; + group.Listings.TryGetListing(genPath + ".milo_xbox", out var miloListing); + group.Listings.TryGetListing(genPath + "_keep.png_xbox", out var imgListing); + + if (info.Metadata.Playlist.Length == 0) + { + info.Metadata.Playlist = group.DefaultPlaylist; + } + + string psuedoDirectory = Path.Combine(group.Location, group.Listings[midiListing.PathIndex].Filename); + var entry = new PackedRBCONEntry(in info, modification, in hash, midiListing, moggListing, miloListing, imgListing, psuedoDirectory); + return (ScanResult.Success, entry); } - public static PackedRBCONEntry? TryLoadFromCache(in CONFile conFile, string nodename, Dictionary, RBProUpgrade Upgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + public static PackedRBCONEntry? TryLoadFromCache(List listings, string nodename, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var psuedoDirectory = stream.ReadString(); string midiFilename = stream.ReadString(); - if (!conFile.TryGetListing(midiFilename, out var midiListing)) + if (!listings.TryGetListing(midiFilename, out var midiListing)) { return null; } @@ -74,68 +93,75 @@ public static (ScanResult, PackedRBCONEntry?) ProcessNewEntry(PackedCONGroup gro } } - var upgrade = upgrades.TryGetValue(nodename, out var node) ? node.Upgrade : null; - - conFile.TryGetListing(Path.ChangeExtension(midiFilename, ".mogg"), out var moggListing); + listings.TryGetListing(Path.ChangeExtension(midiFilename, ".mogg"), out var moggListing); if (!midiFilename.StartsWith($"songs/{nodename}")) + { nodename = midiFilename.Split('/')[1]; + } string genPath = $"songs/{nodename}/gen/{nodename}"; - conFile.TryGetListing(genPath + ".milo_xbox", out var miloListing); - conFile.TryGetListing(genPath + "_keep.png_xbox", out var imgListing); + listings.TryGetListing(genPath + ".milo_xbox", out var miloListing); + listings.TryGetListing(genPath + "_keep.png_xbox", out var imgListing); return new PackedRBCONEntry(midiListing, lastMidiWrite, moggListing, miloListing, imgListing, psuedoDirectory, updateMidi, upgrade, stream, strings); } - public static PackedRBCONEntry LoadFromCache_Quick(in CONFile conFile, string nodename, Dictionary, RBProUpgrade Upgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + public static PackedRBCONEntry LoadFromCache_Quick(List? listings, string nodename, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { var psuedoDirectory = stream.ReadString(); string midiFilename = stream.ReadString(); - conFile.TryGetListing(midiFilename, out var midiListing); + + var midiListing = default(CONFileListing); + listings?.TryGetListing(midiFilename, out midiListing); + var lastMidiWrite = DateTime.FromBinary(stream.Read(Endianness.Little)); AbridgedFileInfo? updateMidi = stream.ReadBoolean() ? new AbridgedFileInfo(stream) : null; - var upgrade = upgrades.TryGetValue(nodename, out var node) ? node.Upgrade : null; - conFile.TryGetListing(Path.ChangeExtension(midiFilename, ".mogg"), out var moggListing); + var moggListing = default(CONFileListing); + listings?.TryGetListing(Path.ChangeExtension(midiFilename, ".mogg"), out moggListing); if (!midiFilename.StartsWith($"songs/{nodename}")) + { nodename = midiFilename.Split('/')[1]; + } string genPath = $"songs/{nodename}/gen/{nodename}"; - conFile.TryGetListing(genPath + ".milo_xbox", out var miloListing); - conFile.TryGetListing(genPath + "_keep.png_xbox", out var imgListing); + + var miloListing = default(CONFileListing); + listings?.TryGetListing(genPath + ".milo_xbox", out miloListing); + + var imgListing = default(CONFileListing); + listings?.TryGetListing(genPath + "_keep.png_xbox", out imgListing); return new PackedRBCONEntry(midiListing, lastMidiWrite, moggListing, miloListing, imgListing, psuedoDirectory, updateMidi, upgrade, stream, strings); } - private PackedRBCONEntry(PackedCONGroup group, string nodename, in YARGTextContainer container, Dictionary> updates, Dictionary, RBProUpgrade)> upgrades) - : base() + private static bool IsMoggValid(in AbridgedFileInfo? info, CONFileListing? listing, FileStream stream) { - var results = Init(nodename, in container, updates, upgrades, group.DefaultPlaylist); - string midiPath = results.location + ".mid"; - if (!group.ConFile.TryGetListing(midiPath, out _midiListing)) + using var mogg = LoadUpdateMoggStream(in info); + if (mogg != null) { - Location = string.Empty; - return; + int version = mogg.Read(Endianness.Little); + return version == 0x0A || version == 0xf0; } + return listing != null && CONFileListing.GetMoggVersion(listing, stream) == 0x0A; + } - _lastMidiWrite = _midiListing.LastWrite; - - group.ConFile.TryGetListing(results.location + ".mogg", out _moggListing); - - if (!results.location.StartsWith($"songs/{nodename}")) - nodename = _midiListing.Filename.Split('/')[1]; - - string genPath = $"songs/{nodename}/gen/{nodename}"; - group.ConFile.TryGetListing(genPath + ".milo_xbox", out _miloListing); - group.ConFile.TryGetListing(genPath + "_keep.png_xbox", out _imgListing); + private PackedRBCONEntry(in ScanNode info, CONModification modification, in HashWrapper hash + , CONFileListing midiListing, CONFileListing? moggListing, CONFileListing? miloListing, CONFileListing? imgListing, string psuedoDirectory) + : base(in info, modification, in hash) + { + _midiListing = midiListing; + _lastMidiWrite = midiListing.LastWrite; - string midiDirectory = group.ConFile.GetFilename(_midiListing.PathIndex); - Location = Path.Combine(group.Location, midiDirectory); + _moggListing = moggListing; + _miloListing = miloListing; + _imgListing = imgListing; + Location = psuedoDirectory; } - private PackedRBCONEntry(CONFileListing? midi, DateTime midiLastWrite, CONFileListing? moggListing, CONFileListing? miloListing, CONFileListing? imgListing, string directory, + private PackedRBCONEntry(CONFileListing? midi, DateTime midiLastWrite, CONFileListing? moggListing, CONFileListing? miloListing, CONFileListing? imgListing, string psuedoDirectory, AbridgedFileInfo? updateMidi, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) : base(updateMidi, upgrade, stream, strings) { @@ -145,15 +171,31 @@ private PackedRBCONEntry(CONFileListing? midi, DateTime midiLastWrite, CONFileLi _imgListing = imgListing; _lastMidiWrite = midiLastWrite; - Location = directory; + Location = psuedoDirectory; } - public override void Serialize(BinaryWriter writer, CategoryCacheWriteNode node) + public override void Serialize(MemoryStream stream, CategoryCacheWriteNode node) { - writer.Write(Location); - writer.Write(_midiListing!.Filename); - writer.Write(_midiListing.LastWrite.ToBinary()); - base.Serialize(writer, node); + stream.Write(Location); + stream.Write(_midiListing!.Filename); + stream.Write(_midiListing.LastWrite.ToBinary(), Endianness.Little); + stream.Write(UpdateMidi != null); + UpdateMidi?.Serialize(stream); + base.Serialize(stream, node); + } + + public override YARGImage? LoadAlbumData() + { + var bytes = FixedArray.Null; + if (UpdateImage != null && UpdateImage.Value.Exists()) + { + bytes = FixedArray.Load(UpdateImage.Value.FullName); + } + else if (_imgListing != null) + { + bytes = _imgListing.LoadAllBytes(); + } + return bytes.IsAllocated ? new YARGImage(bytes) : null; } public override BackgroundResult? LoadBackground(BackgroundType options) @@ -236,57 +278,23 @@ public override void Serialize(BinaryWriter writer, CategoryCacheWriteNode node) public override FixedArray LoadMiloData() { - var bytes = base.LoadMiloData(); - if (bytes.IsAllocated) + if (UpdateMilo != null && UpdateMilo.Value.Exists()) { - return bytes; + return FixedArray.Load(UpdateMilo.Value.FullName); } return _miloListing != null ? _miloListing.LoadAllBytes() : FixedArray.Null; } protected override Stream? GetMidiStream() - { - if (_midiListing == null || !_midiListing.IsStillValid(_lastMidiWrite)) - return null; - return _midiListing.CreateStream(); - } - - protected override FixedArray LoadMidiFile(Stream? file) { return _midiListing != null && _midiListing.IsStillValid(_lastMidiWrite) - ? _midiListing.LoadAllBytes(file!) - : FixedArray.Null; - } - - protected override FixedArray LoadRawImageData() - { - var bytes = base.LoadRawImageData(); - if (bytes.IsAllocated) - { - return bytes; - } - return _imgListing != null ? _imgListing.LoadAllBytes() : FixedArray.Null; + ? _midiListing.CreateStream() + : null; } protected override Stream? GetMoggStream() { - var stream = base.GetMoggStream(); - if (stream != null) - { - return stream; - } - return _moggListing?.CreateStream(); - } - - protected override bool IsMoggValid(Stream? stream) - { - using var mogg = base.GetMoggStream(); - if (mogg != null) - { - int version = mogg.Read(Endianness.Little); - return version == 0x0A || version == 0xf0; - } - return _moggListing != null && CONFileListing.GetMoggVersion(_moggListing, stream!) == 0x0A; + return LoadUpdateMoggStream(in UpdateMogg) ?? _moggListing?.CreateStream(); } } } diff --git a/YARG.Core/Song/Entries/RBCON/SongEntry.RBCON.cs b/YARG.Core/Song/Entries/RBCON/SongEntry.RBCON.cs index 329ed5ed2..b789fa50a 100644 --- a/YARG.Core/Song/Entries/RBCON/SongEntry.RBCON.cs +++ b/YARG.Core/Song/Entries/RBCON/SongEntry.RBCON.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using YARG.Core.Chart; using YARG.Core.Song.Cache; using YARG.Core.IO; @@ -16,59 +15,103 @@ namespace YARG.Core.Song { public abstract class RBCONEntry : SongEntry { - protected struct DTAResult - { - public static readonly DTAResult Empty = new() - { - pans = Array.Empty(), - volumes = Array.Empty(), - cores = Array.Empty(), - }; - - public bool alternatePath; - public bool discUpdate; - public string location; - public float[] pans; - public float[] volumes; - public float[] cores; - } - private const long NOTE_SNAP_THRESHOLD = 10; - private RBMetadata _rbMetadata; - private RBCONDifficulties _rbDifficulties; + public readonly RBMetadata RBMetadata; + public readonly RBCONDifficulties RBDifficulties; + + public readonly AbridgedFileInfo? UpdateMidi; + public readonly RBProUpgrade? Upgrade; - private AbridgedFileInfo? _updateMidi; - private RBProUpgrade? _upgrade; + public readonly AbridgedFileInfo? UpdateMogg; + public readonly AbridgedFileInfo? UpdateMilo; + public readonly AbridgedFileInfo? UpdateImage; - private AbridgedFileInfo? UpdateMogg; - private AbridgedFileInfo? UpdateMilo; - private AbridgedFileInfo? UpdateImage; + public string RBSongId => RBMetadata.SongID; + public int RBBandDiff => RBDifficulties.Band; - public string RBSongId => _rbMetadata.SongID; - public int RBBandDiff => _rbDifficulties.Band; + public override string Year { get; } + public override int YearAsNumber { get; } + public override ulong SongLengthMilliseconds { get; } + public override bool LoopVideo => false; protected abstract DateTime MidiLastUpdate { get; } - public override DateTime GetAddTime() + protected RBCONEntry(in ScanNode info, CONModification modification, in HashWrapper hash) + : base(in info.Metadata, in info.Parts, in hash, in info.Settings) + { + Year = info.Metadata.Year; + YearAsNumber = info.YearAsNumber; + SongLengthMilliseconds = info.SongLength; + RBMetadata = info.RBMetadata; + RBDifficulties = info.Difficulties; + UpdateMidi = modification.Midi; + UpdateMogg = modification.Mogg; + UpdateImage = modification.Image; + UpdateMilo = modification.Milo; + Upgrade = modification.UpgradeNode; + } + + protected RBCONEntry(AbridgedFileInfo? updateMidi, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + : base(stream, strings) + { + UpdateMidi = updateMidi; + Upgrade = upgrade; + + YearAsNumber = stream.Read(Endianness.Little); + SongLengthMilliseconds = stream.Read(Endianness.Little); + + UpdateMogg = stream.ReadBoolean() ? new AbridgedFileInfo(stream.ReadString(), false) : null; + UpdateMilo = stream.ReadBoolean() ? new AbridgedFileInfo(stream.ReadString(), false) : null; + UpdateImage = stream.ReadBoolean() ? new AbridgedFileInfo(stream.ReadString(), false) : null; + unsafe + { + RBDifficulties = *(RBCONDifficulties*) stream.PositionPointer; + stream.Position += sizeof(RBCONDifficulties); + } + RBMetadata = new RBMetadata(stream); + + Year = YearAsNumber != int.MaxValue ? YearAsNumber.ToString() : Metadata.Year; + } + + public override void Serialize(MemoryStream stream, CategoryCacheWriteNode node) + { + base.Serialize(stream, node); + stream.Write(YearAsNumber, Endianness.Little); + stream.Write(SongLengthMilliseconds, Endianness.Little); + WriteUpdateInfo(UpdateMogg, stream); + WriteUpdateInfo(UpdateMilo, stream); + WriteUpdateInfo(UpdateImage, stream); + unsafe + { + fixed (RBCONDifficulties* ptr = &RBDifficulties) + { + var span = new ReadOnlySpan(ptr, sizeof(RBCONDifficulties)); + stream.Write(span); + } + } + RBMetadata.Serialize(stream); + } + + public override DateTime GetAddDate() { var lastUpdateTime = MidiLastUpdate; - if (_updateMidi != null) + if (UpdateMidi != null) { - if (_updateMidi.Value.LastUpdatedTime > lastUpdateTime) + if (UpdateMidi.Value.LastUpdatedTime > lastUpdateTime) { - lastUpdateTime = _updateMidi.Value.LastUpdatedTime; + lastUpdateTime = UpdateMidi.Value.LastUpdatedTime; } } - if (_upgrade != null) + if (Upgrade != null) { - if (_upgrade.LastUpdatedTime > lastUpdateTime) + if (Upgrade.LastUpdatedTime > lastUpdateTime) { - lastUpdateTime = _upgrade.LastUpdatedTime; + lastUpdateTime = Upgrade.LastUpdatedTime; } } - return lastUpdateTime; + return lastUpdateTime.Date; } public override SongChart? LoadChart() @@ -79,32 +122,46 @@ public override DateTime GetAddTime() using (var midiStream = GetMidiStream()) { if (midiStream == null) + { return null; + } midi = MidiFile.Read(midiStream, readingSettings); } // Merge update MIDI - if (_updateMidi != null) + if (UpdateMidi != null) { - if (!_updateMidi.Value.IsStillValid(false)) + if (!UpdateMidi.Value.IsStillValid(false)) + { return null; + } - using var midiStream = new FileStream(_updateMidi.Value.FullName, FileMode.Open, FileAccess.Read, FileShare.Read); + using var midiStream = new FileStream(UpdateMidi.Value.FullName, FileMode.Open, FileAccess.Read, FileShare.Read); var update = MidiFile.Read(midiStream, readingSettings); midi.Merge(update); } // Merge upgrade MIDI - if (_upgrade != null) + if (Upgrade != null) { - using var midiStream = _upgrade.GetUpgradeMidiStream(); + using var midiStream = Upgrade.GetUpgradeMidiStream(); if (midiStream == null) + { return null; + } var update = MidiFile.Read(midiStream, readingSettings); midi.Merge(update); } - return SongChart.FromMidi(_parseSettings, midi); + var parseSettings = new ParseSettings() + { + HopoThreshold = Settings.HopoThreshold, + SustainCutoffThreshold = Settings.SustainCutoffThreshold, + StarPowerNote = Settings.OverdiveMidiNote, + DrumsType = DrumsType.FourLane, + ChordHopoCancellation = true + }; + return SongChart.FromMidi(in parseSettings, midi); } public override StemMixer? LoadAudio(float speed, double volume, params SongStem[] ignoreStems) @@ -126,7 +183,7 @@ public override DateTime GetAddTime() int start = stream.Read(Endianness.Little); stream.Seek(start, SeekOrigin.Begin); - bool clampStemVolume = _metadata.Source.Str.ToLowerInvariant() == "yarg"; + bool clampStemVolume = Metadata.Source.Str.ToLowerInvariant() == "yarg"; var mixer = GlobalAudioHandler.CreateMixer(ToString(), stream, speed, volume, clampStemVolume); if (mixer == null) { @@ -136,58 +193,58 @@ public override DateTime GetAddTime() } - if (_rbMetadata.Indices.Drums.Length > 0 && !ignoreStems.Contains(SongStem.Drums)) + if (RBMetadata.Indices.Drums.Length > 0 && !ignoreStems.Contains(SongStem.Drums)) { - switch (_rbMetadata.Indices.Drums.Length) + switch (RBMetadata.Indices.Drums.Length) { //drum (0 1): stereo kit --> (0 1) case 1: case 2: - mixer.AddChannel(SongStem.Drums, _rbMetadata.Indices.Drums, _rbMetadata.Panning.Drums!); + mixer.AddChannel(SongStem.Drums, RBMetadata.Indices.Drums, RBMetadata.Panning.Drums!); break; //drum (0 1 2): mono kick, stereo snare/kit --> (0) (1 2) case 3: - mixer.AddChannel(SongStem.Drums1, _rbMetadata.Indices.Drums[0..1], _rbMetadata.Panning.Drums![0..2]); - mixer.AddChannel(SongStem.Drums2, _rbMetadata.Indices.Drums[1..3], _rbMetadata.Panning.Drums[2..6]); + mixer.AddChannel(SongStem.Drums1, RBMetadata.Indices.Drums[0..1], RBMetadata.Panning.Drums![0..2]); + mixer.AddChannel(SongStem.Drums2, RBMetadata.Indices.Drums[1..3], RBMetadata.Panning.Drums[2..6]); break; //drum (0 1 2 3): mono kick, mono snare, stereo kit --> (0) (1) (2 3) case 4: - mixer.AddChannel(SongStem.Drums1, _rbMetadata.Indices.Drums[0..1], _rbMetadata.Panning.Drums![0..2]); - mixer.AddChannel(SongStem.Drums2, _rbMetadata.Indices.Drums[1..2], _rbMetadata.Panning.Drums[2..4]); - mixer.AddChannel(SongStem.Drums3, _rbMetadata.Indices.Drums[2..4], _rbMetadata.Panning.Drums[4..8]); + mixer.AddChannel(SongStem.Drums1, RBMetadata.Indices.Drums[0..1], RBMetadata.Panning.Drums![0..2]); + mixer.AddChannel(SongStem.Drums2, RBMetadata.Indices.Drums[1..2], RBMetadata.Panning.Drums[2..4]); + mixer.AddChannel(SongStem.Drums3, RBMetadata.Indices.Drums[2..4], RBMetadata.Panning.Drums[4..8]); break; //drum (0 1 2 3 4): mono kick, stereo snare, stereo kit --> (0) (1 2) (3 4) case 5: - mixer.AddChannel(SongStem.Drums1, _rbMetadata.Indices.Drums[0..1], _rbMetadata.Panning.Drums![0..2]); - mixer.AddChannel(SongStem.Drums2, _rbMetadata.Indices.Drums[1..3], _rbMetadata.Panning.Drums[2..6]); - mixer.AddChannel(SongStem.Drums3, _rbMetadata.Indices.Drums[3..5], _rbMetadata.Panning.Drums[6..10]); + mixer.AddChannel(SongStem.Drums1, RBMetadata.Indices.Drums[0..1], RBMetadata.Panning.Drums![0..2]); + mixer.AddChannel(SongStem.Drums2, RBMetadata.Indices.Drums[1..3], RBMetadata.Panning.Drums[2..6]); + mixer.AddChannel(SongStem.Drums3, RBMetadata.Indices.Drums[3..5], RBMetadata.Panning.Drums[6..10]); break; //drum (0 1 2 3 4 5): stereo kick, stereo snare, stereo kit --> (0 1) (2 3) (4 5) case 6: - mixer.AddChannel(SongStem.Drums1, _rbMetadata.Indices.Drums[0..2], _rbMetadata.Panning.Drums![0..4]); - mixer.AddChannel(SongStem.Drums2, _rbMetadata.Indices.Drums[2..4], _rbMetadata.Panning.Drums[4..8]); - mixer.AddChannel(SongStem.Drums3, _rbMetadata.Indices.Drums[4..6], _rbMetadata.Panning.Drums[8..12]); + mixer.AddChannel(SongStem.Drums1, RBMetadata.Indices.Drums[0..2], RBMetadata.Panning.Drums![0..4]); + mixer.AddChannel(SongStem.Drums2, RBMetadata.Indices.Drums[2..4], RBMetadata.Panning.Drums[4..8]); + mixer.AddChannel(SongStem.Drums3, RBMetadata.Indices.Drums[4..6], RBMetadata.Panning.Drums[8..12]); break; } } - if (_rbMetadata.Indices.Bass.Length > 0 && !ignoreStems.Contains(SongStem.Bass)) - mixer.AddChannel(SongStem.Bass, _rbMetadata.Indices.Bass, _rbMetadata.Panning.Bass!); + if (RBMetadata.Indices.Bass.Length > 0 && !ignoreStems.Contains(SongStem.Bass)) + mixer.AddChannel(SongStem.Bass, RBMetadata.Indices.Bass, RBMetadata.Panning.Bass!); - if (_rbMetadata.Indices.Guitar.Length > 0 && !ignoreStems.Contains(SongStem.Guitar)) - mixer.AddChannel(SongStem.Guitar, _rbMetadata.Indices.Guitar, _rbMetadata.Panning.Guitar!); + if (RBMetadata.Indices.Guitar.Length > 0 && !ignoreStems.Contains(SongStem.Guitar)) + mixer.AddChannel(SongStem.Guitar, RBMetadata.Indices.Guitar, RBMetadata.Panning.Guitar!); - if (_rbMetadata.Indices.Keys.Length > 0 && !ignoreStems.Contains(SongStem.Keys)) - mixer.AddChannel(SongStem.Keys, _rbMetadata.Indices.Keys, _rbMetadata.Panning.Keys!); + if (RBMetadata.Indices.Keys.Length > 0 && !ignoreStems.Contains(SongStem.Keys)) + mixer.AddChannel(SongStem.Keys, RBMetadata.Indices.Keys, RBMetadata.Panning.Keys!); - if (_rbMetadata.Indices.Vocals.Length > 0 && !ignoreStems.Contains(SongStem.Vocals)) - mixer.AddChannel(SongStem.Vocals, _rbMetadata.Indices.Vocals, _rbMetadata.Panning.Vocals!); + if (RBMetadata.Indices.Vocals.Length > 0 && !ignoreStems.Contains(SongStem.Vocals)) + mixer.AddChannel(SongStem.Vocals, RBMetadata.Indices.Vocals, RBMetadata.Panning.Vocals!); - if (_rbMetadata.Indices.Track.Length > 0 && !ignoreStems.Contains(SongStem.Song)) - mixer.AddChannel(SongStem.Song, _rbMetadata.Indices.Track, _rbMetadata.Panning.Track!); + if (RBMetadata.Indices.Track.Length > 0 && !ignoreStems.Contains(SongStem.Song)) + mixer.AddChannel(SongStem.Song, RBMetadata.Indices.Track, RBMetadata.Panning.Track!); - if (_rbMetadata.Indices.Crowd.Length > 0 && !ignoreStems.Contains(SongStem.Crowd)) - mixer.AddChannel(SongStem.Crowd, _rbMetadata.Indices.Crowd, _rbMetadata.Panning.Crowd!); + if (RBMetadata.Indices.Crowd.Length > 0 && !ignoreStems.Contains(SongStem.Crowd)) + mixer.AddChannel(SongStem.Crowd, RBMetadata.Indices.Crowd, RBMetadata.Panning.Crowd!); if (mixer.Channels.Count == 0) { @@ -205,775 +262,410 @@ public override DateTime GetAddTime() return LoadAudio(speed, 0, SongStem.Crowd); } - public override YARGImage? LoadAlbumData() - { - var bytes = LoadRawImageData(); - return bytes.IsAllocated ? new YARGImage(bytes) : null; - } - - public override FixedArray LoadMiloData() - { - return UpdateMilo != null && UpdateMilo.Value.Exists() - ? FixedArray.Load(UpdateMilo.Value.FullName) - : FixedArray.Null; - } + protected abstract Stream? GetMidiStream(); + protected abstract Stream? GetMoggStream(); - public virtual void Serialize(BinaryWriter writer, CategoryCacheWriteNode node) + public struct ScanNode { - writer.Write(_updateMidi != null); - _updateMidi?.Serialize(writer); - - SerializeMetadata(writer, node); - - WriteUpdateInfo(UpdateMogg, writer); - WriteUpdateInfo(UpdateMilo, writer); - WriteUpdateInfo(UpdateImage, writer); - - writer.Write(_rbMetadata.AnimTempo); - writer.Write(_rbMetadata.SongID); - writer.Write(_rbMetadata.VocalPercussionBank); - writer.Write(_rbMetadata.VocalSongScrollSpeed); - writer.Write(_rbMetadata.VocalGender); - writer.Write(_rbMetadata.VocalTonicNote); - writer.Write(_rbMetadata.SongTonality); - writer.Write(_rbMetadata.TuningOffsetCents); - writer.Write(_rbMetadata.VenueVersion); - writer.Write(_rbMetadata.DrumBank); - - RBAudio.WriteArray(in _rbMetadata.RealGuitarTuning, writer); - RBAudio.WriteArray(in _rbMetadata.RealBassTuning, writer); - - _rbMetadata.Indices.Serialize(writer); - _rbMetadata.Panning.Serialize(writer); - - WriteStringArray(_rbMetadata.Soloes, writer); - WriteStringArray(_rbMetadata.VideoVenues, writer); - - unsafe - { - fixed (RBCONDifficulties* ptr = &_rbDifficulties) - { - var span = new ReadOnlySpan(ptr, sizeof(RBCONDifficulties)); - writer.Write(span); - } - } - } + public static readonly ScanNode Default = new() + { + Metadata = SongMetadata.Default, + RBMetadata = RBMetadata.Default, + Settings = LoaderSettings.Default, + Parts = AvailableParts.Default, + Difficulties = RBCONDifficulties.Default, + YearAsNumber = int.MaxValue, + }; - protected abstract bool IsMoggValid(Stream? file); - protected abstract FixedArray LoadMidiFile(Stream? file); - protected abstract Stream? GetMidiStream(); + public string? Location; + public SongMetadata Metadata; + public RBMetadata RBMetadata; + public LoaderSettings Settings; + public AvailableParts Parts; + public RBCONDifficulties Difficulties; - protected RBCONEntry() : base() - { - _rbMetadata = RBMetadata.Default; - _rbDifficulties = RBCONDifficulties.Default; - _parseSettings.DrumsType = DrumsType.FourLane; - _parseSettings.NoteSnapThreshold = NOTE_SNAP_THRESHOLD; + public int YearAsNumber; + public ulong SongLength; } - protected RBCONEntry(AbridgedFileInfo? updateMidi, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) - : base(stream, strings) + protected static (ScanResult Result, ScanNode Info) ProcessDTAs(string nodename, DTAEntry baseDTA, CONModification modification) { - _updateMidi = updateMidi; - _upgrade = upgrade; - - UpdateMogg = stream.ReadBoolean() ? new AbridgedFileInfo(stream.ReadString(), false) : null; - UpdateMilo = stream.ReadBoolean() ? new AbridgedFileInfo(stream.ReadString(), false) : null; - UpdateImage = stream.ReadBoolean() ? new AbridgedFileInfo(stream.ReadString(), false) : null; - - _rbMetadata.AnimTempo = stream.Read(Endianness.Little); - _rbMetadata.SongID = stream.ReadString(); - _rbMetadata.VocalPercussionBank = stream.ReadString(); - _rbMetadata.VocalSongScrollSpeed = stream.Read(Endianness.Little); - _rbMetadata.VocalGender = stream.ReadBoolean(); - _rbMetadata.VocalTonicNote = stream.Read(Endianness.Little); - _rbMetadata.SongTonality = stream.ReadBoolean(); - _rbMetadata.TuningOffsetCents = stream.Read(Endianness.Little); - _rbMetadata.VenueVersion = stream.Read(Endianness.Little); - _rbMetadata.DrumBank = stream.ReadString(); - - _rbMetadata.RealGuitarTuning = RBAudio.ReadArray(stream); - _rbMetadata.RealBassTuning = RBAudio.ReadArray(stream); - - _rbMetadata.Indices = new RBAudio(stream); - _rbMetadata.Panning = new RBAudio(stream); - - _rbMetadata.Soloes = ReadStringArray(stream); - _rbMetadata.VideoVenues = ReadStringArray(stream); - - unsafe - { - fixed (RBCONDifficulties* ptr = &_rbDifficulties) + float[]? volumes = null; + float[]? pans = null; + float[]? cores = null; + + var info = ScanNode.Default; + void ParseDTA(DTAEntry entry) + { + if (entry.Name != null) { info.Metadata.Name = entry.Name; } + if (entry.Artist != null) { info.Metadata.Artist = entry.Artist; } + if (entry.Charter != null) { info.Metadata.Charter = entry.Charter; } + if (entry.Genre != null) { info.Metadata.Genre = entry.Genre; } + if (entry.YearAsNumber != null) { - var span = new Span(ptr, sizeof(RBCONDifficulties)); - stream.Read(span); + info.YearAsNumber = entry.YearAsNumber.Value; + info.Metadata.Year = info.YearAsNumber.ToString(); + } + if (entry.Source != null) { info.Metadata.Source = entry.Source; } + if (entry.Playlist != null) { info.Metadata.Playlist = entry.Playlist; } + if (entry.SongLength != null) { info.SongLength = entry.SongLength.Value; } + if (entry.IsMaster != null) { info.Metadata.IsMaster = entry.IsMaster.Value; } + if (entry.AlbumTrack != null) { info.Metadata.AlbumTrack = entry.AlbumTrack.Value; } + if (entry.PreviewStart != null) + { + info.Metadata.PreviewStart = entry.PreviewStart.Value; + info.Metadata.PreviewEnd = entry.PreviewEnd!.Value; + } + if (entry.HopoThreshold != null) { info.Settings.HopoThreshold = entry.HopoThreshold.Value; } + if (entry.SongRating != null) { info.Metadata.SongRating = entry.SongRating.Value; } + if (entry.VocalPercussionBank != null) { info.RBMetadata.VocalPercussionBank = entry.VocalPercussionBank; } + if (entry.VocalGender != null) { info.RBMetadata.VocalGender = entry.VocalGender.Value; } + if (entry.VocalSongScrollSpeed != null) { info.RBMetadata.VocalSongScrollSpeed = entry.VocalSongScrollSpeed.Value; } + if (entry.VocalTonicNote != null) { info.RBMetadata.VocalTonicNote = entry.VocalTonicNote.Value; } + if (entry.VideoVenues != null) { info.RBMetadata.VideoVenues = entry.VideoVenues; } + if (entry.DrumBank != null) { info.RBMetadata.DrumBank = entry.DrumBank; } + if (entry.SongID != null) { info.RBMetadata.SongID = entry.SongID; } + if (entry.SongTonality != null) { info.RBMetadata.SongTonality = entry.SongTonality.Value; } + if (entry.Soloes != null) { info.RBMetadata.Soloes = entry.Soloes; } + if (entry.AnimTempo != null) { info.RBMetadata.AnimTempo = entry.AnimTempo.Value; } + if (entry.TuningOffsetCents != null) { info.RBMetadata.TuningOffsetCents = entry.TuningOffsetCents.Value; } + if (entry.RealGuitarTuning != null) { info.RBMetadata.RealGuitarTuning = entry.RealGuitarTuning; } + if (entry.RealBassTuning != null) { info.RBMetadata.RealBassTuning = entry.RealBassTuning; } + + if (entry.Cores != null) { cores = entry.Cores; } + if (entry.Volumes != null) { volumes = entry.Volumes; } + if (entry.Pans != null) { pans = entry.Pans; } + + if (entry.Location != null) { info.Location = entry.Location; } + + if (entry.Indices != null) + { + var crowd = info.RBMetadata.Indices.Crowd; + info.RBMetadata.Indices = entry.Indices.Value; + info.RBMetadata.Indices.Crowd = crowd; } - } - } - protected DTAResult Init(string nodeName, in YARGTextContainer container, Dictionary> updates, Dictionary, RBProUpgrade)> upgrades, string defaultPlaylist) - { - var dtaResults = ParseDTA(nodeName, container); - ApplyRBCONUpdates(ref dtaResults, nodeName, updates); - ApplyRBProUpgrade(nodeName, upgrades); + if (entry.CrowdChannels != null) { info.RBMetadata.Indices.Crowd = entry.CrowdChannels; } - if (dtaResults.pans.Length == 0 || dtaResults.volumes.Length == 0 || dtaResults.cores.Length == 0) - { - throw new Exception("Panning & Volume mappings not set from DTA"); + if (entry.Difficulties.Band >= 0) { info.Difficulties.Band = entry.Difficulties.Band; } + if (entry.Difficulties.FiveFretGuitar >= 0) { info.Difficulties.FiveFretGuitar = entry.Difficulties.FiveFretGuitar; } + if (entry.Difficulties.FiveFretBass >= 0) { info.Difficulties.FiveFretBass = entry.Difficulties.FiveFretBass; } + if (entry.Difficulties.FiveFretRhythm >= 0) { info.Difficulties.FiveFretRhythm = entry.Difficulties.FiveFretRhythm; } + if (entry.Difficulties.FiveFretCoop >= 0) { info.Difficulties.FiveFretCoop = entry.Difficulties.FiveFretCoop; } + if (entry.Difficulties.Keys >= 0) { info.Difficulties.Keys = entry.Difficulties.Keys; } + if (entry.Difficulties.FourLaneDrums >= 0) { info.Difficulties.FourLaneDrums = entry.Difficulties.FourLaneDrums; } + if (entry.Difficulties.ProDrums >= 0) { info.Difficulties.ProDrums = entry.Difficulties.ProDrums; } + if (entry.Difficulties.ProGuitar >= 0) { info.Difficulties.ProGuitar = entry.Difficulties.ProGuitar; } + if (entry.Difficulties.ProBass >= 0) { info.Difficulties.ProBass = entry.Difficulties.ProBass; } + if (entry.Difficulties.ProKeys >= 0) { info.Difficulties.ProKeys = entry.Difficulties.ProKeys; } + if (entry.Difficulties.LeadVocals >= 0) { info.Difficulties.LeadVocals = entry.Difficulties.LeadVocals; } + if (entry.Difficulties.HarmonyVocals >= 0) { info.Difficulties.HarmonyVocals = entry.Difficulties.HarmonyVocals; } } - FinalizeRBCONAudioValues(in dtaResults); - - if (_metadata.Playlist.Length == 0) - _metadata.Playlist = defaultPlaylist; - return dtaResults; - } - - - protected virtual FixedArray LoadRawImageData() - { - return UpdateImage != null && UpdateImage.Value.Exists() - ? FixedArray.Load(UpdateImage.Value.FullName) - : FixedArray.Null; - } - protected virtual Stream? GetMoggStream() - { - if (UpdateMogg == null) + ParseDTA(baseDTA); + if (modification.UpdateDTA != null) { - return null; + ParseDTA(modification.UpdateDTA); } - var mogg = UpdateMogg.Value; - if (!File.Exists(mogg.FullName)) + if (modification.UpgradeDTA != null) { - return null; + ParseDTA(modification.UpgradeDTA); } - if (mogg.FullName.EndsWith(".yarg_mogg")) + if (info.Metadata.Name.Length == 0) { - return new YargMoggReadStream(mogg.FullName); + return (ScanResult.NoName, info); } - return new FileStream(mogg.FullName, FileMode.Open, FileAccess.Read); - } - protected FixedArray LoadUpdateMidiFile() - { - return _updateMidi != null && _updateMidi.Value.IsStillValid(false) - ? FixedArray.Load(_updateMidi.Value.FullName) - : FixedArray.Null; - } - - protected ScanResult ParseRBCONMidi(Stream? file) - { - if (_metadata.Name.Length == 0) + if (info.Location == null || pans == null || volumes == null || cores == null) { - return ScanResult.NoName; + return (ScanResult.DTAError, info); } - if (!IsMoggValid(file)) + if (info.Difficulties.FourLaneDrums > -1) { - return ScanResult.MoggError; + SetRank(ref info.Parts.FourLaneDrums.Intensity, info.Difficulties.FourLaneDrums, DrumDiffMap); + if (info.Parts.ProDrums.Intensity == -1) + { + info.Parts.ProDrums.Intensity = info.Parts.FourLaneDrums.Intensity; + } } - - try + if (info.Difficulties.FiveFretGuitar > -1) { - using var chartFile = LoadMidiFile(file); - using var updateFile = LoadUpdateMidiFile(); - using var upgradeFile = _upgrade != null ? _upgrade.LoadUpgradeMidi() : FixedArray.Null; - - DrumPreparseHandler drumTracker = new() + SetRank(ref info.Parts.FiveFretGuitar.Intensity, info.Difficulties.FiveFretGuitar, GuitarDiffMap); + if (info.Parts.ProGuitar_17Fret.Intensity == -1) { - Type = DrumsType.ProDrums - }; - - long bufLength = 0; - if (_updateMidi != null) + info.Parts.ProGuitar_22Fret.Intensity = info.Parts.ProGuitar_17Fret.Intensity = info.Parts.FiveFretGuitar.Intensity; + } + } + if (info.Difficulties.FiveFretBass > -1) + { + SetRank(ref info.Parts.FiveFretBass.Intensity, info.Difficulties.FiveFretBass, GuitarDiffMap); + if (info.Parts.ProBass_17Fret.Intensity == -1) { - if (!updateFile.IsAllocated) - return ScanResult.MissingUpdateMidi; - - if (!ParseMidi(in updateFile, drumTracker, ref _parts)) - return ScanResult.MultipleMidiTrackNames_Update; - - bufLength += updateFile.Length; + info.Parts.ProBass_22Fret.Intensity = info.Parts.ProBass_17Fret.Intensity = info.Parts.FiveFretGuitar.Intensity; } - - if (_upgrade != null) + } + if (info.Difficulties.LeadVocals > -1) + { + SetRank(ref info.Parts.LeadVocals.Intensity, info.Difficulties.LeadVocals, GuitarDiffMap); + if (info.Parts.HarmonyVocals.Intensity == -1) { - if (!upgradeFile.IsAllocated) - return ScanResult.MissingUpgradeMidi; - - if (!ParseMidi(in upgradeFile, drumTracker, ref _parts)) - return ScanResult.MultipleMidiTrackNames_Upgrade; - - bufLength += upgradeFile.Length; + info.Parts.HarmonyVocals.Intensity = info.Parts.LeadVocals.Intensity; } - - if (!chartFile.IsAllocated) - return ScanResult.MissingMidi; - - if (!ParseMidi(in chartFile, drumTracker, ref _parts)) - return ScanResult.MultipleMidiTrackNames; - - bufLength += chartFile.Length; - - SetDrums(ref _parts, drumTracker); - if (!CheckScanValidity(in _parts)) + } + if (info.Difficulties.Keys > -1) + { + SetRank(ref info.Parts.Keys.Intensity, info.Difficulties.Keys, GuitarDiffMap); + if (info.Parts.ProKeys.Intensity == -1) { - return ScanResult.NoNotes; + info.Parts.ProKeys.Intensity = info.Parts.Keys.Intensity; } - - using var buffer = FixedArray.Alloc(bufLength); - unsafe + } + if (info.Difficulties.ProGuitar > -1) + { + SetRank(ref info.Parts.ProGuitar_17Fret.Intensity, info.Difficulties.ProGuitar, RealGuitarDiffMap); + info.Parts.ProGuitar_22Fret.Intensity = info.Parts.ProGuitar_17Fret.Intensity; + if (info.Parts.FiveFretGuitar.Intensity == -1) { - System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.Ptr, chartFile.Ptr, (uint) chartFile.Length); - - long offset = chartFile.Length; - if (updateFile.IsAllocated) - { - System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.Ptr + offset, updateFile.Ptr, (uint) updateFile.Length); - offset += updateFile.Length; - } - - if (upgradeFile.IsAllocated) - { - System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.Ptr + offset, upgradeFile.Ptr, (uint) upgradeFile.Length); - } + info.Parts.FiveFretGuitar.Intensity = info.Parts.ProGuitar_17Fret.Intensity; } - _hash = HashWrapper.Hash(buffer.ReadOnlySpan); - return ScanResult.Success; } - catch + if (info.Difficulties.ProBass > -1) { - return ScanResult.PossibleCorruption; + SetRank(ref info.Parts.ProBass_17Fret.Intensity, info.Difficulties.ProBass, RealGuitarDiffMap); + info.Parts.ProBass_22Fret.Intensity = info.Parts.ProBass_17Fret.Intensity; + if (info.Parts.FiveFretBass.Intensity == -1) + { + info.Parts.FiveFretBass.Intensity = info.Parts.ProBass_17Fret.Intensity; + } } - } - - private void ApplyRBCONUpdates(ref DTAResult mainResult, string nodeName, Dictionary> updates) - { - if (updates.TryGetValue(nodeName, out var updateList)) + if (info.Difficulties.ProKeys > -1) { - foreach (var update in updateList.Values) + SetRank(ref info.Parts.ProKeys.Intensity, info.Difficulties.ProKeys, RealKeysDiffMap); + if (info.Parts.Keys.Intensity == -1) { - try - { - var updateResults = ParseDTA(nodeName, update.Containers); - Update(update, nodeName, updateResults); - - if (updateResults.cores.Length > 0) - { - mainResult.cores = updateResults.cores; - } - - if (updateResults.volumes.Length > 0) - { - mainResult.volumes = updateResults.volumes; - } - - if (updateResults.pans.Length > 0) - { - mainResult.pans = updateResults.pans; - } - } - catch (Exception ex) - { - YargLogger.LogException(ex, $"Error processing CON Update {update.BaseDirectory} - {nodeName}!"); - } + info.Parts.Keys.Intensity = info.Parts.ProKeys.Intensity; } } - } - - private void ApplyRBProUpgrade(string nodeName, Dictionary Container, RBProUpgrade Upgrade)> upgrades) - { - if (upgrades.TryGetValue(nodeName, out var upgrade)) + if (info.Difficulties.ProDrums > -1) { - try + SetRank(ref info.Parts.ProDrums.Intensity, info.Difficulties.ProDrums, DrumDiffMap); + if (info.Parts.FourLaneDrums.Intensity == -1) { - ParseDTA(nodeName, upgrade.Container); - _upgrade = upgrade.Upgrade; + info.Parts.FourLaneDrums.Intensity = info.Parts.ProDrums.Intensity; } - catch (Exception ex) + } + if (info.Difficulties.HarmonyVocals > -1) + { + SetRank(ref info.Parts.HarmonyVocals.Intensity, info.Difficulties.HarmonyVocals, DrumDiffMap); + if (info.Parts.LeadVocals.Intensity == -1) { - YargLogger.LogException(ex, $"Error processing CON Upgrade {nodeName}!"); + info.Parts.LeadVocals.Intensity = info.Parts.HarmonyVocals.Intensity; } } - } + if (info.Difficulties.Band > -1) + { + SetRank(ref info.Parts.BandDifficulty.Intensity, info.Difficulties.Band, BandDiffMap); + info.Parts.BandDifficulty.SubTracks = 1; + } - private DTAResult ParseDTA(string nodeName, params YARGTextContainer[] containers) - { - var result = DTAResult.Empty; - for (int i = 0; i < containers.Length; ++i) + unsafe { - var container = containers[i]; - while (YARGDTAReader.StartNode(ref container)) + var usedIndices = stackalloc bool[pans.Length]; + float[] CalculateStemValues(int[] indices) { - string name = YARGDTAReader.GetNameOfNode(ref container, false); - switch (name) + float[] values = new float[2 * indices.Length]; + for (int i = 0; i < indices.Length; i++) { - case "name": _metadata.Name = YARGDTAReader.ExtractText(ref container); break; - case "artist": _metadata.Artist = YARGDTAReader.ExtractText(ref container); break; - case "master": _metadata.IsMaster = YARGDTAReader.ExtractBoolean_FlippedDefault(ref container); break; - case "context": /*Context = container.Read();*/ break; - case "song": SongLoop(ref result, ref container); break; - case "song_vocals": while (YARGDTAReader.StartNode(ref container)) YARGDTAReader.EndNode(ref container); break; - case "song_scroll_speed": _rbMetadata.VocalSongScrollSpeed = YARGDTAReader.ExtractUInt32(ref container); break; - case "tuning_offset_cents": _rbMetadata.TuningOffsetCents = YARGDTAReader.ExtractInt32(ref container); break; - case "bank": _rbMetadata.VocalPercussionBank = YARGDTAReader.ExtractText(ref container); break; - case "anim_tempo": - { - string val = YARGDTAReader.ExtractText(ref container); - _rbMetadata.AnimTempo = val switch - { - "kTempoSlow" => 16, - "kTempoMedium" => 32, - "kTempoFast" => 64, - _ => uint.Parse(val) - }; - break; - } - case "preview": - _metadata.PreviewStart = YARGDTAReader.ExtractInt64(ref container); - _metadata.PreviewEnd = YARGDTAReader.ExtractInt64(ref container); - break; - case "rank": DifficultyLoop(ref container); break; - case "solo": _rbMetadata.Soloes = YARGDTAReader.ExtractArray_String(ref container); break; - case "genre": _metadata.Genre = YARGDTAReader.ExtractText(ref container); break; - case "decade": /*Decade = container.ExtractText();*/ break; - case "vocal_gender": _rbMetadata.VocalGender = YARGDTAReader.ExtractText(ref container) == "male"; break; - case "format": /*Format = container.Read();*/ break; - case "version": _rbMetadata.VenueVersion = YARGDTAReader.ExtractUInt32(ref container); break; - case "fake": /*IsFake = container.ExtractText();*/ break; - case "downloaded": /*Downloaded = container.ExtractText();*/ break; - case "game_origin": - { - string str = YARGDTAReader.ExtractText(ref container); - if ((str == "ugc" || str == "ugc_plus")) - { - if (!nodeName.StartsWith("UGC_")) - _metadata.Source = "customs"; - } - else if (str == "#ifdef") - { - string conditional = YARGDTAReader.ExtractText(ref container); - if (conditional == "CUSTOMSOURCE") - { - _metadata.Source = YARGDTAReader.ExtractText(ref container); - } - else - { - _metadata.Source = "customs"; - } - } - else - { - _metadata.Source = str; - } - - //// if the source is any official RB game or its DLC, charter = Harmonix - //if (SongSources.GetSource(str).Type == SongSources.SourceType.RB) - //{ - // _charter = "Harmonix"; - //} - - //// if the source is meant for usage in TBRB, it's a master track - //// TODO: NEVER assume localized version contains "Beatles" - //if (SongSources.SourceToGameName(str).Contains("Beatles")) _isMaster = true; - break; - } - case "song_id": _rbMetadata.SongID = YARGDTAReader.ExtractText(ref container); break; - case "rating": _metadata.SongRating = YARGDTAReader.ExtractUInt32(ref container); break; - case "short_version": /*ShortVersion = container.Read();*/ break; - case "album_art": /*HasAlbumArt = container.ExtractBoolean();*/ break; - case "year_released": - case "year_recorded": YearAsNumber = YARGDTAReader.ExtractInt32(ref container); break; - case "album_name": _metadata.Album = YARGDTAReader.ExtractText(ref container); break; - case "album_track_number": _metadata.AlbumTrack = YARGDTAReader.ExtractInt32(ref container); break; - case "pack_name": _metadata.Playlist = YARGDTAReader.ExtractText(ref container); break; - case "base_points": /*BasePoints = container.Read();*/ break; - case "band_fail_cue": /*BandFailCue = container.ExtractText();*/ break; - case "drum_bank": _rbMetadata.DrumBank = YARGDTAReader.ExtractText(ref container); break; - case "song_length": _metadata.SongLength = YARGDTAReader.ExtractUInt64(ref container); break; - case "sub_genre": /*Subgenre = container.ExtractText();*/ break; - case "author": _metadata.Charter = YARGDTAReader.ExtractText(ref container); break; - case "guide_pitch_volume": /*GuidePitchVolume = container.ReadFloat();*/ break; - case "encoding": - var encoding = YARGDTAReader.ExtractText(ref container).ToLower() switch - { - "latin1" => YARGTextReader.Latin1, - "utf-8" or - "utf8" => Encoding.UTF8, - _ => container.Encoding - }; - - if (container.Encoding != encoding) - { - string Convert(string str) - { - byte[] bytes = container.Encoding.GetBytes(str); - return encoding.GetString(bytes); - } - - if (_metadata.Name != SongMetadata.DEFAULT_NAME) - _metadata.Name = Convert(_metadata.Name); - - if (_metadata.Artist != SongMetadata.DEFAULT_ARTIST) - _metadata.Artist = Convert(_metadata.Artist); - - if (_metadata.Album != SongMetadata.DEFAULT_ALBUM) - _metadata.Album = Convert(_metadata.Album); - - if (_metadata.Genre != SongMetadata.DEFAULT_GENRE) - _metadata.Genre = Convert(_metadata.Genre); - - if (_metadata.Charter != SongMetadata.DEFAULT_CHARTER) - _metadata.Charter = Convert(_metadata.Charter); - - if (_metadata.Source != SongMetadata.DEFAULT_SOURCE) - _metadata.Source = Convert(_metadata.Source); - - if (_metadata.Playlist.Str.Length != 0) - _metadata.Playlist = Convert(_metadata.Playlist); - container.Encoding = encoding; - } - - break; - case "vocal_tonic_note": _rbMetadata.VocalTonicNote = YARGDTAReader.ExtractUInt32(ref container); break; - case "song_tonality": _rbMetadata.SongTonality = YARGDTAReader.ExtractBoolean(ref container); break; - case "alternate_path": result.alternatePath = YARGDTAReader.ExtractBoolean(ref container); break; - case "real_guitar_tuning": _rbMetadata.RealGuitarTuning = YARGDTAReader.ExtractArray_Int(ref container); break; - case "real_bass_tuning": _rbMetadata.RealBassTuning = YARGDTAReader.ExtractArray_Int(ref container); break; - case "video_venues": _rbMetadata.VideoVenues = YARGDTAReader.ExtractArray_String(ref container); break; - case "extra_authoring": - { - StringBuilder authors = new(); - foreach (string str in YARGDTAReader.ExtractArray_String(ref container)) - { - if (str == "disc_update") - result.discUpdate = true; - else if (authors.Length == 0 && _metadata.Charter == SongMetadata.DEFAULT_CHARTER) - authors.Append(str); - else - { - if (authors.Length == 0) - authors.Append(_metadata.Charter); - authors.Append(", " + str); - } - } - - if (authors.Length == 0) - authors.Append(_metadata.Charter); - - _metadata.Charter = authors.ToString(); - } - break; + float theta = (pans[indices[i]] + 1) * ((float) Math.PI / 4); + float volRatio = (float) Math.Pow(10, volumes[indices[i]] / 20); + values[2 * i] = volRatio * (float) Math.Cos(theta); + values[2 * i + 1] = volRatio * (float) Math.Sin(theta); + usedIndices[indices[i]] = true; } - YARGDTAReader.EndNode(ref container); + return values; } - } - return result; - } - private void SongLoop(ref DTAResult result, ref YARGTextContainer container) - { - while (YARGDTAReader.StartNode(ref container)) - { - string descriptor = YARGDTAReader.GetNameOfNode(ref container, false); - switch (descriptor) + if (info.RBMetadata.Indices.Drums.Length > 0) { - case "name": result.location = YARGDTAReader.ExtractText(ref container); break; - case "tracks": TracksLoop(ref container); break; - case "crowd_channels": _rbMetadata.Indices.Crowd = YARGDTAReader.ExtractArray_Int(ref container); break; - //case "vocal_parts": VocalParts = container.Read(); break; - case "pans": result.pans = YARGDTAReader.ExtractArray_Float(ref container); break; - case "vols": result.volumes = YARGDTAReader.ExtractArray_Float(ref container); break; - case "cores": result.cores = YARGDTAReader.ExtractArray_Float(ref container); break; - case "hopo_threshold": _parseSettings.HopoThreshold = YARGDTAReader.ExtractInt64(ref container); break; + info.RBMetadata.Panning.Drums = CalculateStemValues(info.RBMetadata.Indices.Drums); } - YARGDTAReader.EndNode(ref container); - } - } - private void TracksLoop(ref YARGTextContainer container) - { - var crowd = _rbMetadata.Indices.Crowd; - _rbMetadata.Indices = RBAudio.Empty; - _rbMetadata.Indices.Crowd = crowd; - while (YARGDTAReader.StartNode(ref container)) - { - while (YARGDTAReader.StartNode(ref container)) + if (info.RBMetadata.Indices.Bass.Length > 0) { - switch (YARGDTAReader.GetNameOfNode(ref container, false)) - { - case "drum" : _rbMetadata.Indices.Drums = YARGDTAReader.ExtractArray_Int(ref container); break; - case "bass" : _rbMetadata.Indices.Bass = YARGDTAReader.ExtractArray_Int(ref container); break; - case "guitar": _rbMetadata.Indices.Guitar = YARGDTAReader.ExtractArray_Int(ref container); break; - case "keys" : _rbMetadata.Indices.Keys = YARGDTAReader.ExtractArray_Int(ref container); break; - case "vocals": _rbMetadata.Indices.Vocals = YARGDTAReader.ExtractArray_Int(ref container); break; - } - YARGDTAReader.EndNode(ref container); + info.RBMetadata.Panning.Bass = CalculateStemValues(info.RBMetadata.Indices.Bass); } - YARGDTAReader.EndNode(ref container); - } - } - private static readonly int[] BandDiffMap = { 163, 215, 243, 267, 292, 345 }; - private static readonly int[] GuitarDiffMap = { 139, 176, 221, 267, 333, 409 }; - private static readonly int[] BassDiffMap = { 135, 181, 228, 293, 364, 436 }; - private static readonly int[] DrumDiffMap = { 124, 151, 178, 242, 345, 448 }; - private static readonly int[] KeysDiffMap = { 153, 211, 269, 327, 385, 443 }; - private static readonly int[] VocalsDiffMap = { 132, 175, 218, 279, 353, 427 }; - private static readonly int[] RealGuitarDiffMap = { 150, 205, 264, 323, 382, 442 }; - private static readonly int[] RealBassDiffMap = { 150, 208, 267, 325, 384, 442 }; - private static readonly int[] RealDrumsDiffMap = { 124, 151, 178, 242, 345, 448 }; - private static readonly int[] RealKeysDiffMap = { 153, 211, 269, 327, 385, 443 }; - private static readonly int[] HarmonyDiffMap = { 132, 175, 218, 279, 353, 427 }; + if (info.RBMetadata.Indices.Guitar.Length > 0) + { + info.RBMetadata.Panning.Guitar = CalculateStemValues(info.RBMetadata.Indices.Guitar); + } - private void DifficultyLoop(ref YARGTextContainer container) - { - int diff; - while (YARGDTAReader.StartNode(ref container)) - { - string name = YARGDTAReader.GetNameOfNode(ref container, false); - diff = YARGDTAReader.ExtractInt32(ref container); - switch (name) - { - case "drum": - case "drums": - _rbDifficulties.FourLaneDrums = (short) diff; - SetRank(ref _parts.FourLaneDrums.Intensity, diff, DrumDiffMap); - if (_parts.ProDrums.Intensity == -1) - { - _parts.ProDrums.Intensity = _parts.FourLaneDrums.Intensity; - } - break; - case "guitar": - _rbDifficulties.FiveFretGuitar = (short) diff; - SetRank(ref _parts.FiveFretGuitar.Intensity, diff, GuitarDiffMap); - if (_parts.ProGuitar_17Fret.Intensity == -1) - { - _parts.ProGuitar_22Fret.Intensity = _parts.ProGuitar_17Fret.Intensity = _parts.FiveFretGuitar.Intensity; - } - break; - case "bass": - _rbDifficulties.FiveFretBass = (short) diff; - SetRank(ref _parts.FiveFretBass.Intensity, diff, BassDiffMap); - if (_parts.ProBass_17Fret.Intensity == -1) - { - _parts.ProBass_22Fret.Intensity = _parts.ProBass_17Fret.Intensity = _parts.FiveFretBass.Intensity; - } - break; - case "vocals": - _rbDifficulties.LeadVocals = (short) diff; - SetRank(ref _parts.LeadVocals.Intensity, diff, VocalsDiffMap); - if (_parts.HarmonyVocals.Intensity == -1) - { - _parts.HarmonyVocals.Intensity = _parts.LeadVocals.Intensity; - } - break; - case "keys": - _rbDifficulties.Keys = (short) diff; - SetRank(ref _parts.Keys.Intensity, diff, KeysDiffMap); - if (_parts.ProKeys.Intensity == -1) - { - _parts.ProKeys.Intensity = _parts.Keys.Intensity; - } - break; - case "realGuitar": - case "real_guitar": - _rbDifficulties.ProGuitar = (short) diff; - SetRank(ref _parts.ProGuitar_17Fret.Intensity, diff, RealGuitarDiffMap); - _parts.ProGuitar_22Fret.Intensity = _parts.ProGuitar_17Fret.Intensity; - if (_parts.FiveFretGuitar.Intensity == -1) - { - _parts.FiveFretGuitar.Intensity = _parts.ProGuitar_17Fret.Intensity; - } - break; - case "realBass": - case "real_bass": - _rbDifficulties.ProBass = (short) diff; - SetRank(ref _parts.ProBass_17Fret.Intensity, diff, RealBassDiffMap); - _parts.ProBass_22Fret.Intensity = _parts.ProBass_17Fret.Intensity; - if (_parts.FiveFretBass.Intensity == -1) - { - _parts.FiveFretBass.Intensity = _parts.ProBass_17Fret.Intensity; - } - break; - case "realKeys": - case "real_keys": - _rbDifficulties.ProKeys = (short) diff; - SetRank(ref _parts.ProKeys.Intensity, diff, RealKeysDiffMap); - if (_parts.Keys.Intensity == -1) - { - _parts.Keys.Intensity = _parts.ProKeys.Intensity; - } - break; - case "realDrums": - case "real_drums": - _rbDifficulties.ProDrums = (short) diff; - SetRank(ref _parts.ProDrums.Intensity, diff, RealDrumsDiffMap); - if (_parts.FourLaneDrums.Intensity == -1) - { - _parts.FourLaneDrums.Intensity = _parts.ProDrums.Intensity; - } - break; - case "harmVocals": - case "vocal_harm": - _rbDifficulties.HarmonyVocals = (short) diff; - SetRank(ref _parts.HarmonyVocals.Intensity, diff, HarmonyDiffMap); - if (_parts.LeadVocals.Intensity == -1) - { - _parts.LeadVocals.Intensity = _parts.HarmonyVocals.Intensity; - } - break; - case "band": - _rbDifficulties.Band = (short) diff; - SetRank(ref _parts.BandDifficulty.Intensity, diff, BandDiffMap); - _parts.BandDifficulty.SubTracks = 1; - break; + if (info.RBMetadata.Indices.Keys.Length > 0) + { + info.RBMetadata.Panning.Keys = CalculateStemValues(info.RBMetadata.Indices.Keys); } - YARGDTAReader.EndNode(ref container); - } - } - private static void SetRank(ref sbyte intensity, int rank, int[] values) - { - sbyte i = 0; - while (i < 6 && values[i] <= rank) - ++i; - intensity = i; - } + if (info.RBMetadata.Indices.Vocals.Length > 0) + { + info.RBMetadata.Panning.Vocals = CalculateStemValues(info.RBMetadata.Indices.Vocals); + } - private void Update(SongUpdate update, string nodename, in DTAResult results) - { - if (results.discUpdate) - { - if (update.Midi != null) + if (info.RBMetadata.Indices.Crowd.Length > 0) { - if (_updateMidi == null || update.Midi.Value.LastUpdatedTime > _updateMidi.Value.LastUpdatedTime) + info.RBMetadata.Panning.Crowd = CalculateStemValues(info.RBMetadata.Indices.Crowd); + } + + var leftover = new List(pans.Length); + for (int i = 0; i < pans.Length; i++) + { + if (!usedIndices[i]) { - _updateMidi = update.Midi; + leftover.Add(i); } } - else + + if (leftover.Count > 0) { - YargLogger.LogFormatWarning("Update midi expected in directory {0}", Path.Combine(update.BaseDirectory, nodename)); + info.RBMetadata.Indices.Track = leftover.ToArray(); + info.RBMetadata.Panning.Track = CalculateStemValues(info.RBMetadata.Indices.Track); } } - if (update.Mogg != null) + return (ScanResult.Success, info); + } + + protected static (ScanResult Result, HashWrapper Hash) ParseRBCONMidi(in FixedArray mainMidi, CONModification modification, ref ScanNode info) + { + try { - if (UpdateMogg == null || update.Mogg.Value.LastUpdatedTime > UpdateMogg.Value.LastUpdatedTime) + DrumPreparseHandler drumTracker = new() { - UpdateMogg = update.Mogg; - } - } + Type = DrumsType.ProDrums + }; - if (update.Milo != null) - { - if (UpdateMilo == null || update.Milo.Value.LastUpdatedTime > UpdateMilo.Value.LastUpdatedTime) + using var updateMidi = modification.Midi.HasValue ? FixedArray.Load(modification.Midi.Value.FullName) : FixedArray.Null; + using var upgradeMidi = modification.UpgradeNode != null ? modification.UpgradeNode.LoadUpgradeMidi() : FixedArray.Null; + if (modification.UpgradeNode != null && !upgradeMidi.IsAllocated) { - UpdateMilo = update.Milo; + throw new FileNotFoundException("Upgrade midi not located"); } - } - if (results.alternatePath) - { - if (update.Image != null) + long bufLength = mainMidi.Length; + if (updateMidi.IsAllocated) { - if (UpdateImage == null || update.Image.Value.LastUpdatedTime > UpdateImage.Value.LastUpdatedTime) + switch (ParseMidi(in updateMidi, drumTracker, ref info.Parts).Result) { - UpdateImage = update.Image; + case ScanResult.InvalidResolution: return (ScanResult.InvalidResolution_Update, default); + case ScanResult.MultipleMidiTrackNames: return (ScanResult.MultipleMidiTrackNames_Update, default); } + bufLength += updateMidi.Length; } - } - } - - private void FinalizeRBCONAudioValues(in DTAResult result) - { - HashSet pending = new(); - for (int i = 0; i < result.pans.Length; i++) - pending.Add(i); - if (_rbMetadata.Indices.Drums.Length > 0) - _rbMetadata.Panning.Drums = CalculateStemValues(_rbMetadata.Indices.Drums, in result, pending); + if (upgradeMidi.IsAllocated) + { + switch (ParseMidi(in upgradeMidi, drumTracker, ref info.Parts).Result) + { + case ScanResult.InvalidResolution: return (ScanResult.InvalidResolution_Upgrade, default); + case ScanResult.MultipleMidiTrackNames: return (ScanResult.MultipleMidiTrackNames_Upgrade, default); + } + bufLength += upgradeMidi.Length; + } - if (_rbMetadata.Indices.Bass.Length > 0) - _rbMetadata.Panning.Bass = CalculateStemValues(_rbMetadata.Indices.Bass, in result, pending); + var (result, resolution) = ParseMidi(in mainMidi, drumTracker, ref info.Parts); + if (result != ScanResult.Success) + { + return (result, default); + } - if (_rbMetadata.Indices.Guitar.Length > 0) - _rbMetadata.Panning.Guitar = CalculateStemValues(_rbMetadata.Indices.Guitar, in result, pending); + SetDrums(ref info.Parts, drumTracker); + if (!CheckScanValidity(in info.Parts)) + { + return (ScanResult.NoNotes, default); + } - if (_rbMetadata.Indices.Keys.Length > 0) - _rbMetadata.Panning.Keys = CalculateStemValues(_rbMetadata.Indices.Keys, in result, pending); + info.Settings.SustainCutoffThreshold = resolution / 3; + if (info.Settings.HopoThreshold == -1) + { + info.Settings.HopoThreshold = info.Settings.SustainCutoffThreshold; + } - if (_rbMetadata.Indices.Vocals.Length > 0) - _rbMetadata.Panning.Vocals = CalculateStemValues(_rbMetadata.Indices.Vocals, in result, pending); + using var buffer = FixedArray.Alloc(bufLength); + unsafe + { + System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.Ptr, mainMidi.Ptr, (uint) mainMidi.Length); - if (_rbMetadata.Indices.Crowd.Length > 0) - _rbMetadata.Panning.Crowd = CalculateStemValues(_rbMetadata.Indices.Crowd, in result, pending); + long offset = mainMidi.Length; + if (updateMidi.IsAllocated) + { + System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.Ptr + offset, updateMidi.Ptr, (uint) updateMidi.Length); + offset += updateMidi.Length; + } - if (pending.Count > 0) + if (upgradeMidi.IsAllocated) + { + System.Runtime.CompilerServices.Unsafe.CopyBlock(buffer.Ptr + offset, upgradeMidi.Ptr, (uint) upgradeMidi.Length); + } + } + return (ScanResult.Success, HashWrapper.Hash(buffer.ReadOnlySpan)); + } + catch (Exception ex) { - _rbMetadata.Indices.Track = pending.ToArray(); - _rbMetadata.Panning.Track = CalculateStemValues(_rbMetadata.Indices.Track, in result, pending); + YargLogger.LogException(ex); + return (ScanResult.PossibleCorruption, default); } } - private float[] CalculateStemValues(int[] indices, in DTAResult result, HashSet pending) + protected static Stream? LoadUpdateMoggStream(in AbridgedFileInfo? info) { - float[] values = new float[2 * indices.Length]; - for (int i = 0; i < indices.Length; i++) + if (info == null) { - int index = indices[i]; - float theta = (result.pans[index] + 1) * ((float) Math.PI / 4); - float volRatio = (float) Math.Pow(10, result.volumes[index] / 20); - values[2 * i] = volRatio * (float) Math.Cos(theta); - values[2 * i + 1] = volRatio * (float) Math.Sin(theta); - pending.Remove(index); + return null; } - return values; - } - private static AbridgedFileInfo? ReadUpdateInfo(UnmanagedMemoryStream stream) - { - if (!stream.ReadBoolean()) + var mogg = info.Value; + if (!File.Exists(mogg.FullName)) { return null; } - return new AbridgedFileInfo(stream.ReadString(), false); - } - private static string[] ReadStringArray(UnmanagedMemoryStream stream) - { - int length = stream.Read(Endianness.Little); - if (length == 0) + if (mogg.FullName.EndsWith(".yarg_mogg")) { - return Array.Empty(); + return new YargMoggReadStream(mogg.FullName); } - - var strings = new string[length]; - for (int i = 0; i < length; ++i) - strings[i] = stream.ReadString(); - return strings; + return new FileStream(mogg.FullName, FileMode.Open, FileAccess.Read); } - private static void WriteUpdateInfo(in AbridgedFileInfo? info, BinaryWriter writer) + private static readonly int[] BandDiffMap = { 163, 215, 243, 267, 292, 345 }; + private static readonly int[] GuitarDiffMap = { 139, 176, 221, 267, 333, 409 }; + private static readonly int[] BassDiffMap = { 135, 181, 228, 293, 364, 436 }; + private static readonly int[] DrumDiffMap = { 124, 151, 178, 242, 345, 448 }; + private static readonly int[] KeysDiffMap = { 153, 211, 269, 327, 385, 443 }; + private static readonly int[] VocalsDiffMap = { 132, 175, 218, 279, 353, 427 }; + private static readonly int[] RealGuitarDiffMap = { 150, 205, 264, 323, 382, 442 }; + private static readonly int[] RealBassDiffMap = { 150, 208, 267, 325, 384, 442 }; + private static readonly int[] RealDrumsDiffMap = { 124, 151, 178, 242, 345, 448 }; + private static readonly int[] RealKeysDiffMap = { 153, 211, 269, 327, 385, 443 }; + private static readonly int[] HarmonyDiffMap = { 132, 175, 218, 279, 353, 427 }; + + private static void SetRank(ref sbyte intensity, int rank, int[] values) { - if (info != null) + sbyte i = 0; + while (i < 6 && values[i] <= rank) { - writer.Write(true); - writer.Write(info.Value.FullName); + ++i; } - else - writer.Write(false); + intensity = i; } - private static void WriteStringArray(string[] strings, BinaryWriter writer) + private static void WriteUpdateInfo(in AbridgedFileInfo? info, MemoryStream stream) { - writer.Write(strings.Length); - for (int i = 0; i < strings.Length; ++i) + stream.Write(info != null); + if (info != null) { - writer.Write(strings[i]); + stream.Write(info.Value.FullName); } } } diff --git a/YARG.Core/Song/Entries/RBCON/SongEntry.UnpackedRBCON.cs b/YARG.Core/Song/Entries/RBCON/SongEntry.UnpackedRBCON.cs index fb64bbdd7..86868f8e9 100644 --- a/YARG.Core/Song/Entries/RBCON/SongEntry.UnpackedRBCON.cs +++ b/YARG.Core/Song/Entries/RBCON/SongEntry.UnpackedRBCON.cs @@ -1,10 +1,8 @@ using System; using System.IO; -using System.Collections.Generic; using YARG.Core.Extensions; using YARG.Core.Song.Cache; using YARG.Core.IO; -using YARG.Core.Logging; using YARG.Core.Venue; namespace YARG.Core.Song @@ -21,31 +19,48 @@ public sealed class UnpackedRBCONEntry : RBCONEntry public override string DirectoryActual => Location; public override EntryType SubType => EntryType.ExCON; - public static (ScanResult, UnpackedRBCONEntry?) ProcessNewEntry(UnpackedCONGroup group, string nodename, in YARGTextContainer container, Dictionary> updates, Dictionary, RBProUpgrade)> upgrades) + public static (ScanResult, UnpackedRBCONEntry?) ProcessNewEntry(UnpackedCONGroup group, string nodename, DTAEntry node, CONModification modification) { - try + var (dtaResult, info) = ProcessDTAs(nodename, node, modification); + if (dtaResult != ScanResult.Success) { - var song = new UnpackedRBCONEntry(group, nodename, in container, updates, upgrades); - if (song._midi == null) - { - return (ScanResult.MissingMidi, null); - } + return (dtaResult, null); + } - var result = song.ParseRBCONMidi(null); - if (result != ScanResult.Success) - { - return (result, null); - } - return (result, song); + if (!info.Location!.StartsWith("songs/" + nodename)) + { + nodename = info.Location!.Split('/')[1]; + } + + string directory = Path.Combine(group.Location, nodename); + if (!IsMoggValid(in modification.Mogg, directory, nodename)) + { + return (ScanResult.MoggError, null); + } + + var midiInfo = new FileInfo(Path.Combine(directory, nodename + ".mid")); + if (!midiInfo.Exists) + { + return (ScanResult.MissingCONMidi, null); + } + + using var mainMidi = FixedArray.Load(midiInfo.FullName); + var (midiResult, hash) = ParseRBCONMidi(in mainMidi, modification, ref info); + if (midiResult != ScanResult.Success) + { + return (midiResult, null); } - catch (Exception ex) + + if (info.Metadata.Playlist.Length == 0) { - YargLogger.LogException(ex, null); - return (ScanResult.DTAError, null); + info.Metadata.Playlist = group.DefaultPlaylist; } + + var entry = new UnpackedRBCONEntry(info, modification, in hash, directory, nodename, midiInfo, in group.Info); + return (ScanResult.Success, entry); } - public static UnpackedRBCONEntry? TryLoadFromCache(string directory, in AbridgedFileInfo dta, string nodename, Dictionary, RBProUpgrade Upgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + public static UnpackedRBCONEntry? TryLoadFromCache(string directory, in AbridgedFileInfo dta, string nodename, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { string subname = stream.ReadString(); string songDirectory = Path.Combine(directory, subname); @@ -66,12 +81,10 @@ public static (ScanResult, UnpackedRBCONEntry?) ProcessNewEntry(UnpackedCONGroup return null; } } - - var upgrade = upgrades.TryGetValue(nodename, out var node) ? node.Upgrade : null; return new UnpackedRBCONEntry(midiInfo.Value, dta, songDirectory, subname, updateMidi, upgrade, stream, strings); } - public static UnpackedRBCONEntry LoadFromCache_Quick(string directory, in AbridgedFileInfo? dta, string nodename, Dictionary, RBProUpgrade Upgrade)> upgrades, UnmanagedMemoryStream stream, CategoryCacheStrings strings) + public static UnpackedRBCONEntry LoadFromCache_Quick(string directory, in AbridgedFileInfo? dta, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) { string subname = stream.ReadString(); string songDirectory = Path.Combine(directory, subname); @@ -80,11 +93,21 @@ public static UnpackedRBCONEntry LoadFromCache_Quick(string directory, in Abridg var midiInfo = new AbridgedFileInfo(midiPath, stream); AbridgedFileInfo? updateMidi = stream.ReadBoolean() ? new AbridgedFileInfo(stream) : null; - - var upgrade = upgrades.TryGetValue(nodename, out var node) ? node.Upgrade : null; return new UnpackedRBCONEntry(midiInfo, dta, songDirectory, subname, updateMidi, upgrade, stream, strings); } + private static bool IsMoggValid(in AbridgedFileInfo? info, string directory, string nodename) + { + using var stream = LoadMoggStream(in info, directory, nodename); + if (stream == null) + { + return false; + } + + int version = stream.Read(Endianness.Little); + return version == 0x0A || version == 0xf0; + } + private UnpackedRBCONEntry(AbridgedFileInfo midi, AbridgedFileInfo? dta, string directory, string nodename, AbridgedFileInfo? updateMidi, RBProUpgrade? upgrade, UnmanagedMemoryStream stream, CategoryCacheStrings strings) : base(updateMidi, upgrade, stream, strings) @@ -96,33 +119,35 @@ private UnpackedRBCONEntry(AbridgedFileInfo midi, AbridgedFileInfo? dta, string _nodename = nodename; } - private UnpackedRBCONEntry(UnpackedCONGroup group, string nodename, in YARGTextContainer container, Dictionary> updates, Dictionary, RBProUpgrade)> upgrades) - : base() + private UnpackedRBCONEntry(in ScanNode info, CONModification modification, in HashWrapper hash + , string directory, string nodename, FileInfo midiInfo, in AbridgedFileInfo dtaInfo) + : base(in info, modification, in hash) { - var results = Init(nodename, in container, updates, upgrades, group.DefaultPlaylist); - if (!results.location.StartsWith($"songs/" + nodename)) - nodename = results.location.Split('/')[1]; + Location = directory; _nodename = nodename; + _midi = new AbridgedFileInfo(midiInfo); + _dta = dtaInfo; + } - Location = Path.Combine(group.Location, nodename); - string midiPath = Path.Combine(Location, nodename + ".mid"); + public override void Serialize(MemoryStream stream, CategoryCacheWriteNode node) + { + stream.Write(_nodename); + stream.Write(_midi!.Value.LastUpdatedTime.ToBinary(), Endianness.Little); + stream.Write(UpdateMidi != null); + UpdateMidi?.Serialize(stream); + base.Serialize(stream, node); + } - FileInfo midiInfo = new(midiPath); - if (!midiInfo.Exists) + public override YARGImage? LoadAlbumData() + { + if (UpdateImage != null && UpdateImage.Value.Exists()) { - return; + var update = FixedArray.Load(UpdateImage.Value.FullName); + return new YARGImage(update); } - _midi = new AbridgedFileInfo(midiInfo); - _dta = group.DTA; - } - - public override void Serialize(BinaryWriter writer, CategoryCacheWriteNode node) - { - writer.Write(_nodename); - var info = _midi!.Value; - writer.Write(info.LastUpdatedTime.ToBinary()); - base.Serialize(writer, node); + string imgFilename = Path.Combine(Location, "gen", _nodename + "_keep.png_xbox"); + return File.Exists(imgFilename) ? new YARGImage(FixedArray.Load(imgFilename)) : null; } public override BackgroundResult? LoadBackground(BackgroundType options) @@ -179,10 +204,9 @@ public override void Serialize(BinaryWriter writer, CategoryCacheWriteNode node) public override FixedArray LoadMiloData() { - var bytes = base.LoadMiloData(); - if (bytes.IsAllocated) + if (UpdateMilo != null && UpdateMilo.Value.Exists()) { - return bytes; + return FixedArray.Load(UpdateMilo.Value.FullName); } string filename = Path.Combine(Location, "gen", _nodename + ".milo_xbox"); @@ -198,57 +222,27 @@ public override FixedArray LoadMiloData() return new FileStream(_midi.Value.FullName, FileMode.Open, FileAccess.Read, FileShare.Read); } - protected override FixedArray LoadMidiFile(Stream? file) + protected override Stream? GetMoggStream() { - return _dta != null && _dta.Value.IsStillValid() && _midi!.Value.IsStillValid() - ? FixedArray.Load(_midi.Value.FullName) - : FixedArray.Null; + return LoadMoggStream(in UpdateMogg, Location, _nodename); } - protected override FixedArray LoadRawImageData() + private static Stream? LoadMoggStream(in AbridgedFileInfo? updateMogg, string directory, string nodename) { - var bytes = base.LoadRawImageData(); - if (bytes.IsAllocated) - { - return bytes; - } - - string filename = Path.Combine(Location, "gen", _nodename + "_keep.png_xbox"); - return File.Exists(filename) ? FixedArray.Load(filename) : FixedArray.Null; - } - - protected override Stream? GetMoggStream() - { - var stream = base.GetMoggStream(); + var stream = LoadUpdateMoggStream(in updateMogg); if (stream != null) { return stream; } - string path = Path.Combine(Location, _nodename + ".yarg_mogg"); + string path = Path.Combine(directory, nodename + ".yarg_mogg"); if (File.Exists(path)) { return new YargMoggReadStream(path); } - path = Path.Combine(Location, _nodename + ".mogg"); - if (!File.Exists(path)) - { - return null; - } - return new FileStream(path, FileMode.Open, FileAccess.Read); - } - - protected override bool IsMoggValid(Stream? file) - { - using var stream = GetMoggStream(); - if (stream == null) - { - return false; - } - - int version = stream.Read(Endianness.Little); - return version == 0x0A || version == 0xf0; + path = Path.Combine(directory, nodename + ".mogg"); + return File.Exists(path) ? File.OpenRead(path) : null; } } } diff --git a/YARG.Core/Song/Entries/SongEntry.Loading.cs b/YARG.Core/Song/Entries/SongEntry.Loading.cs index c1787a25e..244185df9 100644 --- a/YARG.Core/Song/Entries/SongEntry.Loading.cs +++ b/YARG.Core/Song/Entries/SongEntry.Loading.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Threading; -using System.Threading.Tasks; using YARG.Core.Audio; using YARG.Core.Chart; using YARG.Core.IO; -using YARG.Core.Logging; using YARG.Core.Venue; namespace YARG.Core.Song diff --git a/YARG.Core/Song/Entries/SongEntry.Scanning.cs b/YARG.Core/Song/Entries/SongEntry.Scanning.cs index ea6e959e8..81d51c09c 100644 --- a/YARG.Core/Song/Entries/SongEntry.Scanning.cs +++ b/YARG.Core/Song/Entries/SongEntry.Scanning.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Text; using YARG.Core.Chart; using YARG.Core.IO; using YARG.Core.Song.Preparsers; @@ -10,9 +7,14 @@ namespace YARG.Core.Song { public abstract partial class SongEntry { - protected static bool ParseMidi(in FixedArray file, DrumPreparseHandler drums, ref AvailableParts parts) + protected static (ScanResult Result, long Resolution) ParseMidi(in FixedArray file, DrumPreparseHandler drums, ref AvailableParts parts) { var midiFile = new YARGMidiFile(file.ToStream()); + if (midiFile.Resolution == 0) + { + return (ScanResult.InvalidResolution, 0); + } + bool harm2 = false; bool harm3 = false; foreach (var track in midiFile) @@ -22,10 +24,14 @@ protected static bool ParseMidi(in FixedArray file, DrumPreparseHandler dr var trackname = track.FindTrackName(Encoding.ASCII); if (trackname == null) - return false; + { + return (ScanResult.MultipleMidiTrackNames, 0); + } if (!YARGMidiTrack.TRACKNAMES.TryGetValue(trackname, out var type)) + { continue; + } switch (type) { @@ -73,18 +79,22 @@ protected static bool ParseMidi(in FixedArray file, DrumPreparseHandler dr parts.HarmonyVocals.SetSubtrack(2); } } - return true; + return (ScanResult.Success, midiFile.Resolution); } protected static void SetDrums(ref AvailableParts parts, DrumPreparseHandler drumTracker) { if (drumTracker.Type == DrumsType.FiveLane) + { parts.FiveLaneDrums.Difficulties = drumTracker.ValidatedDiffs; + } else { parts.FourLaneDrums.Difficulties = drumTracker.ValidatedDiffs; if (drumTracker.Type == DrumsType.ProDrums) + { parts.ProDrums.Difficulties = drumTracker.ValidatedDiffs; + } } } diff --git a/YARG.Core/Song/Entries/SongEntry.Sorting.cs b/YARG.Core/Song/Entries/SongEntry.Sorting.cs index 06806b1fe..8d33c0534 100644 --- a/YARG.Core/Song/Entries/SongEntry.Sorting.cs +++ b/YARG.Core/Song/Entries/SongEntry.Sorting.cs @@ -33,10 +33,7 @@ public int CompareTo(SongEntry other) return strCmp; } - public virtual DateTime GetAddTime() - { - return DateTime.MinValue; - } + public abstract DateTime GetAddDate(); public bool IsPreferedOver(SongEntry other) { diff --git a/YARG.Core/Song/Entries/SongEntry.cs b/YARG.Core/Song/Entries/SongEntry.cs index f70c8f9bf..7945de63b 100644 --- a/YARG.Core/Song/Entries/SongEntry.cs +++ b/YARG.Core/Song/Entries/SongEntry.cs @@ -1,8 +1,6 @@ using System; using System.IO; -using YARG.Core.Chart; using YARG.Core.Extensions; -using YARG.Core.IO.Ini; using YARG.Core.Song.Cache; namespace YARG.Core.Song @@ -20,12 +18,14 @@ public enum ScanResult DTAError, MoggError, UnsupportedEncryption, - MissingMidi, - MissingUpdateMidi, - MissingUpgradeMidi, + MissingCONMidi, PossibleCorruption, FailedSngLoad, + InvalidResolution, + InvalidResolution_Update, + InvalidResolution_Upgrade, + NoAudio, PathTooLong, MultipleMidiTrackNames, @@ -38,7 +38,7 @@ public enum ScanResult /// /// The type of chart file to read. /// - public enum ChartType + public enum ChartFormat { Mid, Midi, @@ -53,6 +53,20 @@ public enum EntryType CON, } + public struct LoaderSettings + { + public static readonly LoaderSettings Default = new() + { + HopoThreshold = -1, + SustainCutoffThreshold = -1, + OverdiveMidiNote = 116 + }; + + public long HopoThreshold; + public long SustainCutoffThreshold; + public int OverdiveMidiNote; + } + /// /// The metadata for a song. /// @@ -71,7 +85,6 @@ public enum EntryType [Serializable] public abstract partial class SongEntry { - public const double MILLISECOND_FACTOR = 1000.0; protected static readonly string[] BACKGROUND_FILENAMES = { "bg", "background", "video" @@ -91,150 +104,89 @@ public abstract partial class SongEntry protected static readonly string YARGROUND_FULLNAME = "bg.yarground"; protected static readonly Random BACKROUND_RNG = new(); - private string _parsedYear; - private int _intYear; - - protected SongMetadata _metadata; - protected AvailableParts _parts; - protected ParseSettings _parseSettings; - protected HashWrapper _hash; + public readonly SongMetadata Metadata; + public readonly AvailableParts Parts; + public readonly HashWrapper Hash; + public readonly LoaderSettings Settings; public abstract string Location { get; } public abstract string DirectoryActual { get; } - public abstract EntryType SubType { get; } + public abstract string Year { get; } + public abstract int YearAsNumber { get; } + public abstract ulong SongLengthMilliseconds { get; } + public abstract bool LoopVideo { get; } - public SortString Name => _metadata.Name; - public SortString Artist => _metadata.Artist; - public SortString Album => _metadata.Album; - public SortString Genre => _metadata.Genre; - public SortString Charter => _metadata.Charter; - public SortString Source => _metadata.Source; - public SortString Playlist => _metadata.Playlist; + public SortString Name => Metadata.Name; + public SortString Artist => Metadata.Artist; + public SortString Album => Metadata.Album; + public SortString Genre => Metadata.Genre; + public SortString Charter => Metadata.Charter; + public SortString Source => Metadata.Source; + public SortString Playlist => Metadata.Playlist; - public string Year => _parsedYear; + public string UnmodifiedYear => Metadata.Year; - public string UnmodifiedYear => _metadata.Year; + public bool IsMaster => Metadata.IsMaster; - public int YearAsNumber - { - get => _intYear; - set - { - _intYear = value; - _parsedYear = _metadata.Year = value.ToString(); - } - } + public int AlbumTrack => Metadata.AlbumTrack; - public bool IsMaster => _metadata.IsMaster; + public int PlaylistTrack => Metadata.PlaylistTrack; - public int AlbumTrack => _metadata.AlbumTrack; + public string LoadingPhrase => Metadata.LoadingPhrase; + + public string CreditWrittenBy => Metadata.CreditWrittenBy; + + public string CreditPerformedBy => Metadata.CreditPerformedBy; + + public string CreditCourtesyOf => Metadata.CreditCourtesyOf; + + public string CreditAlbumCover => Metadata.CreditAlbumCover; + + public string CreditLicense => Metadata.CreditLicense; - public int PlaylistTrack => _metadata.PlaylistTrack; + public long SongOffsetMilliseconds => Metadata.SongOffset; - public string LoadingPhrase => _metadata.LoadingPhrase; + public long PreviewStartMilliseconds => Metadata.PreviewStart; - public string CreditWrittenBy => _metadata.CreditWrittenBy; - public string CreditPerformedBy => _metadata.CreditPerformedBy; - public string CreditCourtesyOf => _metadata.CreditCourtesyOf; - public string CreditAlbumCover => _metadata.CreditAlbumCover; - public string CreditLicense => _metadata.CreditLicense; + public long PreviewEndMilliseconds => Metadata.PreviewEnd; - public ulong SongLengthMilliseconds - { - get => _metadata.SongLength; - set => _metadata.SongLength = value; - } + public long VideoStartTimeMilliseconds => Metadata.VideoStartTime; - public long SongOffsetMilliseconds - { - get => _metadata.SongOffset; - set => _metadata.SongOffset = value; - } + public long VideoEndTimeMilliseconds => Metadata.VideoEndTime; - public double SongLengthSeconds - { - get => _metadata.SongLength / MILLISECOND_FACTOR; - set => _metadata.SongLength = (ulong) (value * MILLISECOND_FACTOR); - } - - public double SongOffsetSeconds - { - get => _metadata.SongOffset / MILLISECOND_FACTOR; - set => _metadata.SongOffset = (long) (value * MILLISECOND_FACTOR); - } + public double SongLengthSeconds => SongLengthMilliseconds / SongMetadata.MILLISECOND_FACTOR; - public long PreviewStartMilliseconds - { - get => _metadata.PreviewStart; - set => _metadata.PreviewStart = value; - } + public double SongOffsetSeconds => SongOffsetMilliseconds / SongMetadata.MILLISECOND_FACTOR; - public long PreviewEndMilliseconds - { - get => _metadata.PreviewEnd; - set => _metadata.PreviewEnd = value; - } + public double PreviewStartSeconds => PreviewStartMilliseconds / SongMetadata.MILLISECOND_FACTOR; - public double PreviewStartSeconds - { - get => _metadata.PreviewStart / MILLISECOND_FACTOR; - set => _metadata.PreviewStart = (long) (value * MILLISECOND_FACTOR); - } + public double PreviewEndSeconds => PreviewEndMilliseconds / SongMetadata.MILLISECOND_FACTOR; - public double PreviewEndSeconds - { - get => _metadata.PreviewEnd / MILLISECOND_FACTOR; - set => _metadata.PreviewEnd = (long) (value * MILLISECOND_FACTOR); - } + public double VideoStartTimeSeconds => VideoStartTimeMilliseconds / SongMetadata.MILLISECOND_FACTOR; - public long VideoStartTimeMilliseconds - { - get => _metadata.VideoStartTime; - set => _metadata.VideoStartTime = value; - } - - public long VideoEndTimeMilliseconds - { - get => _metadata.VideoEndTime; - set => _metadata.VideoEndTime = value; - } - - public double VideoStartTimeSeconds - { - get => _metadata.VideoStartTime / MILLISECOND_FACTOR; - set => _metadata.VideoStartTime = (long) (value * MILLISECOND_FACTOR); - } - - public double VideoEndTimeSeconds - { - get => _metadata.VideoEndTime >= 0 ? _metadata.VideoEndTime / MILLISECOND_FACTOR : -1; - set => _metadata.VideoEndTime = value >= 0 ? (long) (value * MILLISECOND_FACTOR) : -1; - } - - public HashWrapper Hash => _hash; + public double VideoEndTimeSeconds => VideoEndTimeMilliseconds >= 0 ? VideoEndTimeMilliseconds / SongMetadata.MILLISECOND_FACTOR : -1; public int VocalsCount { get { - if (_parts.HarmonyVocals[2]) + if (Parts.HarmonyVocals[2]) { return 3; } - if (_parts.HarmonyVocals[1]) + if (Parts.HarmonyVocals[1]) { return 2; } - return _parts.HarmonyVocals[0] || _parts.LeadVocals[0] ? 1 : 0; + return Parts.HarmonyVocals[0] || Parts.LeadVocals[0] ? 1 : 0; } } + public sbyte BandDifficulty => Parts.BandDifficulty.Intensity; - public sbyte BandDifficulty => _parts.BandDifficulty.Intensity; - - public override string ToString() { return _metadata.Artist + " | " + _metadata.Name; } + public override string ToString() { return Artist + " | " + Name; } public PartValues this[Instrument instrument] { @@ -242,35 +194,35 @@ public PartValues this[Instrument instrument] { return instrument switch { - Instrument.FiveFretGuitar => _parts.FiveFretGuitar, - Instrument.FiveFretBass => _parts.FiveFretBass, - Instrument.FiveFretRhythm => _parts.FiveFretRhythm, - Instrument.FiveFretCoopGuitar => _parts.FiveFretCoopGuitar, - Instrument.Keys => _parts.Keys, + Instrument.FiveFretGuitar => Parts.FiveFretGuitar, + Instrument.FiveFretBass => Parts.FiveFretBass, + Instrument.FiveFretRhythm => Parts.FiveFretRhythm, + Instrument.FiveFretCoopGuitar => Parts.FiveFretCoopGuitar, + Instrument.Keys => Parts.Keys, - Instrument.SixFretGuitar => _parts.SixFretGuitar, - Instrument.SixFretBass => _parts.SixFretBass, - Instrument.SixFretRhythm => _parts.SixFretRhythm, - Instrument.SixFretCoopGuitar => _parts.SixFretCoopGuitar, + Instrument.SixFretGuitar => Parts.SixFretGuitar, + Instrument.SixFretBass => Parts.SixFretBass, + Instrument.SixFretRhythm => Parts.SixFretRhythm, + Instrument.SixFretCoopGuitar => Parts.SixFretCoopGuitar, - Instrument.FourLaneDrums => _parts.FourLaneDrums, - Instrument.FiveLaneDrums => _parts.FiveLaneDrums, - Instrument.ProDrums => _parts.ProDrums, + Instrument.FourLaneDrums => Parts.FourLaneDrums, + Instrument.FiveLaneDrums => Parts.FiveLaneDrums, + Instrument.ProDrums => Parts.ProDrums, - Instrument.EliteDrums => _parts.EliteDrums, + Instrument.EliteDrums => Parts.EliteDrums, - Instrument.ProGuitar_17Fret => _parts.ProGuitar_17Fret, - Instrument.ProGuitar_22Fret => _parts.ProGuitar_22Fret, - Instrument.ProBass_17Fret => _parts.ProBass_17Fret, - Instrument.ProBass_22Fret => _parts.ProBass_22Fret, + Instrument.ProGuitar_17Fret => Parts.ProGuitar_17Fret, + Instrument.ProGuitar_22Fret => Parts.ProGuitar_22Fret, + Instrument.ProBass_17Fret => Parts.ProBass_17Fret, + Instrument.ProBass_22Fret => Parts.ProBass_22Fret, - Instrument.ProKeys => _parts.ProKeys, + Instrument.ProKeys => Parts.ProKeys, // Instrument.Dj => DJ, - Instrument.Vocals => _parts.LeadVocals, - Instrument.Harmony => _parts.HarmonyVocals, - Instrument.Band => _parts.BandDifficulty, + Instrument.Vocals => Parts.LeadVocals, + Instrument.Harmony => Parts.HarmonyVocals, + Instrument.Band => Parts.BandDifficulty, _ => throw new NotImplementedException($"Unhandled instrument {instrument}!") }; @@ -281,328 +233,76 @@ public bool HasInstrument(Instrument instrument) { return instrument switch { - Instrument.FiveFretGuitar => _parts.FiveFretGuitar.SubTracks > 0, - Instrument.FiveFretBass => _parts.FiveFretBass.SubTracks > 0, - Instrument.FiveFretRhythm => _parts.FiveFretRhythm.SubTracks > 0, - Instrument.FiveFretCoopGuitar => _parts.FiveFretCoopGuitar.SubTracks > 0, - Instrument.Keys => _parts.Keys.SubTracks > 0, + Instrument.FiveFretGuitar => Parts.FiveFretGuitar.SubTracks > 0, + Instrument.FiveFretBass => Parts.FiveFretBass.SubTracks > 0, + Instrument.FiveFretRhythm => Parts.FiveFretRhythm.SubTracks > 0, + Instrument.FiveFretCoopGuitar => Parts.FiveFretCoopGuitar.SubTracks > 0, + Instrument.Keys => Parts.Keys.SubTracks > 0, - Instrument.SixFretGuitar => _parts.SixFretGuitar.SubTracks > 0, - Instrument.SixFretBass => _parts.SixFretBass.SubTracks > 0, - Instrument.SixFretRhythm => _parts.SixFretRhythm.SubTracks > 0, - Instrument.SixFretCoopGuitar => _parts.SixFretCoopGuitar.SubTracks > 0, + Instrument.SixFretGuitar => Parts.SixFretGuitar.SubTracks > 0, + Instrument.SixFretBass => Parts.SixFretBass.SubTracks > 0, + Instrument.SixFretRhythm => Parts.SixFretRhythm.SubTracks > 0, + Instrument.SixFretCoopGuitar => Parts.SixFretCoopGuitar.SubTracks > 0, - Instrument.FourLaneDrums => _parts.FourLaneDrums.SubTracks > 0, - Instrument.FiveLaneDrums => _parts.FiveLaneDrums.SubTracks > 0, - Instrument.ProDrums => _parts.ProDrums.SubTracks > 0, + Instrument.FourLaneDrums => Parts.FourLaneDrums.SubTracks > 0, + Instrument.FiveLaneDrums => Parts.FiveLaneDrums.SubTracks > 0, + Instrument.ProDrums => Parts.ProDrums.SubTracks > 0, - Instrument.EliteDrums => _parts.EliteDrums.SubTracks > 0, + Instrument.EliteDrums => Parts.EliteDrums.SubTracks > 0, - Instrument.ProGuitar_17Fret => _parts.ProGuitar_17Fret.SubTracks > 0, - Instrument.ProGuitar_22Fret => _parts.ProGuitar_22Fret.SubTracks > 0, - Instrument.ProBass_17Fret => _parts.ProBass_17Fret.SubTracks > 0, - Instrument.ProBass_22Fret => _parts.ProBass_22Fret.SubTracks > 0, + Instrument.ProGuitar_17Fret => Parts.ProGuitar_17Fret.SubTracks > 0, + Instrument.ProGuitar_22Fret => Parts.ProGuitar_22Fret.SubTracks > 0, + Instrument.ProBass_17Fret => Parts.ProBass_17Fret.SubTracks > 0, + Instrument.ProBass_22Fret => Parts.ProBass_22Fret.SubTracks > 0, - Instrument.ProKeys => _parts.ProKeys.SubTracks > 0, + Instrument.ProKeys => Parts.ProKeys.SubTracks > 0, - // Instrument.Dj => _parts.DJ.SubTracks > 0, + // Instrument.Dj => Parts.DJ.SubTracks > 0, - Instrument.Vocals => _parts.LeadVocals.SubTracks > 0, - Instrument.Harmony => _parts.HarmonyVocals.SubTracks > 0, - Instrument.Band => _parts.BandDifficulty.SubTracks > 0, + Instrument.Vocals => Parts.LeadVocals.SubTracks > 0, + Instrument.Harmony => Parts.HarmonyVocals.SubTracks > 0, + Instrument.Band => Parts.BandDifficulty.SubTracks > 0, _ => false }; } - protected SongEntry() - { - _metadata = SongMetadata.Default; - _parts = AvailableParts.Default; - _parseSettings = ParseSettings.Default; - _parsedYear = SongMetadata.DEFAULT_YEAR; - _intYear = int.MaxValue; - } - - private static readonly SortString DEFAULT_NAME_SORT = SortString.Convert(SongMetadata.DEFAULT_NAME); - private static readonly SortString DEFAULT_ARTIST_SORT = SortString.Convert(SongMetadata.DEFAULT_ARTIST); - private static readonly SortString DEFAULT_ALBUM_SORT = SortString.Convert(SongMetadata.DEFAULT_ALBUM); - private static readonly SortString DEFAULT_GENRE_SORT = SortString.Convert(SongMetadata.DEFAULT_GENRE); - private static readonly SortString DEFAULT_CHARTER_SORT = SortString.Convert(SongMetadata.DEFAULT_CHARTER); - private static readonly SortString DEFAULT_SOURCE_SORT = SortString.Convert(SongMetadata.DEFAULT_SOURCE); - - protected SongEntry(in AvailableParts parts, in HashWrapper hash, IniSection modifiers, in string defaultPlaylist) + protected SongEntry(in SongMetadata metadata, in AvailableParts parts, in HashWrapper hash, in LoaderSettings settings) { - _parts = parts; - _hash = hash; - - modifiers.TryGet("name", out _metadata.Name, DEFAULT_NAME_SORT); - modifiers.TryGet("artist", out _metadata.Artist, DEFAULT_ARTIST_SORT); - modifiers.TryGet("album", out _metadata.Album, DEFAULT_ALBUM_SORT); - modifiers.TryGet("genre", out _metadata.Genre, DEFAULT_GENRE_SORT); - - if (!modifiers.TryGet("year", out _metadata.Year)) - { - // Capitalization = from .chart - if (modifiers.TryGet("Year", out _metadata.Year)) - { - if (_metadata.Year.StartsWith(", ")) - { - _metadata.Year = _metadata.Year[2..]; - } - else if (_metadata.Year.StartsWith(',')) - { - _metadata.Year = _metadata.Year[1..]; - } - } - else - { - _metadata.Year = SongMetadata.DEFAULT_YEAR; - } - } - - _intYear = ParseYear(in _metadata.Year, out _parsedYear) - ? int.Parse(_parsedYear) - : int.MaxValue; - - if (!modifiers.TryGet("charter", out _metadata.Charter, DEFAULT_CHARTER_SORT)) - { - modifiers.TryGet("frets", out _metadata.Charter, DEFAULT_CHARTER_SORT); - } - - modifiers.TryGet("icon", out _metadata.Source, DEFAULT_SOURCE_SORT); - modifiers.TryGet("playlist", out _metadata.Playlist, defaultPlaylist); - - modifiers.TryGet("loading_phrase", out _metadata.LoadingPhrase); - - modifiers.TryGet("credit_written_by", out _metadata.CreditWrittenBy); - modifiers.TryGet("credit_performed_by", out _metadata.CreditPerformedBy); - modifiers.TryGet("credit_courtesy_of", out _metadata.CreditCourtesyOf); - modifiers.TryGet("credit_album_cover", out _metadata.CreditAlbumCover); - modifiers.TryGet("credit_license", out _metadata.CreditLicense); - - if (!modifiers.TryGet("playlist_track", out _metadata.PlaylistTrack)) - { - _metadata.PlaylistTrack = -1; - } - - if (!modifiers.TryGet("album_track", out _metadata.AlbumTrack)) - { - _metadata.AlbumTrack = -1; - } - - modifiers.TryGet("song_length", out _metadata.SongLength); - modifiers.TryGet("rating", out _metadata.SongRating); - - modifiers.TryGet("video_start_time", out _metadata.VideoStartTime); - if (!modifiers.TryGet("video_end_time", out _metadata.VideoEndTime)) - { - _metadata.VideoEndTime = -1; - } - - if (!modifiers.TryGet("preview", out _metadata.PreviewStart, out _metadata.PreviewEnd)) - { - if (!modifiers.TryGet("preview_start_time", out _metadata.PreviewStart)) - { - // Capitlization = from .chart - if (modifiers.TryGet("PreviewStart", out double previewStartSeconds)) - { - _metadata.PreviewStart = (long) (previewStartSeconds * MILLISECOND_FACTOR); - } - else - { - _metadata.PreviewStart = -1; - } - } - - if (!modifiers.TryGet("preview_end_time", out _metadata.PreviewEnd)) - { - // Capitlization = from .chart - if (modifiers.TryGet("PreviewEnd", out double previewEndSeconds)) - { - _metadata.PreviewEnd = (long) (previewEndSeconds * MILLISECOND_FACTOR); - } - else - { - _metadata.PreviewEnd = -1; - } - } - } - - if (!modifiers.TryGet("delay", out _metadata.SongOffset) || _metadata.SongOffset == 0) - { - // Capitlization = from .chart - if (modifiers.TryGet("Offset", out double songOffsetSeconds)) - { - _metadata.SongOffset = (long) (songOffsetSeconds * MILLISECOND_FACTOR); - } - } - - if (parts.FourLaneDrums.SubTracks > 0) - { - _parseSettings.DrumsType = DrumsType.FourLane; - } - else if (parts.FiveLaneDrums.SubTracks > 0) - { - _parseSettings.DrumsType = DrumsType.FiveLane; - } - else - { - _parseSettings.DrumsType = DrumsType.Unknown; - } - - if (!modifiers.TryGet("hopo_frequency", out _parseSettings.HopoThreshold)) - { - _parseSettings.HopoThreshold = -1; - } - - if (!modifiers.TryGet("hopofreq", out _parseSettings.HopoFreq_FoF)) - { - _parseSettings.HopoFreq_FoF = -1; - } - - modifiers.TryGet("eighthnote_hopo", out _parseSettings.EighthNoteHopo); - - if (!modifiers.TryGet("sustain_cutoff_threshold", out _parseSettings.SustainCutoffThreshold)) - { - _parseSettings.SustainCutoffThreshold = -1; - } - - if (!modifiers.TryGet("multiplier_note", out _parseSettings.StarPowerNote)) - { - _parseSettings.StarPowerNote = -1; - } - - _metadata.IsMaster = !modifiers.TryGet("tags", out string tag) || tag.ToLower() != "cover"; + Metadata = metadata; + Parts = parts; + Hash = hash; + Settings = settings; } - protected SongEntry(UnmanagedMemoryStream stream, in CategoryCacheStrings strings) + protected SongEntry(UnmanagedMemoryStream stream, CategoryCacheStrings strings) { - _metadata.Name = strings.titles[stream.Read(Endianness.Little)]; - _metadata.Artist = strings.artists[stream.Read(Endianness.Little)]; - _metadata.Album = strings.albums[stream.Read(Endianness.Little)]; - _metadata.Genre = strings.genres[stream.Read(Endianness.Little)]; - - _metadata.Year = strings.years[stream.Read(Endianness.Little)]; - _metadata.Charter = strings.charters[stream.Read(Endianness.Little)]; - _metadata.Playlist = strings.playlists[stream.Read(Endianness.Little)]; - _metadata.Source = strings.sources[stream.Read(Endianness.Little)]; - - _metadata.IsMaster = stream.ReadBoolean(); - - _metadata.AlbumTrack = stream.Read(Endianness.Little); - _metadata.PlaylistTrack = stream.Read(Endianness.Little); - - _metadata.SongLength = stream.Read(Endianness.Little); - _metadata.SongOffset = stream.Read(Endianness.Little); - _metadata.SongRating = stream.Read(Endianness.Little); - - _metadata.PreviewStart = stream.Read(Endianness.Little); - _metadata.PreviewEnd = stream.Read(Endianness.Little); - - _metadata.VideoStartTime = stream.Read(Endianness.Little); - _metadata.VideoEndTime = stream.Read(Endianness.Little); - - _metadata.LoadingPhrase = stream.ReadString(); - - _metadata.CreditWrittenBy = stream.ReadString(); - _metadata.CreditPerformedBy = stream.ReadString(); - _metadata.CreditCourtesyOf = stream.ReadString(); - _metadata.CreditAlbumCover = stream.ReadString(); - _metadata.CreditLicense = stream.ReadString(); - - _parseSettings.HopoThreshold = stream.Read(Endianness.Little); - _parseSettings.HopoFreq_FoF = stream.Read(Endianness.Little); - _parseSettings.EighthNoteHopo = stream.ReadBoolean(); - _parseSettings.SustainCutoffThreshold = stream.Read(Endianness.Little); - _parseSettings.NoteSnapThreshold = stream.Read(Endianness.Little); - _parseSettings.StarPowerNote = stream.Read(Endianness.Little); - _parseSettings.DrumsType = (DrumsType) stream.Read(Endianness.Little); - + Hash = HashWrapper.Deserialize(stream); unsafe { - fixed (AvailableParts* ptr = &_parts) - { - stream.Read(new Span(ptr, sizeof(AvailableParts))); - } + Parts = *(AvailableParts*) stream.PositionPointer; + stream.Position += sizeof(AvailableParts); } - _hash = HashWrapper.Deserialize(stream); - - _intYear = ParseYear(in _metadata.Year, out _parsedYear) - ? int.Parse(_parsedYear) - : int.MaxValue; + Metadata = new SongMetadata(stream, strings); + Settings.HopoThreshold = stream.Read(Endianness.Little); + Settings.SustainCutoffThreshold = stream.Read(Endianness.Little); + Settings.OverdiveMidiNote = stream.Read(Endianness.Little); } - protected void SerializeMetadata(in BinaryWriter writer, in CategoryCacheWriteNode node) + public virtual void Serialize(MemoryStream stream, CategoryCacheWriteNode node) { - writer.Write(node.title); - writer.Write(node.artist); - writer.Write(node.album); - writer.Write(node.genre); - writer.Write(node.year); - writer.Write(node.charter); - writer.Write(node.playlist); - writer.Write(node.source); - - writer.Write(_metadata.IsMaster); - - writer.Write(_metadata.AlbumTrack); - writer.Write(_metadata.PlaylistTrack); - - writer.Write(_metadata.SongLength); - writer.Write(_metadata.SongOffset); - writer.Write(_metadata.SongRating); - - writer.Write(_metadata.PreviewStart); - writer.Write(_metadata.PreviewEnd); - - writer.Write(_metadata.VideoStartTime); - writer.Write(_metadata.VideoEndTime); - - writer.Write(_metadata.LoadingPhrase); - - writer.Write(_metadata.CreditWrittenBy); - writer.Write(_metadata.CreditPerformedBy); - writer.Write(_metadata.CreditCourtesyOf); - writer.Write(_metadata.CreditAlbumCover); - writer.Write(_metadata.CreditLicense); - - writer.Write(_parseSettings.HopoThreshold); - writer.Write(_parseSettings.HopoFreq_FoF); - writer.Write(_parseSettings.EighthNoteHopo); - writer.Write(_parseSettings.SustainCutoffThreshold); - writer.Write(_parseSettings.NoteSnapThreshold); - writer.Write(_parseSettings.StarPowerNote); - writer.Write((int) _parseSettings.DrumsType); - + Hash.Serialize(stream); unsafe { - fixed (AvailableParts* ptr = &_parts) - { - writer.Write(new Span(ptr, sizeof(AvailableParts))); - } - } - _hash.Serialize(writer); - } - - private static bool ParseYear(in string baseString, out string parsedString) - { - for (int i = 0; i <= baseString.Length - 4; ++i) - { - int pivot = i; - while (i < pivot + 4 && i < baseString.Length && char.IsDigit(baseString[i])) - { - ++i; - } - - if (i == pivot + 4) + fixed (AvailableParts* ptr = &Parts) { - parsedString = baseString[pivot..i]; - return true; + stream.Write(new Span(ptr, sizeof(AvailableParts))); } } - parsedString = baseString; - return false; + Metadata.Serialize(stream, node); + stream.Write(Settings.HopoThreshold, Endianness.Little); + stream.Write(Settings.SustainCutoffThreshold, Endianness.Little); + stream.Write(Settings.OverdiveMidiNote, Endianness.Little); } } } \ No newline at end of file diff --git a/YARG.Core/Song/Entries/Types/HashWrapper.cs b/YARG.Core/Song/Entries/Types/HashWrapper.cs index 41cad34c8..325e793e4 100644 --- a/YARG.Core/Song/Entries/Types/HashWrapper.cs +++ b/YARG.Core/Song/Entries/Types/HashWrapper.cs @@ -94,6 +94,15 @@ public readonly void Serialize(BinaryWriter writer) } } + public readonly void Serialize(Stream stream) + { + fixed (int* values = _hash) + { + var bytes = new Span(values, HASH_SIZE_IN_BYTES); + stream.Write(bytes); + } + } + public readonly int CompareTo(HashWrapper other) { for (int i = 0; i < HASH_SIZE_IN_INTS; ++i) diff --git a/YARG.Core/Song/Entries/Types/SongMetadata.cs b/YARG.Core/Song/Entries/Types/SongMetadata.cs index 0979a4788..8ab95f285 100644 --- a/YARG.Core/Song/Entries/Types/SongMetadata.cs +++ b/YARG.Core/Song/Entries/Types/SongMetadata.cs @@ -1,7 +1,13 @@ -namespace YARG.Core.Song +using System.IO; +using YARG.Core.Extensions; +using YARG.Core.IO.Ini; +using YARG.Core.Song.Cache; + +namespace YARG.Core.Song { public struct SongMetadata { + public const double MILLISECOND_FACTOR = 1000.0; public static readonly SortString DEFAULT_NAME = "Unknown Name"; public static readonly SortString DEFAULT_ARTIST = "Unknown Artist"; public static readonly SortString DEFAULT_ALBUM = "Unknown Album"; @@ -29,7 +35,6 @@ public struct SongMetadata CreditAlbumCover = string.Empty, CreditLicense = string.Empty, Year = DEFAULT_YEAR, - SongLength = 0, SongOffset = 0, PreviewStart = -1, PreviewEnd = -1, @@ -47,7 +52,6 @@ public struct SongMetadata public string Year; - public ulong SongLength; public long SongOffset; public uint SongRating; // 1 = FF; 2 = SR; 3 = M; 4 = NR @@ -69,5 +73,174 @@ public struct SongMetadata public string CreditCourtesyOf; public string CreditAlbumCover; public string CreditLicense; + + public SongMetadata(IniSection modifiers, string defaultPlaylist) + { + modifiers.TryGet("name", out Name, DEFAULT_NAME); + modifiers.TryGet("artist", out Artist, DEFAULT_ARTIST); + modifiers.TryGet("album", out Album, DEFAULT_ALBUM); + modifiers.TryGet("genre", out Genre, DEFAULT_GENRE); + + if (!modifiers.TryGet("year", out Year)) + { + if (modifiers.TryGet("year_chart", out Year)) + { + if (Year.StartsWith(", ")) + { + Year = Year[2..]; + } + else if (Year.StartsWith(',')) + { + Year = Year[1..]; + } + } + else + { + Year = DEFAULT_YEAR; + } + } + + if (!modifiers.TryGet("charter", out Charter, DEFAULT_CHARTER)) + { + modifiers.TryGet("frets", out Charter, DEFAULT_CHARTER); + } + + modifiers.TryGet("icon", out Source, DEFAULT_SOURCE); + modifiers.TryGet("playlist", out Playlist, defaultPlaylist); + + modifiers.TryGet("loading_phrase", out LoadingPhrase); + + modifiers.TryGet("credit_written_by", out CreditWrittenBy); + modifiers.TryGet("credit_performed_by", out CreditPerformedBy); + modifiers.TryGet("credit_courtesy_of", out CreditCourtesyOf); + modifiers.TryGet("credit_album_cover", out CreditAlbumCover); + modifiers.TryGet("credit_license", out CreditLicense); + + if (!modifiers.TryGet("playlist_track", out PlaylistTrack)) + { + PlaylistTrack = -1; + } + + if (!modifiers.TryGet("album_track", out AlbumTrack)) + { + AlbumTrack = -1; + } + + modifiers.TryGet("rating", out SongRating); + + modifiers.TryGet("video_start_time", out VideoStartTime); + if (!modifiers.TryGet("video_end_time", out VideoEndTime)) + { + VideoEndTime = -1; + } + + if (!modifiers.TryGet("preview", out PreviewStart, out PreviewEnd)) + { + if (!modifiers.TryGet("preview_start_time", out PreviewStart)) + { + // Capitlization = from .chart + if (modifiers.TryGet("previewStart_chart", out double previewStartSeconds)) + { + PreviewStart = (long) (previewStartSeconds * MILLISECOND_FACTOR); + } + else + { + PreviewStart = -1; + } + } + + if (!modifiers.TryGet("preview_end_time", out PreviewEnd)) + { + // Capitlization = from .chart + if (modifiers.TryGet("previewEnd_chart", out double previewEndSeconds)) + { + PreviewEnd = (long) (previewEndSeconds * MILLISECOND_FACTOR); + } + else + { + PreviewEnd = -1; + } + } + } + + if (!modifiers.TryGet("delay", out SongOffset) || SongOffset == 0) + { + if (modifiers.TryGet("delay_chart", out double songOffsetSeconds)) + { + SongOffset = (long) (songOffsetSeconds * MILLISECOND_FACTOR); + } + } + + IsMaster = !modifiers.TryGet("tags", out string tag) || tag.ToLower() != "cover"; + } + + public SongMetadata(UnmanagedMemoryStream stream, CategoryCacheStrings strings) + { + Name = strings.titles[stream.Read(Endianness.Little)]; + Artist = strings.artists[stream.Read(Endianness.Little)]; + Album = strings.albums[stream.Read(Endianness.Little)]; + Genre = strings.genres[stream.Read(Endianness.Little)]; + + Year = strings.years[stream.Read(Endianness.Little)]; + Charter = strings.charters[stream.Read(Endianness.Little)]; + Playlist = strings.playlists[stream.Read(Endianness.Little)]; + Source = strings.sources[stream.Read(Endianness.Little)]; + + IsMaster = stream.ReadBoolean(); + + AlbumTrack = stream.Read(Endianness.Little); + PlaylistTrack = stream.Read(Endianness.Little); + + SongOffset = stream.Read(Endianness.Little); + SongRating = stream.Read(Endianness.Little); + + PreviewStart = stream.Read(Endianness.Little); + PreviewEnd = stream.Read(Endianness.Little); + + VideoStartTime = stream.Read(Endianness.Little); + VideoEndTime = stream.Read(Endianness.Little); + + LoadingPhrase = stream.ReadString(); + + CreditWrittenBy = stream.ReadString(); + CreditPerformedBy = stream.ReadString(); + CreditCourtesyOf = stream.ReadString(); + CreditAlbumCover = stream.ReadString(); + CreditLicense = stream.ReadString(); + } + + public readonly void Serialize(MemoryStream stream, CategoryCacheWriteNode node) + { + stream.Write(node.title, Endianness.Little); + stream.Write(node.artist, Endianness.Little); + stream.Write(node.album, Endianness.Little); + stream.Write(node.genre, Endianness.Little); + stream.Write(node.year, Endianness.Little); + stream.Write(node.charter, Endianness.Little); + stream.Write(node.playlist, Endianness.Little); + stream.Write(node.source, Endianness.Little); + + stream.Write(IsMaster); + + stream.Write(AlbumTrack, Endianness.Little); + stream.Write(PlaylistTrack, Endianness.Little); + + stream.Write(SongOffset, Endianness.Little); + stream.Write(SongRating, Endianness.Little); + + stream.Write(PreviewStart, Endianness.Little); + stream.Write(PreviewEnd, Endianness.Little); + + stream.Write(VideoStartTime, Endianness.Little); + stream.Write(VideoEndTime, Endianness.Little); + + stream.Write(LoadingPhrase); + + stream.Write(CreditWrittenBy); + stream.Write(CreditPerformedBy); + stream.Write(CreditCourtesyOf); + stream.Write(CreditAlbumCover); + stream.Write(CreditLicense); + } } } diff --git a/YARG.Core/Song/Preparsers/DrumPreparseHandler.cs b/YARG.Core/Song/Preparsers/DrumPreparseHandler.cs index 8a86e10e2..0ed5563db 100644 --- a/YARG.Core/Song/Preparsers/DrumPreparseHandler.cs +++ b/YARG.Core/Song/Preparsers/DrumPreparseHandler.cs @@ -90,16 +90,12 @@ public unsafe void ParseMidi(YARGMidiTrack track) const int MAX_NUMPADS = 7; const int DRUMNOTE_MAX = 101; const int DOUBLE_KICK_NOTE = 95; - const int EXPERT_INDEX = 3; - const int EXPERT_PLUS_INDEX = 4; - const int DOUBLE_KICK_OFFSET = EXPERT_INDEX * MAX_NUMPADS + 1; + const int DOUBLE_KICK_MASK = 1 << (3 * MAX_NUMPADS + 1); const int FIVE_LANE_INDEX = 6; const int YELLOW_FLAG = 110; const int GREEN_FLAG = 112; - // +1 for Expert+ - var difficulties = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES + 1]; - var statuses = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES * MAX_NUMPADS]; + int statusBitMask = 0; var note = default(MidiNote); while (track.ParseEvent()) { @@ -112,7 +108,7 @@ public unsafe void ParseMidi(YARGMidiTrack track) // Must be checked first as it still resides in the normal note range window if (note.value == DOUBLE_KICK_NOTE) { - if (difficulties[EXPERT_PLUS_INDEX]) + if ((_validations & DifficultyMask.ExpertPlus) > 0) { continue; } @@ -120,22 +116,21 @@ public unsafe void ParseMidi(YARGMidiTrack track) // Note Ons with no velocity equates to a note Off by spec if (track.Type == MidiEventType.Note_On && note.velocity > 0) { - statuses[DOUBLE_KICK_OFFSET] = true; + statusBitMask |= DOUBLE_KICK_MASK; } // NoteOff here - else if (statuses[DOUBLE_KICK_OFFSET]) + else if ((statusBitMask & DOUBLE_KICK_MASK) > 0) { _validations |= DifficultyMask.Expert | DifficultyMask.ExpertPlus; - difficulties[EXPERT_INDEX] = true; - difficulties[EXPERT_PLUS_INDEX] = true; } } else if (MidiPreparser_Constants.DEFAULT_NOTE_MIN <= note.value && note.value <= DRUMNOTE_MAX) { int noteOffset = note.value - MidiPreparser_Constants.DEFAULT_NOTE_MIN; int diffIndex = MidiPreparser_Constants.DIFF_INDICES[noteOffset]; + var diffMask = (DifficultyMask) (1 << (diffIndex + 1)); // Necessary to account for potential five lane - if (difficulties[diffIndex] && Type != DrumsType.Unknown && Type != DrumsType.UnknownPro) + if ((_validations & diffMask) > 0 && Type != DrumsType.Unknown && Type != DrumsType.UnknownPro) { continue; } @@ -148,20 +143,20 @@ public unsafe void ParseMidi(YARGMidiTrack track) continue; } + int statusMask = 1 << (diffIndex * MAX_NUMPADS + laneIndex); // Note Ons with no velocity equates to a note Off by spec if (track.Type == MidiEventType.Note_On && note.velocity > 0) { - statuses[diffIndex * MAX_NUMPADS + laneIndex] = true; + statusBitMask |= statusMask; if (laneIndex == FIVE_LANE_INDEX) { Type = DrumsType.FiveLane; } } // NoteOff here - else if (statuses[diffIndex * MAX_NUMPADS + laneIndex]) + else if ((statusBitMask & statusMask) > 0) { - _validations |= (DifficultyMask) (1 << (diffIndex + 1)); - difficulties[diffIndex] = true; + _validations |= diffMask; } } else if (YELLOW_FLAG <= note.value && note.value <= GREEN_FLAG && Type != DrumsType.FiveLane) diff --git a/YARG.Core/Song/Preparsers/Midi/MidiEliteDrumsPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiEliteDrumsPreparser.cs index bbeb7f788..53f59bf00 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiEliteDrumsPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiEliteDrumsPreparser.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using YARG.Core.IO; +using YARG.Core.IO; namespace YARG.Core.Song { @@ -14,8 +11,7 @@ public static class Midi_EliteDrums_Preparser public static unsafe DifficultyMask Parse(YARGMidiTrack track) { var validations = default(DifficultyMask); - var difficulties = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES]; - var statuses = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES * NUM_LANES]; + long statusBitMask = 0; var note = default(MidiNote); while (track.ParseEvent()) @@ -31,21 +27,22 @@ public static unsafe DifficultyMask Parse(YARGMidiTrack track) int diffIndex = MidiPreparser_Constants.EXTENDED_DIFF_INDICES[note.value]; int laneIndex = MidiPreparser_Constants.EXTENDED_LANE_INDICES[note.value]; - if (difficulties[diffIndex] || laneIndex >= NUM_LANES) + var diffMask = (DifficultyMask)(1 << (diffIndex + 1)); + if ((validations & diffMask) > 0 || laneIndex >= NUM_LANES) { continue; } + long statusMask = 1L << (diffIndex * NUM_LANES + laneIndex); // Note Ons with no velocity equates to a note Off by spec if (track.Type == MidiEventType.Note_On && note.velocity > 0) { - statuses[diffIndex * NUM_LANES + laneIndex] = true; + statusBitMask |= statusMask; } // Note off here - else if (statuses[diffIndex * NUM_LANES + laneIndex]) + else if ((statusBitMask & statusMask) > 0) { - validations |= (DifficultyMask) (1 << (diffIndex + 1)); - difficulties[diffIndex] = true; + validations |= diffMask; if (validations == MidiPreparser_Constants.ALL_DIFFICULTIES) { break; diff --git a/YARG.Core/Song/Preparsers/Midi/MidiFiveFretPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiFiveFretPreparser.cs index 5fb990b27..7b2f0d1bb 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiFiveFretPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiFiveFretPreparser.cs @@ -25,8 +25,7 @@ public static unsafe DifficultyMask Parse(YARGMidiTrack track) { ReadOnlySpan SYSEXTAG = stackalloc byte[] { (byte) 'P', (byte) 'S', (byte) '\0', }; var validations = default(DifficultyMask); - var difficulties = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES]; - var statuses = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES * NUM_LANES]; + int statusBitMask = 0; // Zero is reserved for open notes. Open notes apply in two situations: // 1. The 13s will swap to zeroes when the ENHANCED_OPENS toggle occurs @@ -55,21 +54,22 @@ public static unsafe DifficultyMask Parse(YARGMidiTrack track) int noteOffset = note.value - FIVEFRET_MIN; int diffIndex = MidiPreparser_Constants.DIFF_INDICES[noteOffset]; int laneIndex = indices[noteOffset]; - if (difficulties[diffIndex] || laneIndex >= NUM_LANES) + var diffMask = (DifficultyMask) (1 << (diffIndex + 1)); + if ((validations & diffMask) > 0 || laneIndex >= NUM_LANES) { continue; } + int statusMask = 1 << (diffIndex * NUM_LANES + laneIndex); // Note Ons with no velocity equates to a note Off by spec if (track.Type == MidiEventType.Note_On && note.velocity > 0) { - statuses[diffIndex * NUM_LANES + laneIndex] = true; + statusBitMask |= statusMask; } // Note off here - else if (statuses[diffIndex * NUM_LANES + laneIndex]) + else if ((statusBitMask & statusMask) > 0) { - validations |= (DifficultyMask) (1 << (diffIndex + 1)); - difficulties[diffIndex] = true; + validations |= diffMask; if (validations == MidiPreparser_Constants.ALL_DIFFICULTIES) { break; diff --git a/YARG.Core/Song/Preparsers/Midi/MidiInstrumentPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiInstrumentPreparser.cs index c6205a4eb..0c0d72a00 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiInstrumentPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiInstrumentPreparser.cs @@ -1,6 +1,4 @@ -using YARG.Core.IO; - -namespace YARG.Core.Song +namespace YARG.Core.Song { public static class MidiPreparser_Constants { diff --git a/YARG.Core/Song/Preparsers/Midi/MidiProGuitarPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiProGuitarPreparser.cs index 93737a13d..4a66b4a2f 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiProGuitarPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiProGuitarPreparser.cs @@ -26,8 +26,7 @@ public static DifficultyMask Parse_22Fret(YARGMidiTrack track) private static unsafe DifficultyMask Parse(YARGMidiTrack track, int maxVelocity) { var validations = default(DifficultyMask); - var difficulties = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES]; - var statuses = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES * NUM_STRINGS]; + int statusBitMask = 0; var note = default(MidiNote); while (track.ParseEvent()) @@ -43,25 +42,26 @@ private static unsafe DifficultyMask Parse(YARGMidiTrack track, int maxVelocity) int noteOffset = note.value - PROGUITAR_MIN; int diffIndex = MidiPreparser_Constants.EXTENDED_DIFF_INDICES[noteOffset]; int laneIndex = MidiPreparser_Constants.EXTENDED_LANE_INDICES[noteOffset]; + var diffMask = (DifficultyMask) (1 << (diffIndex + 1)); // Ghost notes aren't played - if (difficulties[diffIndex] || laneIndex >= NUM_STRINGS || track.Channel == ARPEGGIO_CHANNEL) + if ((validations & diffMask) > 0 || laneIndex >= NUM_STRINGS || track.Channel == ARPEGGIO_CHANNEL) { continue; } + int statusMask = 1 << (diffIndex * NUM_STRINGS + laneIndex); // Note Ons with no velocity equates to a note Off by spec if (track.Type == MidiEventType.Note_On && note.velocity > 0) { if (MIN_VELOCITY <= note.velocity && note.velocity <= maxVelocity) { - statuses[diffIndex * NUM_STRINGS + laneIndex] = true; + statusBitMask |= statusMask; } } // Note off here - else if (statuses[diffIndex * NUM_STRINGS + laneIndex]) + else if ((statusBitMask & statusMask) > 0) { - validations |= (DifficultyMask) (1 << (diffIndex + 1)); - difficulties[diffIndex] = true; + validations |= diffMask; if (validations == MidiPreparser_Constants.ALL_DIFFICULTIES) { break; diff --git a/YARG.Core/Song/Preparsers/Midi/MidiProKeysPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiProKeysPreparser.cs index ff48c5caa..0cfb69a16 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiProKeysPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiProKeysPreparser.cs @@ -10,7 +10,7 @@ public static class Midi_ProKeys_Preparser public static unsafe bool Parse(YARGMidiTrack track) { - var statuses = stackalloc bool[NOTES_IN_DIFFICULTY]; + int statusBitMask = 0; var note = default(MidiNote); while (track.ParseEvent()) { @@ -19,11 +19,12 @@ public static unsafe bool Parse(YARGMidiTrack track) track.ExtractMidiNote(ref note); if (PROKEYS_MIN <= note.value && note.value <= PROKEYS_MAX) { + int statusMask = 1 << (note.value - PROKEYS_MIN); if (track.Type == MidiEventType.Note_On && note.velocity > 0) { - statuses[note.value - PROKEYS_MIN] = true; + statusBitMask |= statusMask; } - else if (statuses[note.value - PROKEYS_MIN]) + else if ((statusBitMask & statusMask) > 0) { return true; } diff --git a/YARG.Core/Song/Preparsers/Midi/MidiSixFretPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiSixFretPreparser.cs index d2b23273d..1b2d52f49 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiSixFretPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiSixFretPreparser.cs @@ -21,8 +21,7 @@ public static class Midi_SixFret_Preparser public static unsafe DifficultyMask Parse(YARGMidiTrack track) { var validations = default(DifficultyMask); - var difficulties = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES]; - var statuses = stackalloc bool[MidiPreparser_Constants.NUM_DIFFICULTIES * NUM_LANES]; + int statusBitMask = 0; var note = default(MidiNote); while (track.ParseEvent()) @@ -38,21 +37,22 @@ public static unsafe DifficultyMask Parse(YARGMidiTrack track) int noteOffset = note.value - SIXFRET_MIN; int diffIndex = MidiPreparser_Constants.DIFF_INDICES[noteOffset]; int laneIndex = INDICES[noteOffset]; - if (difficulties[diffIndex] || laneIndex >= NUM_LANES) + var diffMask = (DifficultyMask) (1 << (diffIndex + 1)); + if ((validations & diffMask) > 0 || laneIndex >= NUM_LANES) { continue; } + int statusMask = 1 << (diffIndex * NUM_LANES + laneIndex); // Note Ons with no velocity equates to a note Off by spec if (track.Type == MidiEventType.Note_On && note.velocity > 0) { - statuses[diffIndex * NUM_LANES + laneIndex] = true; + statusBitMask |= statusMask; } // Note off here - else if (statuses[diffIndex * NUM_LANES + laneIndex]) + else if ((statusBitMask & statusMask) > 0) { - validations |= (DifficultyMask) (1 << (diffIndex + 1)); - difficulties[diffIndex] = true; + validations |= diffMask; if (validations == MidiPreparser_Constants.ALL_DIFFICULTIES) { break; diff --git a/YARG.Core/Song/Preparsers/Midi/MidiVocalPreparser.cs b/YARG.Core/Song/Preparsers/Midi/MidiVocalPreparser.cs index f4d4c91dd..14de5c96d 100644 --- a/YARG.Core/Song/Preparsers/Midi/MidiVocalPreparser.cs +++ b/YARG.Core/Song/Preparsers/Midi/MidiVocalPreparser.cs @@ -1,5 +1,4 @@ -using System; -using YARG.Core.IO; +using YARG.Core.IO; namespace YARG.Core.Song {