diff --git a/MSURandomizer/MSURandomizer.csproj b/MSURandomizer/MSURandomizer.csproj index 691d7a3..86c0262 100644 --- a/MSURandomizer/MSURandomizer.csproj +++ b/MSURandomizer/MSURandomizer.csproj @@ -6,7 +6,7 @@ true app.manifest true - 3.0.0-rc.3 + 3.0.0-rc.3a MSURandomizerIcon.ico true MattEqualsCoder.MSURandomizer.Avalonia diff --git a/MSURandomizer/Services/AppInitializationService.cs b/MSURandomizer/Services/AppInitializationService.cs index 3e184ef..9d39714 100644 --- a/MSURandomizer/Services/AppInitializationService.cs +++ b/MSURandomizer/Services/AppInitializationService.cs @@ -31,7 +31,7 @@ public void Initialize(string[] args) msuRandomizerInitializationService.Initialize(new MsuRandomizerInitializationRequest() { InitializeAppSettings = true, - InitializeMsuTypes = false, + InitializeMsuTypes = true, InitializeCache = false, InitializeUserOptions = true, LookupMsus = false, @@ -78,7 +78,7 @@ public void FinishInitialization() InitializeAppSettings = false, InitializeMsuTypes = true, InitializeCache = true, - InitializeUserOptions = true, + InitializeUserOptions = false, LookupMsus = true }); msuGameService.InstallLuaScripts(); diff --git a/MSURandomizer/Services/MsuDetailsService.cs b/MSURandomizer/Services/MsuDetailsService.cs index e6cbded..013113d 100644 --- a/MSURandomizer/Services/MsuDetailsService.cs +++ b/MSURandomizer/Services/MsuDetailsService.cs @@ -25,10 +25,18 @@ public MsuDetailsWindowViewModel InitilizeModel(MsuViewModel model) mapper.Map(_parentModel.Msu.Settings, Model); _originalMsuTypeName = Model.MsuTypeName; Model.Msu = _parentModel.Msu; - Model.Tracks = - _parentModel.Msu.MsuType?.Tracks.OrderBy(x => x.Number) - .Select(t => new MsuTrackViewModel(t, _parentModel.Msu.Tracks)).ToList() ?? - []; + + if (_parentModel.Msu.MsuType != null) + { + Model.Tracks = _parentModel.Msu.MsuType.Tracks.OrderBy(x => x.Number) + .Select(t => new MsuTrackViewModel(t, _parentModel.Msu.Tracks)).ToList(); + } + else + { + Model.Tracks = _parentModel.Msu.Tracks.OrderBy(x => x.Number) + .Select(t => new MsuTrackViewModel(t, _parentModel.Msu.Tracks)).ToList(); + } + Model.TrackCount = Model.Tracks.Count; Model.MsuTypeNames = [""]; Model.MsuTypeNames.AddRange(msuTypeService.MsuTypes.Select(x => x.DisplayName).OrderBy(x => x)); diff --git a/MSURandomizer/Services/MsuListService.cs b/MSURandomizer/Services/MsuListService.cs index 60da3c1..8fc35dd 100644 --- a/MSURandomizer/Services/MsuListService.cs +++ b/MSURandomizer/Services/MsuListService.cs @@ -72,11 +72,6 @@ public void FilterMSUs(MsuType msuType, MsuFilter msuFilter) { Model.MsuTypeName = msuType.DisplayName; Model.MsuType = msuType; - var msuTypePath = Model.HardwareMode ? "" : - userOptions.MsuUserOptions.MsuTypePaths.TryGetValue(msuType, out var path) ? path : - userOptions.MsuUserOptions.DefaultMsuPath; - var rootPath = Model.HardwareMode ? "" : GetMsuTypeBasePath(msuType); - var useAbsolutePath = string.IsNullOrWhiteSpace(rootPath); // Hardware MSUs are more limited in compatibility List? compatibleMsuNames = null; @@ -94,15 +89,10 @@ public void FilterMSUs(MsuType msuType, MsuFilter msuFilter) } var filteredMsus = Model.MsuViewModels - .Where(x => x.Msu.MatchesFilter(msuFilter, msuType, msuTypePath, compatibleMsuNames) && + .Where(x => x.Msu.MatchesFilter(msuFilter, msuType, compatibleMsuNames) && (x.Msu.NumUniqueTracks > x.Msu.MsuType?.RequiredTrackNumbers.Count / 5 || x.Msu.NumUniqueTracks > 10)) .OrderBy(x => x.MsuName) .ToList(); - foreach (var filteredMsu in filteredMsus) - { - filteredMsu.DisplayPath = useAbsolutePath ? filteredMsu.MsuPath : Path.GetRelativePath(rootPath!, filteredMsu.MsuPath); - } - Model.FilteredMsus = filteredMsus; Model.SelectedMsus = Model.FilteredMsus .Where(x => Model.SelectedMsus.Select(vm => vm.MsuPath).Contains(x.MsuPath)).ToList(); @@ -189,19 +179,4 @@ public void PopulateMsuViewModels(List? msus) OnDisplayUnknownMsuWindowRequest?.Invoke(this, EventArgs.Empty); } } - - private string? GetMsuTypeBasePath(MsuType? msuType) - { - if (msuType == null) - { - return userOptions.MsuUserOptions.DefaultMsuPath; - } - - if (userOptions.MsuUserOptions.MsuTypeNamePaths.TryGetValue(msuType.DisplayName, out string? path)) - { - return path; - } - - return userOptions.MsuUserOptions.DefaultMsuPath; - } } \ No newline at end of file diff --git a/MSURandomizer/Services/SettingsWindowService.cs b/MSURandomizer/Services/SettingsWindowService.cs index 57b7715..41c873b 100644 --- a/MSURandomizer/Services/SettingsWindowService.cs +++ b/MSURandomizer/Services/SettingsWindowService.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Collections.ObjectModel; using System.Linq; using AutoMapper; using AvaloniaControls.Controls; @@ -16,23 +16,23 @@ public class SettingsWindowService( IMsuUserOptionsService userOptionsService, IMsuTypeService msuTypeService, IMsuLookupService msuLookupService, + IMsuCacheService msuCacheService, IMapper mapper) : ControlService { private readonly SettingsWindowViewModel _model = new(); + private Dictionary _msuTypes = []; + private List _msuTypeList = []; public SettingsWindowViewModel InitializeModel() { mapper.Map(userOptionsService.MsuUserOptions, _model); _model.DefaultDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - foreach (var msuType in msuTypeService.MsuTypes.OrderBy(x => x.DisplayName)) - { - _model.MsuTypeNamePathsList.Add(new MsuTypePath() - { - MsuType = msuType, - Path = _model.MsuTypeNamePaths.GetValueOrDefault(msuType.DisplayName, ""), - DefaultDirectory = _model.DefaultDirectory - }); - } + _msuTypes = msuTypeService.MsuTypes.OrderBy(x => x.DisplayName).ToDictionary(x => x.DisplayName, x => x); + _msuTypeList = _msuTypes.Keys.ToList(); + _model.MsuDirectoryList = new ObservableCollection( + userOptionsService.MsuUserOptions.MsuDirectories.Select(x => + new MsuDirectory(x.Key, x.Value, _msuTypeList))); + _model.DisplayNoMsuDirectoriesMessage = _model.MsuDirectoryList.Count == 0; return _model; } @@ -41,12 +41,7 @@ public void SaveModel() var options = userOptionsService.MsuUserOptions; var hasPathUpdated = HasPathUpdated(options); mapper.Map(_model, options); - options.MsuTypePaths = _model.MsuTypeNamePathsList - .Where(x => !string.IsNullOrWhiteSpace(x.Path) && Directory.Exists(x.Path) && x.MsuType != null) - .ToDictionary(x => x.MsuType!, x => x.Path); - options.MsuTypeNamePaths = _model.MsuTypeNamePathsList - .Where(x => !string.IsNullOrWhiteSpace(x.Path) && Directory.Exists(x.Path) && x.MsuType != null) - .ToDictionary(x => x.MsuType!.DisplayName, x => x.Path); + options.MsuDirectories = _model.MsuDirectoryList.ToDictionary(x => x.Path, x => x.MsuTypeName); userOptionsService.Save(); ScalableWindow.GlobalScaleFactor = options.UiScaling; @@ -54,28 +49,42 @@ public void SaveModel() { ITaskService.Run(() => { + msuCacheService.Clear(false); msuLookupService.LookupMsus(); }); } } - private bool HasPathUpdated(MsuUserOptions options) + public bool AddDirectory(string directory) { - if (_model.DefaultMsuPath != options.DefaultMsuPath) + if (_model.MsuDirectoryList.Any(x => x.Path == directory)) { - return true; + return false; } - - foreach (var msuPath in _model.MsuTypeNamePathsList.Where(x => x.MsuType != null)) + _model.MsuDirectoryList.Add(new MsuDirectory(directory, _msuTypeList.FirstOrDefault() ?? "", _msuTypeList)); + _model.DisplayNoMsuDirectoriesMessage = false; + return true; + } + + public void RemoveDirectory(string directory) + { + var directoryToRemove = _model.MsuDirectoryList.FirstOrDefault(x => x.Path == directory); + if (directoryToRemove != null) { - var newPath = msuPath.Path.Trim(); - var oldPath = options.MsuTypePaths.GetValueOrDefault(msuPath.MsuType!, "").Trim(); - if (newPath != oldPath) - { - return true; - } + _model.MsuDirectoryList.Remove(directoryToRemove); + _model.DisplayNoMsuDirectoriesMessage = _model.MsuDirectoryList.Count == 0; + } + } + + private bool HasPathUpdated(MsuUserOptions options) + { + if (_model.MsuDirectoryList.Count != options.MsuDirectories.Count) + { + return true; } - return false; + var currentDirectories = options.MsuDirectories.Select(x => $"{x.Key}={x.Value}"); + var newDirectories = _model.MsuDirectoryList.Select(x => $"{x.Path}={x.MsuTypeName}"); + return !currentDirectories.SequenceEqual(newDirectories); } } \ No newline at end of file diff --git a/MSURandomizer/ViewModels/MsuTrackViewModel.cs b/MSURandomizer/ViewModels/MsuTrackViewModel.cs index 6fa57bc..d11995c 100644 --- a/MSURandomizer/ViewModels/MsuTrackViewModel.cs +++ b/MSURandomizer/ViewModels/MsuTrackViewModel.cs @@ -21,6 +21,15 @@ public MsuTrackViewModel(MsuTypeTrack msuTypeTrack, ICollection songs) .Select(x => new MsuSongViewModel(x)).ToList(); Display = Songs.Count != 0; } + + public MsuTrackViewModel(Track msuTrack, ICollection songs) + { + TrackNumber = msuTrack.Number; + TrackName = msuTrack.TrackName; + Songs = songs.Where(x => x.Number == msuTrack.Number && !x.IsCopied).OrderBy(x => x.IsAlt) + .Select(x => new MsuSongViewModel(x)).ToList(); + Display = Songs.Count != 0; + } public string TrackDisplay => $"{TrackNumber} - {TrackName}"; diff --git a/MSURandomizer/ViewModels/MsuViewModel.cs b/MSURandomizer/ViewModels/MsuViewModel.cs index c21cbbd..ec07f87 100644 --- a/MSURandomizer/ViewModels/MsuViewModel.cs +++ b/MSURandomizer/ViewModels/MsuViewModel.cs @@ -22,7 +22,8 @@ public MsuViewModel(Msu msu) Msu = msu; MsuName = msu.DisplayName; MsuCreator = msu.DisplayCreator; - MsuPath = DisplayPath = msu.Path; + MsuPath = msu.Path; + DisplayPath = msu.RelativePath; MsuTypeName = msu.MsuType?.DisplayName ?? msu.MsuTypeName; MsuTrackCount = $"{msu.ValidTracks.Count} Tracks"; IsFavorite = msu.Settings.IsFavorite; diff --git a/MSURandomizer/ViewModels/SettingsWindowViewModel.cs b/MSURandomizer/ViewModels/SettingsWindowViewModel.cs index 002d63c..ca190c3 100644 --- a/MSURandomizer/ViewModels/SettingsWindowViewModel.cs +++ b/MSURandomizer/ViewModels/SettingsWindowViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using AvaloniaControls; using AvaloniaControls.Models; using MSURandomizerLibrary; @@ -36,6 +37,10 @@ public class SettingsWindowViewModel : ViewModelBase public bool LaunchArgumentsEnabled => !string.IsNullOrEmpty(LaunchApplication); + [Reactive] public bool DisplayNoMsuDirectoriesMessage { get; set; } + + public ObservableCollection MsuDirectoryList { get; set; } = []; + [Reactive] public string? LaunchArguments { get; set; } [Reactive] @@ -61,6 +66,21 @@ public class MsuTypePath : ViewModelBase public string MsuTypeName => MsuType?.DisplayName ?? "A Link to the Past"; } +public class MsuDirectory : ViewModelBase +{ + public MsuDirectory(string path, string msuTypeName = "", List? msuTypes = null) + { + Path = path; + MsuTypeName = msuTypeName; + MsuTypes = msuTypes ?? []; + } + + [Reactive] public string Path { get; set; } + [Reactive] public string MsuTypeName { get; set; } + [Reactive] public List MsuTypes { get; set; } + public string AbbreviatedPath => Path.Length > 40 ? string.Concat("...", Path.AsSpan(Path.Length - 38)) : Path; +} + [MapsTo(typeof(SnesConnectorSettings))] public class SnesConnectorSettingsViewModel : ViewModelBase { diff --git a/MSURandomizer/Views/SettingsWindow.axaml b/MSURandomizer/Views/SettingsWindow.axaml index 536af35..3f824fd 100644 --- a/MSURandomizer/Views/SettingsWindow.axaml +++ b/MSURandomizer/Views/SettingsWindow.axaml @@ -5,6 +5,7 @@ xmlns:controls="clr-namespace:AvaloniaControls.Controls;assembly=AvaloniaControls" xmlns:tools="clr-namespace:AvaloniaControls.Converters;assembly=AvaloniaControls" xmlns:viewModels="clr-namespace:MSURandomizer.ViewModels" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" Width="800" Height="500" @@ -23,6 +24,31 @@ + + + + + + At least one MSU directory must be added. + + + + + + + + + + + + + + + + + @@ -34,14 +60,6 @@ > - - - - - - - - - - - - - - - diff --git a/MSURandomizer/Views/SettingsWindow.axaml.cs b/MSURandomizer/Views/SettingsWindow.axaml.cs index 488b9d5..82ff79c 100644 --- a/MSURandomizer/Views/SettingsWindow.axaml.cs +++ b/MSURandomizer/Views/SettingsWindow.axaml.cs @@ -1,5 +1,8 @@ +using System; +using System.IO; using Avalonia.Controls; using Avalonia.Interactivity; +using AvaloniaControls; using AvaloniaControls.Controls; using AvaloniaControls.Extensions; using MSURandomizer.Services; @@ -26,6 +29,10 @@ public SettingsWindow() new MsuTypePath(), new MsuTypePath(), new MsuTypePath(), + ], + MsuDirectoryList = + [ + new MsuDirectory("/test/path") ] }; return; @@ -45,4 +52,33 @@ private void CancelButton_OnClick(object? sender, RoutedEventArgs e) { Close(); } + + private async void AddDirectoryButton_OnClick(object? sender, RoutedEventArgs e) + { + if (_service == null) + { + return; + } + + var storageItem = await CrossPlatformTools.OpenFileDialogAsync(this, FileInputControlType.Folder, null, null); + var path = storageItem?.Path.LocalPath; + if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) + { + return; + } + + if (!_service.AddDirectory(path)) + { + _ = MessageWindow.ShowErrorDialog("That directory has already been selected"); + } + } + + private void RemoveDirectoryButton_OnClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button { Tag: string directory } || _service == null) + { + return; + } + _service.RemoveDirectory(directory); + } } \ No newline at end of file diff --git a/MSURandomizer/Views/UnknownMsuWindow.axaml b/MSURandomizer/Views/UnknownMsuWindow.axaml index 9247482..94c4d04 100644 --- a/MSURandomizer/Views/UnknownMsuWindow.axaml +++ b/MSURandomizer/Views/UnknownMsuWindow.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:AvaloniaControls.Controls;assembly=AvaloniaControls" xmlns:viewModels="clr-namespace:MSURandomizer.ViewModels" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" Width="800" Height="450" @@ -23,17 +24,30 @@ - The below MSUs have at least 15 tracks, but the MSU Randomizer was unable to discern what type of MSU it is. You can manually set an MSU type below. You can always change this later by right clicking on an MSU and viewing its details. + + The below MSUs have at least 15 tracks, but the application was unable to discern what type of MSU it is. You can manually set an MSU type below or leave it as blank to have it not included as a known compatible MSU. You can always change this later by right clicking on an MSU and viewing its details. + - - - - + + + + public ICollection Tracks { get; set; } = new List(); + /// + /// The MSU Type directory this was found under + /// + public string ParentMsuTypeDirectory { get; set; } = ""; + + /// + /// The relative path to the MSU from its parent msu type directory + /// + [JsonIgnore] + public string RelativePath => !string.IsNullOrEmpty(ParentMsuTypeDirectory) + ? System.IO.Path.GetRelativePath(ParentMsuTypeDirectory, Path) + : Path; + /// /// If this unknown MSU should be ignored /// @@ -180,12 +193,11 @@ public Msu(MsuType? type, string name, string folderName, string fileName, strin /// /// How closely this MSU needs to match the MSU type /// The MSU type being looked for - /// The path being looked at /// Optional list of MSU type names to use for filtering, unless the FilterType All is selected /// True if matches, false otherwise - public bool MatchesFilter(MsuFilter filter, MsuType type, string? path, List? compatibleMsuTypeNames = null) + public bool MatchesFilter(MsuFilter filter, MsuType type, List? compatibleMsuTypeNames = null) { - if (MatchesFilterType(filter, type) && MatchesPath(path) && Tracks.Count >= 1) + if (MatchesFilterType(filter, type) && Tracks.Count >= 1) { if (compatibleMsuTypeNames == null || filter == MsuFilter.All) { @@ -208,11 +220,6 @@ private bool MatchesFilterType(MsuFilter filter, MsuType type) (filter == MsuFilter.Exact && SelectedMsuType?.IsExactMatchWith(type) == true); } - private bool MatchesPath(string? path) - { - return string.IsNullOrEmpty(path) || Path.StartsWith(path); - } - /// /// Returns the track that will play for the track number. Returns the valid fallback track if applicable. /// diff --git a/MSURandomizerLibrary/Configs/MsuUserOptions.cs b/MSURandomizerLibrary/Configs/MsuUserOptions.cs index dd61a03..b5b8bb0 100644 --- a/MSURandomizerLibrary/Configs/MsuUserOptions.cs +++ b/MSURandomizerLibrary/Configs/MsuUserOptions.cs @@ -137,6 +137,11 @@ public class MsuUserOptions /// Directory roms should be copied to before randomizing /// public string? CopyRomDirectory { get; set; } + + /// + /// Dictionary of all MSU directories and the MSU type associated with them + /// + public Dictionary MsuDirectories { get; set; } = new(); /// /// Specific directories to load for specific MSU types @@ -159,7 +164,6 @@ public MsuSettings GetMsuSettings(string path) /// public bool HasMsuFolder() { - return (!string.IsNullOrEmpty(DefaultMsuPath) && Directory.Exists(DefaultMsuPath)) || - MsuTypePaths.Values.Any(x => !string.IsNullOrEmpty(x) && Directory.Exists(x)); + return MsuDirectories.Count > 0; } } diff --git a/MSURandomizerLibrary/MSURandomizerLibrary.csproj b/MSURandomizerLibrary/MSURandomizerLibrary.csproj index e868c7d..e1a68c1 100644 --- a/MSURandomizerLibrary/MSURandomizerLibrary.csproj +++ b/MSURandomizerLibrary/MSURandomizerLibrary.csproj @@ -14,7 +14,7 @@ False False snupkg - 3.0.0-rc.3 + 3.0.0-rc.3a MattEqualsCoder.MSURandomizer.Library True net8.0 diff --git a/MSURandomizerLibrary/Services/IMsuCacheService.cs b/MSURandomizerLibrary/Services/IMsuCacheService.cs index 12c7529..b895143 100644 --- a/MSURandomizerLibrary/Services/IMsuCacheService.cs +++ b/MSURandomizerLibrary/Services/IMsuCacheService.cs @@ -42,4 +42,10 @@ public interface IMsuCacheService /// Saves the cache to the path specified in the Initialize method /// public void Save(); + + /// + /// Removes everything from the cache + /// + /// If the cache should be immediately saved after the operation + public void Clear(bool saveCache); } \ No newline at end of file diff --git a/MSURandomizerLibrary/Services/IMsuLookupService.cs b/MSURandomizerLibrary/Services/IMsuLookupService.cs index fbae090..54fb5f0 100644 --- a/MSURandomizerLibrary/Services/IMsuLookupService.cs +++ b/MSURandomizerLibrary/Services/IMsuLookupService.cs @@ -24,10 +24,10 @@ public interface IMsuLookupService /// Loads all MSUs within a directory and all subdirectories /// /// The directory to search for MSUs - /// A collection of folders for specific msu types + /// A collection of folders for specific msu types /// If what was previously stored in the cache should be ignored /// A collection of all MSUs found - public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary? msuTypeDirectories = null, bool ignoreCache = false); + public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary? msuDirectories = null, bool ignoreCache = false); /// /// Loads a specific MSU and all of its tracks @@ -38,8 +38,9 @@ public interface IMsuLookupService /// If the msu details should be automatically saved to the cache file. /// If what was previously stored in the cache should be ignored /// Force load, even if it was generated by the MSU Randomizer + /// The path in which the MSU was looked for under /// The MSU object with details of the MSU and its found tracks - public Msu? LoadMsu(string msuPath, MsuType? msuTypeFilter = null, bool saveToCache = true, bool ignoreCache = false, bool forceLoad = false); + public Msu? LoadMsu(string msuPath, MsuType? msuTypeFilter = null, bool saveToCache = true, bool ignoreCache = false, bool forceLoad = false, string parentDirectory = ""); /// /// Creates an MSU from files on hardware such as an FxPakPro diff --git a/MSURandomizerLibrary/Services/MsuCacheService.cs b/MSURandomizerLibrary/Services/MsuCacheService.cs index 37d2018..953ddde 100644 --- a/MSURandomizerLibrary/Services/MsuCacheService.cs +++ b/MSURandomizerLibrary/Services/MsuCacheService.cs @@ -6,7 +6,7 @@ namespace MSURandomizerLibrary.Services; internal class MsuCacheService : IMsuCacheService { - private const int CurrentCacheVersion = 2; + private const int CurrentCacheVersion = 3; private readonly IMsuTypeService _msuTypeService; private readonly MsuUserOptions _msuUserOptions; private readonly ILogger _logger; @@ -109,6 +109,15 @@ public void Remove(string msuPath, bool saveCache) } } + public void Clear(bool saveCache) + { + _cache.Data.Clear(); + if (saveCache) + { + Save(); + } + } + public void Save() { if (!_initialized || !_hasUpdated) return; diff --git a/MSURandomizerLibrary/Services/MsuDetailsService.cs b/MSURandomizerLibrary/Services/MsuDetailsService.cs index be9dbb2..95e5219 100644 --- a/MSURandomizerLibrary/Services/MsuDetailsService.cs +++ b/MSURandomizerLibrary/Services/MsuDetailsService.cs @@ -14,6 +14,7 @@ internal class MsuDetailsService : IMsuDetailsService private readonly ISerializer _serializer; private readonly IDeserializer _deserializer; private readonly MsuUserOptions _msuUserOptions; + private readonly Dictionary _loadedDetails = []; public MsuDetailsService(ILogger logger, IMsuUserOptionsService msuUserOptionsService) { @@ -76,12 +77,21 @@ public bool SaveMsuDetails(Msu msu, string outputPath, out string? error) public MsuDetails? GetMsuDetailsForPath(string msuPath, MsuType? msuType) { - var yamlPath = GetMatchingYamlFile(msuPath, msuType); - if (yamlPath == null) + var msuFolder = new FileInfo(msuPath).Directory?.Name; + var msuFileName = Path.GetFileNameWithoutExtension(msuPath); + + foreach (var (yamlPath, msuDetails) in _loadedDetails) { - return null; + var yamlFolder = new FileInfo(yamlPath).Directory?.Name; + var yamlFileName = Path.GetFileNameWithoutExtension(yamlPath); + + if (msuFolder == yamlFolder && msuFileName == yamlFileName) + { + return msuDetails; + } } - return InternalGetMsuDetails(yamlPath, out var yamlHash, out var error); + + return null; } public MsuDetails? GetMsuDetails(string msuPath, out string yamlHash, out string? error) @@ -222,7 +232,9 @@ private MsuDetailsTrack InternalConvertToTrackDetails(Msu msu, IGrouping(yamlText); + var details = _deserializer.Deserialize(yamlText); + _loadedDetails[yamlPath] = details; + return details; } catch (Exception e) { @@ -403,39 +415,4 @@ private List GetTrackDetails(MsuType msuType, MsuDetails msuDetails, stri return null; } } - - private string? GetMatchingYamlFile(string path, MsuType? msuType = null) - { - var msuFolder = new FileInfo(path).Directory?.Name; - var msuFileName = Path.GetFileNameWithoutExtension(path); - - if (Directory.Exists(_msuUserOptions.DefaultMsuPath)) - { - foreach (var ymlFilePath in Directory.EnumerateFiles(_msuUserOptions.DefaultMsuPath, $"{msuFileName}.yml", SearchOption.AllDirectories)) - { - if (new FileInfo(ymlFilePath).Directory?.Name == msuFolder) - { - return ymlFilePath; - } - } - } - - if (msuType != null && _msuUserOptions.MsuTypePaths.TryGetValue(msuType, out var msuTypeDirectory)) - { - if (!Directory.Exists(msuTypeDirectory)) - { - return null; - } - - foreach (var ymlFilePath in Directory.EnumerateFiles(msuTypeDirectory, $"{msuFileName}.yml", SearchOption.AllDirectories)) - { - if (new FileInfo(ymlFilePath).Directory?.Name == msuFolder) - { - return ymlFilePath; - } - } - } - - return null; - } } \ No newline at end of file diff --git a/MSURandomizerLibrary/Services/MsuLookupService.cs b/MSURandomizerLibrary/Services/MsuLookupService.cs index 8a7a389..d834ed7 100644 --- a/MSURandomizerLibrary/Services/MsuLookupService.cs +++ b/MSURandomizerLibrary/Services/MsuLookupService.cs @@ -31,7 +31,7 @@ public MsuLookupService(ILogger logger, IMsuTypeService msuTyp public IReadOnlyCollection LookupMsus() { - return LookupMsus(_msuUserOptions.DefaultMsuPath, _msuUserOptions.MsuTypePaths); + return LookupMsus(_msuUserOptions.DefaultMsuPath, _msuUserOptions.MsuDirectories); } public void RefreshMsuDisplay() @@ -39,7 +39,7 @@ public void RefreshMsuDisplay() OnMsuLookupComplete?.Invoke(this, new MsuListEventArgs(_msus, _errors)); } - public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary? msuTypeDirectories = null, bool ignoreCache = false) + public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary? msuDirectories = null, bool ignoreCache = false) { if (!_msuTypeService.MsuTypes.Any()) { @@ -47,6 +47,8 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< "No valid MSU Types found. Make sure to call IMsuTypeService.GetMsuTypes first."); } + msuDirectories ??= _msuUserOptions.MsuDirectories; + _logger.LogInformation("MSU lookup started"); _errors.Clear(); Status = MsuLoadStatus.Loading; @@ -54,13 +56,29 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< var stopwatch = new Stopwatch(); stopwatch.Start(); - var msusToLoad = GetMsusToLoad(defaultDirectory, msuTypeDirectories); + if (!string.IsNullOrEmpty(defaultDirectory) && msuDirectories?.Count == 0) + { + msuDirectories = new Dictionary() + { + { defaultDirectory, "" } + }; + } + + if (msuDirectories == null || msuDirectories.Count == 0) + { + _msus = []; + Status = MsuLoadStatus.Loaded; + OnMsuLookupComplete?.Invoke(this, new(_msus, _errors)); + return []; + } + + var msusToLoad = GetMsusToLoad(msuDirectories!); var msus = new ConcurrentBag(); Parallel.ForEach(msusToLoad, new ParallelOptions() { MaxDegreeOfParallelism = 5 }, loadInfo => { - var msu = LoadMsu(loadInfo.Item1, loadInfo.Item2, false, ignoreCache); + var msu = LoadMsu(loadInfo.msuPath, loadInfo.msuType, false, ignoreCache, parentDirectory: loadInfo.msuTypeDirectory); if (msu != null) { msus.Add(msu); @@ -77,34 +95,26 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< return _msus; } - private List<(string, MsuType?)> GetMsusToLoad(string? defaultDirectory, Dictionary? msuTypeDirectories = null) + private List<(string msuPath, MsuType? msuType, string msuTypeDirectory)> GetMsusToLoad(Dictionary msuDirectories) { - var msuLookups = new List<(string, MsuType?)>(); - - if (Directory.Exists(defaultDirectory)) - { - foreach (var msuFile in Directory.EnumerateFiles(defaultDirectory, "*.msu", SearchOption.AllDirectories)) - { - msuLookups.Add((msuFile, null)); - } - } + var msuLookups = new List<(string, MsuType?, string)>(); - if (msuTypeDirectories != null) + foreach (var msuDirectory in msuDirectories) { - foreach (var msuTypeDirectory in msuTypeDirectories) + if (!Directory.Exists(msuDirectory.Key)) continue; + foreach (var msuFile in Directory.EnumerateFiles(msuDirectory.Key, "*.msu", SearchOption.AllDirectories)) { - if (!Directory.Exists(msuTypeDirectory.Value)) continue; - foreach (var msuFile in Directory.EnumerateFiles(msuTypeDirectory.Value, "*.msu", SearchOption.AllDirectories)) - { - msuLookups.Add((msuFile, msuTypeDirectory.Key)); - } - } + var msuType = _msuTypeService.GetMsuType(msuDirectory.Value); + msuLookups.Add((msuFile, msuType, msuDirectory.Key)); + } } return msuLookups; } - public Msu? LoadMsu(string msuPath, MsuType? msuTypeFilter = null, bool saveToCache = true, bool ignoreCache = false, bool forceLoad = false) + private Dictionary _yamlFiles = []; + + public Msu? LoadMsu(string msuPath, MsuType? msuTypeFilter = null, bool saveToCache = true, bool ignoreCache = false, bool forceLoad = false, string parentDirectory = "") { var directory = new FileInfo(msuPath).Directory!.FullName; @@ -112,7 +122,7 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< { return null; } - + var msuDetails = _msuDetailsService.GetMsuDetails(msuPath, out var yamlHash, out var yamlError); if (!string.IsNullOrEmpty(yamlError)) { @@ -121,6 +131,17 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< var baseName = Path.GetFileName(msuPath).Replace(".msu", "", StringComparison.OrdinalIgnoreCase); var pcmFiles = Directory.EnumerateFiles(directory, $"{baseName}-*.pcm", SearchOption.AllDirectories).ToList(); + var msuDetailMismatch = false; + + if (msuDetails?.Tracks != null && pcmFiles.Count > 0) + { + var msuDetailSongCount = msuDetails.Tracks.Values.Sum(x => 1 + (x.Alts?.Count ?? 0)) * 0.75; + if (pcmFiles.Count < msuDetailSongCount) + { + yamlHash = ""; + msuDetailMismatch = true; + } + } // Load the MSU from cache if possible if (!ignoreCache) @@ -150,7 +171,7 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< Msu msu; // If it's an unknown MSU type, simply load the details as is - if (msuType == null) + if (msuType == null || msuDetailMismatch) { msu = LoadUnknownMsu(msuPath, directory, baseName, pcmFiles); } @@ -172,6 +193,17 @@ public IReadOnlyCollection LookupMsus(string? defaultDirectory, Dictionary< msu.Album = msuDetails?.Album; msu.Url = msuDetails?.Url; + if (string.IsNullOrEmpty(parentDirectory)) + { + var directoryInfo = new DirectoryInfo(directory); + msu.ParentMsuTypeDirectory = directoryInfo.Parent?.FullName ?? directory; + } + else + { + msu.ParentMsuTypeDirectory = parentDirectory; + } + + _msuCacheService.Put(msu, yamlHash, pcmFiles, saveToCache); return msu; @@ -534,7 +566,7 @@ public void RefreshMsu(Msu msu) _errors.Clear(); // Reload the MSU - var newMsu = LoadMsu(msu.Path); + var newMsu = LoadMsu(msu.Path, parentDirectory: msu.ParentMsuTypeDirectory); if (newMsu == null) { _logger.LogInformation("Reloaded MSU for Path {Path} returned null", msu.Path); diff --git a/MSURandomizerLibrary/Services/MsuUserOptionsService.cs b/MSURandomizerLibrary/Services/MsuUserOptionsService.cs index 010af6e..03d1c70 100644 --- a/MSURandomizerLibrary/Services/MsuUserOptionsService.cs +++ b/MSURandomizerLibrary/Services/MsuUserOptionsService.cs @@ -45,16 +45,42 @@ public MsuUserOptions Initialize(string settingsFilePath) .Build(); var yaml = File.ReadAllText(_settingsFilePath); _options = deserializer.Deserialize(yaml); - - foreach (var msuTypeDirectory in _options.MsuTypeNamePaths) + + // If there are no MSU directories, see if there are old settings that need to be updated + if (_options.MsuDirectories.Count == 0) { - var msuType = _msuTypeService.MsuTypes.FirstOrDefault(x => x.DisplayName == msuTypeDirectory.Key); - if (msuType != null) + var addedPaths = new List(); + if (!string.IsNullOrEmpty(_options.DefaultMsuPath) && Directory.Exists(_options.DefaultMsuPath)) { - _options.MsuTypePaths[msuType] = msuTypeDirectory.Value; + var smz3Type = _msuTypeService.GetSMZ3MsuType(); + if (smz3Type != null) + { + _options.MsuDirectories.Add(_options.DefaultMsuPath, smz3Type.DisplayName); + addedPaths.Add(_options.DefaultMsuPath); + } + } + + foreach (var prevSetting in _options.MsuTypeNamePaths) + { + if (addedPaths.Contains(prevSetting.Value)) + { + continue; + } + + _options.MsuDirectories.Add(prevSetting.Value, prevSetting.Key); + addedPaths.Add(prevSetting.Value); } - } + if (addedPaths.Count > 0) + { + _options.DefaultMsuPath = null; + _options.MsuTypeNamePaths = []; + _options.MsuTypePaths = []; + Save(); + } + + } + return _options; } diff --git a/MSURandomizerLibraryTests/MsuEndtoEndTests.cs b/MSURandomizerLibraryTests/MsuEndtoEndTests.cs index df7b123..ae85ca0 100644 --- a/MSURandomizerLibraryTests/MsuEndtoEndTests.cs +++ b/MSURandomizerLibraryTests/MsuEndtoEndTests.cs @@ -43,7 +43,10 @@ public void ShuffledMsuTest() TestHelpers.DeleteFolder(TestHelpers.MsuTestFolder); CreateMsus(msuType!, msuCount, altCount, addSongInfo); - var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder); + var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder, new Dictionary() + { + { TestHelpers.MsuTestFolder, msuType.DisplayName } + }); VerifyMsus(msus, msuCount, altCount, addSongInfo); var response = _msuSelectorService.CreateShuffledMsu(new MsuSelectorRequest() @@ -69,7 +72,10 @@ public void SingleShuffledMsuTest() TestHelpers.DeleteFolder(TestHelpers.MsuTestFolder); CreateMsus(msuType!, msuCount, altCount, addSongInfo); - var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder); + var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder, new Dictionary() + { + { TestHelpers.MsuTestFolder, msuType.DisplayName } + }); VerifyMsus(msus, msuCount, altCount, addSongInfo); var response = _msuSelectorService.CreateShuffledMsu(new MsuSelectorRequest() @@ -81,7 +87,7 @@ public void SingleShuffledMsuTest() VerifyResponse(response, msuType!, addSongInfo, true, false, false, false); } - + [Test] public void AssignMsuTest() { @@ -91,12 +97,17 @@ public void AssignMsuTest() var msuCount = 1; var altCount = 2; var addSongInfo = true; - + TestHelpers.DeleteFolder(TestHelpers.MsuTestFolder); CreateMsus(msuType!, msuCount, altCount, addSongInfo); - var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder); - VerifyMsus(msus, msuCount, altCount, addSongInfo); + var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder, + new Dictionary() + { + { TestHelpers.MsuTestFolder, msuType.DisplayName } + }); + + VerifyMsus(msus, msuCount, altCount, addSongInfo); var response = _msuSelectorService.AssignMsu(new MsuSelectorRequest() { @@ -121,7 +132,10 @@ public void RandomMsuTest() TestHelpers.DeleteFolder(TestHelpers.MsuTestFolder); CreateMsus(msuType!, msuCount, altCount, addSongInfo); - var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder); + var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder, new Dictionary() + { + { TestHelpers.MsuTestFolder, msuType.DisplayName } + }); VerifyMsus(msus, msuCount, altCount, addSongInfo); var response = _msuSelectorService.PickRandomMsu(new MsuSelectorRequest() @@ -147,7 +161,10 @@ public void SaveMsuTest() TestHelpers.DeleteFolder(TestHelpers.MsuTestFolder); CreateMsus(msuType!, msuCount, altCount, addSongInfo); - var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder); + var msus = _msuLookupService.LookupMsus(TestHelpers.MsuTestFolder, new Dictionary() + { + { TestHelpers.MsuTestFolder, msuType.DisplayName } + }); VerifyMsus(msus, msuCount, altCount, addSongInfo); var response = _msuSelectorService.SaveMsu(new MsuSelectorRequest() diff --git a/MSURandomizerLibraryTests/MsuLookupServiceTests.cs b/MSURandomizerLibraryTests/MsuLookupServiceTests.cs index e959c7a..2bc9697 100644 --- a/MSURandomizerLibraryTests/MsuLookupServiceTests.cs +++ b/MSURandomizerLibraryTests/MsuLookupServiceTests.cs @@ -28,7 +28,10 @@ public void LookupMsusTest() var folder = new FileInfo(msuPath).Directory?.Parent?.FullName; var service = CreateMsuLookupService(tracks); - var msus = service.LookupMsus(folder); + var msus = service.LookupMsus(folder, new Dictionary() + { + { TestHelpers.MsuTestFolder, TestHelpers.TestMsuTypeName } + }); Assert.That(service.Msus.Count, Is.EqualTo(2)); Assert.That(service.Msus.Any(x => x.Name == "test-msu-1")); @@ -47,7 +50,10 @@ public void RefreshMsuTest() var folder = new FileInfo(msuPath).Directory?.Parent?.FullName; var service = CreateMsuLookupService(tracks); - service.LookupMsus(folder); + service.LookupMsus(folder, new Dictionary() + { + { TestHelpers.MsuTestFolder, TestHelpers.TestMsuTypeName } + }); Assert.That(service.Msus.Count, Is.EqualTo(1)); Assert.That(service.Msus.First().Tracks.Count, Is.EqualTo(5)); @@ -55,7 +61,10 @@ public void RefreshMsuTest() tracks = new List<(int, int)>() { (1, 6) }; TestHelpers.CreateMsu(tracks, "test-msu-1"); - service.LookupMsus(folder); + service.LookupMsus(folder, new Dictionary() + { + { TestHelpers.MsuTestFolder, TestHelpers.TestMsuTypeName } + }); Assert.That(service.Msus.Count, Is.EqualTo(1)); Assert.That(service.Msus.First().Tracks.Count, Is.EqualTo(6)); } @@ -71,7 +80,10 @@ public void GetMsusByPathTest() var folder = new FileInfo(path1).Directory?.Parent?.FullName; var service = CreateMsuLookupService(tracks); - service.LookupMsus(folder); + service.LookupMsus(folder, new Dictionary() + { + { TestHelpers.MsuTestFolder, TestHelpers.TestMsuTypeName } + }); Assert.That(service.Msus.Count, Is.EqualTo(3)); var msus = service.GetMsusByPath(new List() { path1, path2 }); @@ -91,7 +103,10 @@ public void GetMsuByPathTest() var folder = new FileInfo(path1).Directory?.Parent?.FullName; var service = CreateMsuLookupService(tracks); - service.LookupMsus(folder); + service.LookupMsus(folder, new Dictionary() + { + { TestHelpers.MsuTestFolder, TestHelpers.TestMsuTypeName } + }); Assert.That(service.Msus.Count, Is.EqualTo(2)); var msu = service.GetMsuByPath(path1); diff --git a/MSURandomizerLibraryTests/MsuSelectorServiceTests.cs b/MSURandomizerLibraryTests/MsuSelectorServiceTests.cs index c517a0c..5991d5e 100644 --- a/MSURandomizerLibraryTests/MsuSelectorServiceTests.cs +++ b/MSURandomizerLibraryTests/MsuSelectorServiceTests.cs @@ -749,7 +749,10 @@ private MsuSelectorService CreateMsuSelectorService(List> msuTy index++; } var lookupService = new MsuLookupService(lookupLogger, msuTypeService, msuDetailsService, new MsuAppSettings(), msuCacheService, msuUserOptionsService); - lookupService.LookupMsus(folder); + lookupService.LookupMsus(folder, new Dictionary() + { + { TestHelpers.MsuTestFolder, TestHelpers.TestMsuTypeName } + }); msus = lookupService.Msus.ToList(); diff --git a/MSURandomizerLibraryTests/MsuUserOptionsServiceTests.cs b/MSURandomizerLibraryTests/MsuUserOptionsServiceTests.cs index 527ef57..558a3ab 100644 --- a/MSURandomizerLibraryTests/MsuUserOptionsServiceTests.cs +++ b/MSURandomizerLibraryTests/MsuUserOptionsServiceTests.cs @@ -53,9 +53,9 @@ public void SaveTest() Assert.That(options.Name, Is.Not.EqualTo(TestOptionName)); options.Name = TestOptionName; - options.MsuTypeNamePaths = new Dictionary() + options.MsuDirectories = new Dictionary() { - { testMsuType.Name, TestMsuTypeValue } + { TestMsuTypeValue, testMsuType.Name } }; msuUserOptionsService.Save(); @@ -64,8 +64,8 @@ public void SaveTest() msuUserOptionsService.Initialize(file.FullName); Assert.That(msuUserOptionsService.MsuUserOptions.Name, Is.EqualTo(TestOptionName), "Reloaded MSU User Options name is invalid"); - Assert.That(msuUserOptionsService.MsuUserOptions.MsuTypePaths.Keys.Contains(testMsuType), Is.True); - Assert.That(msuUserOptionsService.MsuUserOptions.MsuTypePaths[testMsuType], Is.EqualTo(TestMsuTypeValue)); + Assert.That(msuUserOptionsService.MsuUserOptions.MsuDirectories.Keys.Contains(TestMsuTypeValue), Is.True); + Assert.That(msuUserOptionsService.MsuUserOptions.MsuDirectories[TestMsuTypeValue], Is.EqualTo(testMsuType.Name )); file.Delete(); } diff --git a/MSURandomizerLibraryTests/TestHelpers.cs b/MSURandomizerLibraryTests/TestHelpers.cs index 65a1e50..1e03d26 100644 --- a/MSURandomizerLibraryTests/TestHelpers.cs +++ b/MSURandomizerLibraryTests/TestHelpers.cs @@ -10,6 +10,8 @@ namespace MSURandomizerLibraryTests; public abstract class TestHelpers { + public static readonly string TestMsuTypeName = "Test MSU Type"; + public static IMsuAppSettingsService CreateMsuAppSettingsService(MsuAppSettings? settings = null) { settings ??= new MsuAppSettings(); @@ -58,8 +60,8 @@ public static IMsuTypeService CreateMockMsuTypeService(List<(int, int)> tracks, var msuType = new MsuType() { - Name = "Test MSU Type", - DisplayName = "Test MSU Type", + Name = TestMsuTypeName, + DisplayName = TestMsuTypeName, RequiredTrackNumbers = trackNumbers.ToHashSet(), ValidTrackNumbers = trackNumbers.ToHashSet(), Tracks = trackNumbers.Select(x => new MsuTypeTrack() @@ -190,7 +192,7 @@ public static string CreateMsu(List<(int, int)> tracks, string msuName = "test-m { return CreateMsu(GetTracksFromRanges(tracks), msuName, deleteOld, createAlts, specialTracks); } - + public static IMsuDetailsService CreateMockMsuDetailsService(MsuDetails? returnMsuDetails, Msu? returnMsu) { var msuDetailsService = new Mock();