diff --git a/Assets/Script/Data/Difficulty.cs b/Assets/Script/Data/Difficulty.cs index d311b66af..8c913a195 100644 --- a/Assets/Script/Data/Difficulty.cs +++ b/Assets/Script/Data/Difficulty.cs @@ -29,5 +29,15 @@ public static Difficulty FromChar(char diff) { _ => throw new System.Exception("Unknown difficulty.") }; } + + public static string ToDifficultyString(Difficulty difficulty) { + return difficulty switch { + Difficulty.EASY => "Easy", + Difficulty.MEDIUM => "Medium", + Difficulty.HARD => "Hard", + Difficulty.EXPERT => "Expert", + Difficulty.EXPERT_PLUS => "Expert+", + }; + } } } \ No newline at end of file diff --git a/Assets/Script/Song/Preparsers.meta b/Assets/Script/Song/Preparsers.meta new file mode 100644 index 000000000..1e7faa20f --- /dev/null +++ b/Assets/Script/Song/Preparsers.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6ce168f8e97745bda3d6ee9eebfa28ab +timeCreated: 1683322724 \ No newline at end of file diff --git a/Assets/Script/Song/Preparsers/ChartPreparser.cs b/Assets/Script/Song/Preparsers/ChartPreparser.cs new file mode 100644 index 000000000..03a3851a9 --- /dev/null +++ b/Assets/Script/Song/Preparsers/ChartPreparser.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; +using YARG.Data; +using Debug = UnityEngine.Debug; + +namespace YARG.Song.Preparsers { + public static class ChartPreparser { + + private static readonly Regex ChartEventRegex = new Regex(@"(\d+)\s?=\s?[NSE]\s?((\d+\s?\d+)|\w+)", RegexOptions.Compiled); + + private static readonly IReadOnlyDictionary DifficultyLookup = new Dictionary() + { + { Difficulty.EASY, "Easy" }, + { Difficulty.MEDIUM, "Medium" }, + { Difficulty.HARD, "Hard" }, + { Difficulty.EXPERT, "Expert" } + }; + + private static readonly IReadOnlyDictionary InstrumentLookup = new Dictionary() + { + { Instrument.GUITAR, "Single" }, + { Instrument.GUITAR_COOP, "DoubleGuitar" }, + { Instrument.BASS, "DoubleBass" }, + { Instrument.RHYTHM, "DoubleRhythm" }, + { Instrument.DRUMS, "Drums" }, + { Instrument.KEYS, "Keyboard" }, + // { Instrument.GHLiveGuitar, "GHLGuitar" }, + // { Instrument.GHLiveBass, "GHLBass" }, + // { Instrument.GHLiveRhythm, "GHLRhythm" }, + // { Instrument.GHLiveCoop, "GHLCoop" } + }; + + public static ulong GetAvailableTracks(byte[] chartData) { + using var stream = new MemoryStream(chartData); + + using var reader = new StreamReader(stream); + return ReadStream(reader); + } + + public static ulong GetAvailableTracks(SongEntry song) { + using var reader = File.OpenText(Path.Combine(song.Location, song.NotesFile)); + + return ReadStream(reader); + } + + private static ulong ReadStream(StreamReader reader) { + ulong tracks = 0; + + while (!reader.EndOfStream) { + string line = reader.ReadLine()?.Trim(); + if (line is null) { + continue; + } + + if (line.Length <= 0) + continue; + + if (line[0] != '[' && line[^1] != ']') continue; + + string headerName = line[1..^1]; + if (reader.ReadLine()?.Trim() != "{") continue; + + string eventLine = reader.ReadLine()?.Trim(); + if (eventLine is null) { + continue; + } + + // This track has an event in it! + if (!ChartEventRegex.IsMatch(eventLine) || !GetTrackFromHeader(headerName, out var track)) { + continue; + } + + int shiftAmount = (int)track.instrument * 4 + (int)track.difficulty; + tracks |= (uint)(1 << shiftAmount); + } + + return tracks; + } + + private static bool GetTrackFromHeader(string header, out (Instrument instrument, Difficulty difficulty) track) { + var diffEnums = (Difficulty[]) Enum.GetValues(typeof(Difficulty)); + var instrumentEnums = (Instrument[]) Enum.GetValues(typeof(Instrument)); + + foreach (var instrument in instrumentEnums) { + if (!InstrumentLookup.ContainsKey(instrument)) + continue; + + foreach (var difficulty in diffEnums) { + if(!DifficultyLookup.ContainsKey(difficulty)) + continue; + + var trackName = $"{DifficultyLookup[difficulty]}{InstrumentLookup[instrument]}"; + + if (header != trackName) { + continue; + } + + track = (instrument, difficulty); + return true; + } + } + + track = (Instrument.INVALID, (Difficulty)(-1)); + return false; + } + } +} \ No newline at end of file diff --git a/Assets/Script/Song/Preparsers/ChartPreparser.cs.meta b/Assets/Script/Song/Preparsers/ChartPreparser.cs.meta new file mode 100644 index 000000000..c98880fd1 --- /dev/null +++ b/Assets/Script/Song/Preparsers/ChartPreparser.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 638b3b28bd9f4b4fb121b8de77cb540f +timeCreated: 1683322845 \ No newline at end of file diff --git a/Assets/Script/Song/Scanning/SongScanThread.cs b/Assets/Script/Song/Scanning/SongScanThread.cs index 90fdfc305..51aef6a9f 100644 --- a/Assets/Script/Song/Scanning/SongScanThread.cs +++ b/Assets/Script/Song/Scanning/SongScanThread.cs @@ -6,6 +6,7 @@ using System.Threading; using UnityEngine; using YARG.Serialization; +using YARG.Song.Preparsers; namespace YARG.Song { public class SongScanThread { @@ -194,12 +195,18 @@ private static ScanResult ScanIniSong(string cache, string directory, out SongEn var checksum = BitConverter.ToString(SHA1.Create().ComputeHash(bytes)).Replace("-", ""); + var tracks = ulong.MaxValue; + if (notesFile == "notes.chart") { + tracks = ChartPreparser.GetAvailableTracks(bytes); + } + // We have a song.ini, notes file and audio. The song is scannable. song = new IniSongEntry { CacheRoot = cache, Location = directory, Checksum = checksum, NotesFile = notesFile, + AvailableParts = tracks, }; return ScanHelpers.ParseSongIni(Path.Combine(directory, "song.ini"), (IniSongEntry) song); diff --git a/Assets/Script/Song/SongCache.cs b/Assets/Script/Song/SongCache.cs index bb73b2519..44c6c8a1f 100644 --- a/Assets/Script/Song/SongCache.cs +++ b/Assets/Script/Song/SongCache.cs @@ -12,7 +12,7 @@ public class SongCache { /// /// The date in which the cache version is based on. /// - private const int CACHE_VERSION = 23_05_05; + private const int CACHE_VERSION = 23_05_06; private readonly string _folder; private readonly string _cacheFile; @@ -111,6 +111,8 @@ private static void WriteSongEntry(BinaryWriter writer, SongEntry song) { writer.Write((int) difficulty.Key); writer.Write(difficulty.Value); } + + writer.Write(song.AvailableParts); switch (song) { case ExtractedConSongEntry conSong: @@ -172,6 +174,8 @@ private static SongEntry ReadSongEntry(BinaryReader reader) { result.PartDifficulties.Add(part, difficulty); } + result.AvailableParts = (ulong)reader.ReadInt64(); + switch (type) { case SongType.ExtractedRbCon: CacheHelpers.ReadExtractedConData(reader, (ExtractedConSongEntry) result); diff --git a/Assets/Script/Song/Types/SongEntry.cs b/Assets/Script/Song/Types/SongEntry.cs index 071405a99..a5cb7f2a7 100644 --- a/Assets/Script/Song/Types/SongEntry.cs +++ b/Assets/Script/Song/Types/SongEntry.cs @@ -59,9 +59,20 @@ public abstract class SongEntry { public Dictionary PartDifficulties { get; } = new(); - public string Checksum { get; set; } + public ulong AvailableParts { get; set; } + + public string Checksum { get; set; } public string NotesFile { get; set; } - public string Location { get; set; } + public string Location { get; set; } + + public bool HasInstrument(Instrument instrument) { + long instrumentBits = 0xF << (int)instrument * 4; + return (AvailableParts & (ulong)instrumentBits) != 0; + } + public bool HasPart(Instrument instrument, Difficulty difficulty) { + long instrumentBits = 0x1 << (int)instrument * 4 + (int)difficulty; + return (AvailableParts & (ulong)instrumentBits) != 0; + } } } \ No newline at end of file diff --git a/Assets/Script/UI/DifficultySelect.cs b/Assets/Script/UI/DifficultySelect.cs index 23166fcfc..ec4deef5e 100644 --- a/Assets/Script/UI/DifficultySelect.cs +++ b/Assets/Script/UI/DifficultySelect.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using TMPro; using UnityEngine; using UnityEngine.AddressableAssets; @@ -29,6 +31,7 @@ private enum State { private int playerIndex; private string[] instruments; + private Difficulty[] difficulties; private State state; private int optionCount; @@ -144,10 +147,11 @@ public void Next() { bool showExpertPlus = player.chosenInstrument == "drums" || player.chosenInstrument == "realDrums" || player.chosenInstrument == "ghDrums"; - UpdateDifficulty(showExpertPlus); + UpdateDifficulty(player.chosenInstrument, showExpertPlus); } } else if (state == State.DIFFICULTY) { - player.chosenDifficulty = (Difficulty) selected; + player.chosenDifficulty = difficulties[selected]; + Debug.Log(player.chosenDifficulty); OnInstrumentSelection?.Invoke(player); IncreasePlayerIndex(); } else if (state == State.VOCALS) { @@ -217,15 +221,25 @@ private void UpdateInstrument() { state = State.INSTRUMENT; // Get allowed instruments - var allowedInstruments = player.inputStrategy.GetAllowedInstruments(); - optionCount = allowedInstruments.Length + 1; + string[] allowedInstruments = player.inputStrategy.GetAllowedInstruments(); + + var availableInstruments = allowedInstruments + .Where(instrument => MainMenu.Instance.chosenSong + .HasInstrument(InstrumentHelper.FromStringName(instrument))).ToList(); + + optionCount = availableInstruments.Count + 1; // Add to options - string[] ops = new string[6]; - instruments = new string[allowedInstruments.Length]; - for (int i = 0; i < allowedInstruments.Length; i++) { - instruments[i] = allowedInstruments[i]; - ops[i] = allowedInstruments[i] switch { + var ops = new string[availableInstruments.Count + 1]; + instruments = new string[availableInstruments.Count]; + + for (int i = 0; i < instruments.Length; i++) { + if (!MainMenu.Instance.chosenSong.HasInstrument( + InstrumentHelper.FromStringName(allowedInstruments[i]))) { + continue; + } + instruments[i] = availableInstruments[i]; + ops[i] = availableInstruments[i] switch { "drums" => "Drums", "realDrums" => "Pro Drums", "guitar" => "Guitar", @@ -242,13 +256,17 @@ private void UpdateInstrument() { _ => "Unknown" }; } - ops[allowedInstruments.Length] = "Sit Out"; + ops[^1] = "Sit Out"; // Set text and sprites for (int i = 0; i < 6; i++) { - options[i].SetText(ops[i]); + options[i].SetText(""); options[i].SetSelected(false); + if (i < ops.Length) { + options[i].SetText(ops[i]); + } + if (i < instruments.Length) { var sprite = Addressables.LoadAssetAsync($"FontSprites[{instruments[i]}]").WaitForCompletion(); options[i].SetImage(sprite); @@ -260,31 +278,50 @@ private void UpdateInstrument() { options[0].SetSelected(true); } - private void UpdateDifficulty(bool showExpertPlus) { + private void UpdateDifficulty(string chosenInstrument, bool showExpertPlus) { state = State.DIFFICULTY; - optionCount = 4; - string[] ops = { - "Easy", - "Medium", - "Hard", - "Expert", - null, - null - }; + var instrument = InstrumentHelper.FromStringName(chosenInstrument); + var availableDifficulties = new List(); + for (int i = 0; i < (int)Difficulty.EXPERT_PLUS; i++) { + if (!MainMenu.Instance.chosenSong.HasPart(instrument, (Difficulty)i)) { + continue; + } + availableDifficulties.Add((Difficulty)i); + } if (showExpertPlus) { - optionCount++; - ops[4] = "Expert+"; + availableDifficulties.Add(Difficulty.EXPERT_PLUS); + } + + optionCount = availableDifficulties.Count; + + difficulties = new Difficulty[optionCount]; + var ops = new string[optionCount]; + + for(int i = 0; i < optionCount; i++) { + ops[i] = availableDifficulties[i] switch { + Difficulty.EASY => "Easy", + Difficulty.MEDIUM => "Medium", + Difficulty.HARD => "Hard", + Difficulty.EXPERT => "Expert", + Difficulty.EXPERT_PLUS => "Expert+", + _ => "Unknown" + }; + difficulties[i] = availableDifficulties[i]; } for (int i = 0; i < 6; i++) { - options[i].SetText(ops[i]); + options[i].SetText(""); options[i].SetSelected(false); + + if (i < ops.Length) { + options[i].SetText(ops[i]); + } } - selected = 3; - options[3].SetSelected(true); + selected = optionCount - 1; + options[optionCount - 1].SetSelected(true); } private void UpdateVocalOptions() {