From 45b9687a0442bbc83338676e5c73cb6e30464c3d Mon Sep 17 00:00:00 2001 From: MattEqualsCoder Date: Sun, 20 Aug 2023 23:16:23 -0400 Subject: [PATCH] Initial MSU Randomization Integration --- src/Randomizer.App/App.xaml.cs | 29 + .../Controls/FileSystemInput.xaml.cs | 5 + src/Randomizer.App/MsuGeneratorService.cs | 179 ++++--- src/Randomizer.App/Randomizer.App.csproj | 7 +- src/Randomizer.App/RomGenerator.cs | 6 +- .../Windows/GenerateRomWindow.xaml | 41 +- .../Windows/GenerateRomWindow.xaml.cs | 107 +++- src/Randomizer.App/Windows/OptionsWindow.xaml | 8 + .../Windows/OptionsWindow.xaml.cs | 20 + .../Windows/TrackerWindow.xaml.cs | 18 +- .../msu-randomizer-settings.yml | 10 + src/Randomizer.App/msu-randomizer-types.json | 502 ++++++++++++++++++ .../Configuration/ConfigFiles/MsuConfig.cs | 140 +++++ .../Configuration/ConfigProvider.cs | 8 + .../ConfigServiceCollectionExtensions.cs | 6 + src/Randomizer.Data/Configuration/Configs.cs | 6 + .../Configuration/Yaml/README.md | 1 + src/Randomizer.Data/Options/GeneralOptions.cs | 2 + src/Randomizer.Data/Options/PatchOptions.cs | 49 +- .../Options/RandomizerOptions.cs | 1 + src/Randomizer.Data/Randomizer.Data.csproj | 1 + .../Randomizer.Multiplayer.Server.csproj | 2 +- .../ZeldaStateChecks/SpeckyClip.cs | 4 +- .../Randomizer.SMZ3.Tracking.csproj | 1 + src/Randomizer.SMZ3.Tracking/Tracker.cs | 9 +- .../VoiceCommands/MsuModule.cs | 228 ++++++++ .../20230819021736_MsuPaths.Designer.cs | 490 +++++++++++++++++ .../Migrations/20230819021736_MsuPaths.cs | 28 + ...19130906_MsuRandomizationStyle.Designer.cs | 493 +++++++++++++++++ .../20230819130906_MsuRandomizationStyle.cs | 28 + .../RandomizerContextModelSnapshot.cs | 11 +- src/Randomizer.Shared/Models/GeneratedRom.cs | 3 + .../Randomizer.Shared.csproj | 1 + src/Randomizer.Tools/RomGenerator.cs | 4 +- 34 files changed, 2344 insertions(+), 104 deletions(-) create mode 100644 src/Randomizer.App/msu-randomizer-settings.yml create mode 100644 src/Randomizer.App/msu-randomizer-types.json create mode 100644 src/Randomizer.Data/Configuration/ConfigFiles/MsuConfig.cs create mode 100644 src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs create mode 100644 src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.Designer.cs create mode 100644 src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.cs create mode 100644 src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.Designer.cs create mode 100644 src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.cs diff --git a/src/Randomizer.App/App.xaml.cs b/src/Randomizer.App/App.xaml.cs index e86a7d10c..eecf7e89a 100644 --- a/src/Randomizer.App/App.xaml.cs +++ b/src/Randomizer.App/App.xaml.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Reflection; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -9,6 +11,10 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Win32; +using MSURandomizerLibrary; +using MSURandomizerLibrary.Models; +using MSURandomizerLibrary.Services; +using MSURandomizerUI; using Randomizer.App.Controls; using Randomizer.App.Windows; using Randomizer.Data.Configuration; @@ -131,6 +137,9 @@ protected static void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddWindows(); + // MSU Randomzer + services.AddMsuRandomizerServices(); + services.AddMsuRandomizerUIServices(); } private void Application_Startup(object sender, StartupEventArgs e) @@ -155,6 +164,8 @@ private void Application_Startup(object sender, StartupEventArgs e) ToolTipService.ShowDurationProperty.OverrideMetadata(typeof(DependencyObject), new FrameworkPropertyMetadata(int.MaxValue)); + InitializeMsuRandomizer(); + var mainWindow = _host.Services.GetRequiredService(); mainWindow.Show(); } @@ -193,5 +204,23 @@ private void Application_DispatcherUnhandledException(object sender, System.Wind e.Handled = true; Environment.FailFast("Uncaught exception in Dispatcher", e.Exception); } + + private void InitializeMsuRandomizer() + { + var settingsStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("Randomizer.App.msu-randomizer-settings.yml"); + var typesStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("Randomizer.App.msu-randomizer-types.json"); + var msuInitializationRequest = new MsuRandomizerInitializationRequest() + { + MsuAppSettingsStream = settingsStream, + MsuTypeConfigStream = typesStream, + LookupMsus = false + }; +#if DEBUG + msuInitializationRequest.UserOptionsPath = "%LocalAppData%\\SMZ3CasRandomizer\\msu-user-settings-debug.yml"; +#endif + _host!.Services.GetRequiredService().Initialize(msuInitializationRequest); + } } } diff --git a/src/Randomizer.App/Controls/FileSystemInput.xaml.cs b/src/Randomizer.App/Controls/FileSystemInput.xaml.cs index 4c3a50b67..a79d2a666 100644 --- a/src/Randomizer.App/Controls/FileSystemInput.xaml.cs +++ b/src/Randomizer.App/Controls/FileSystemInput.xaml.cs @@ -99,6 +99,7 @@ private void BrowseFile() if (string.IsNullOrEmpty(FileValidationHash) || string.IsNullOrEmpty(FileValidationErrorMessage)) { Path = dialog.FileName; + OnPathUpdated?.Invoke(this, EventArgs.Empty); } else { @@ -115,6 +116,7 @@ private void BrowseFile() } Path = dialog.FileName; + OnPathUpdated?.Invoke(this, EventArgs.Empty); } } } @@ -133,8 +135,11 @@ private void BrowseFolder() if (dialog.ShowDialog(owner) == CommonFileDialogResult.Ok) { Path = dialog.FileName; + OnPathUpdated?.Invoke(this, EventArgs.Empty); } } + public event EventHandler? OnPathUpdated; + } } diff --git a/src/Randomizer.App/MsuGeneratorService.cs b/src/Randomizer.App/MsuGeneratorService.cs index 62bd6b5e1..2b57ab96b 100644 --- a/src/Randomizer.App/MsuGeneratorService.cs +++ b/src/Randomizer.App/MsuGeneratorService.cs @@ -1,112 +1,137 @@ using System; using System.IO; using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using Microsoft.WindowsAPICodePack.Dialogs; +using MSURandomizerLibrary; +using MSURandomizerLibrary.Models; +using MSURandomizerLibrary.Services; +using MSURandomizerUI; +using Randomizer.Data.Options; namespace Randomizer.App; public class MsuGeneratorService { - /// - /// Enables MSU support for a rom - /// - /// The path to the msu file - /// The path to the rom file - /// Any error that was ran into when updating the rom - /// True if successful, false otherwise - public bool EnableMsu1Support(string msuPath, string romPath, out string error) + private readonly IMsuLookupService _msuLookupService; + private readonly IMsuUiFactory _msuUiFactory; + private readonly RandomizerOptions _options; + private readonly IMsuSelectorService _msuSelectorService; + private readonly IMsuTypeService _msuTypeService; + + public MsuGeneratorService(OptionsFactory optionsFactory, IMsuLookupService msuLookupService, IMsuUiFactory msuUiFactory, IMsuSelectorService msuSelectorService, IMsuTypeService msuTypeService) { - if (!File.Exists(msuPath)) + _options = optionsFactory.Create(); + _msuLookupService = msuLookupService; + _msuUiFactory = msuUiFactory; + _msuSelectorService = msuSelectorService; + _msuTypeService = msuTypeService; + } + + public void OpenMsuWindow(Window parentWindow, SelectionMode selectionMode, MsuRandomizationStyle? randomizationStyle) + { + if (!VerifyMsuDirectory(parentWindow)) return; + if (!_msuUiFactory.OpenMsuWindow(selectionMode, true, out var options)) return; + if (options.SelectedMsus?.Any() != true) return; + _options.PatchOptions.MsuPaths = options.SelectedMsus.ToList(); + _options.PatchOptions.MsuRandomizationStyle = randomizationStyle; + } + + public bool LookupMsus() + { + if (!string.IsNullOrEmpty(_options.GeneralOptions.MsuPath) && Directory.Exists(_options.GeneralOptions.MsuPath)) { - error = ""; - return false; + Task.Run(() => + { + _msuLookupService.LookupMsus(_options.GeneralOptions.MsuPath); + }); + + return true; } - var romDrive = Path.GetPathRoot(romPath); - var msuDrive = Path.GetPathRoot(msuPath); - if (romDrive?.Equals(msuDrive, StringComparison.OrdinalIgnoreCase) == false) + return false; + } + + private bool VerifyMsuDirectory(Window parentWindow) + { + if (!string.IsNullOrEmpty(_options.GeneralOptions.MsuPath) && Directory.Exists(_options.GeneralOptions.MsuPath)) { - error = $"Due to technical limitations, the MSU-1 " + - $"pack and the ROM need to be on the same drive. MSU-1 " + - $"support cannot be enabled.\n\nPlease move or copy the MSU-1 " + - $"files to somewhere on {romDrive}, or change the ROM output " + - $"folder setting to be on the {msuDrive} drive."; - return false; + return true; } - var romFolder = Path.GetDirectoryName(romPath); - var msuFolder = Path.GetDirectoryName(msuPath); - var romBaseName = Path.GetFileNameWithoutExtension(romPath); - var msuBaseName = Path.GetFileNameWithoutExtension(msuPath); - - var swap = false; + MessageBox.Show(parentWindow, "Please select the parent folder than contains all of your MSUs. To preserve drive space, it is recommended that the Rom Output and MSU folders be on the same drive.", "MSU Path Needed", + MessageBoxButton.OK, MessageBoxImage.Exclamation); - var hasTrack41 = File.Exists(msuPath.Replace(".msu", "-41.pcm")); - var hasTrack141 = File.Exists(msuPath.Replace(".msu", "-141.pcm")); - var hasTrack33 = File.Exists(msuPath.Replace(".msu", "-33.pcm")); - var hasTrack133 = File.Exists(msuPath.Replace(".msu", "-133.pcm")); + using var dialog = new CommonOpenFileDialog(); + dialog.EnsurePathExists = true; + dialog.Title = "Select MSU Path"; + dialog.IsFolderPicker = true; - // Swap if we see the Skull Woods theme above 100 or if we see that the Zelda epilogue track number - // only exists above 100 - if (hasTrack141 || (hasTrack133 && !hasTrack33)) + if (dialog.ShowDialog(parentWindow) == CommonFileDialogResult.Ok) { - swap = true; + _options.GeneralOptions.MsuPath = dialog.FileName; } - // If we don't see Skull Woods below 100 and we don't see the Zelda epilogue track only existing below 100 - // then try to check loop tracks as a last ditch effort to see if we need to swap - else if (!hasTrack41 && !(hasTrack33 && !hasTrack133)) + + if (string.IsNullOrEmpty(_options.GeneralOptions.MsuPath) || + !Directory.Exists(_options.GeneralOptions.MsuPath)) { - var track33Loops = DoesPCMLoop(msuPath.Replace(".msu", "-33.pcm")); - var track130Loops = DoesPCMLoop(msuPath.Replace(".msu", "-130.pcm")); - if (track33Loops || track130Loops) - { - swap = true; - } + return false; } - // Copy pcm files - foreach (var msuFile in Directory.EnumerateFiles(msuFolder!, $"{msuBaseName}*")) + Task.Run(() => { - var fileName = Path.GetFileName(msuFile); - var suffix = fileName.Replace(msuBaseName, ""); - var link = Path.Combine(romFolder!, romBaseName + suffix); + _msuLookupService.LookupMsus(_options.GeneralOptions.MsuPath); + }); - // Swap SM and Z3 tracks if needed - if (swap && suffix.Contains(".pcm")) - { - if (int.TryParse(suffix.Replace("-", "").Replace(".pcm", ""), out var trackNumber) && trackNumber > 0) - { - // Skip track 99 (SMZ3 combined credits) as it doesn't change - if (trackNumber < 99) - { - trackNumber += 100; - } - else if (trackNumber > 99) - { - trackNumber -= 100; - } - - var oldLink = link; - link = Path.Combine(romFolder!, romBaseName + "-" + trackNumber + ".pcm"); - } - } - NativeMethods.CreateHardLink(link, msuFile, IntPtr.Zero); - } + MessageBox.Show(parentWindow, "Updated MSU folder. If you want to change the MSU path in the future, you can do so in the Tools -> Options window", "MSU Path Updated", + MessageBoxButton.OK, MessageBoxImage.Information); + + LookupMsus(); - error = ""; return true; } - private static bool DoesPCMLoop(string fileName) + public bool ApplyMsuOptions(string romPath) { - if (!File.Exists(fileName)) + if (!_options.PatchOptions.MsuPaths.Any()) { return false; } - using var fileStream = File.OpenRead(fileName); - fileStream.Seek(4, SeekOrigin.Begin); - var bytes = new byte[4]; - fileStream.Read(bytes, 0, 4); - return bytes.Any(x => x != 0); + + var romFileInfo = new FileInfo(romPath); + var outputPath = romFileInfo.FullName.Replace(romFileInfo.Extension, ".msu"); + + if (_options.PatchOptions.MsuRandomizationStyle == null) + { + _msuSelectorService.AssignMsu(new MsuSelectorRequest() + { + MsuPath = _options.PatchOptions.MsuPaths.First(), + OutputMsuType = _msuTypeService.GetSMZ3MsuType(), + OutputPath = outputPath, + }); + } + else if (_options.PatchOptions.MsuRandomizationStyle == MsuRandomizationStyle.Single) + { + _msuSelectorService.PickRandomMsu(new MsuSelectorRequest() + { + MsuPaths = _options.PatchOptions.MsuPaths, + OutputMsuType = _msuTypeService.GetSMZ3MsuType(), + OutputPath = outputPath, + }); + } + else + { + _msuSelectorService.CreateShuffledMsu(new MsuSelectorRequest() + { + MsuPaths = _options.PatchOptions.MsuPaths, + OutputMsuType = _msuTypeService.GetSMZ3MsuType(), + OutputPath = outputPath, + }); + } + + return true; } } diff --git a/src/Randomizer.App/Randomizer.App.csproj b/src/Randomizer.App/Randomizer.App.csproj index 2296eb8aa..5d54dd671 100644 --- a/src/Randomizer.App/Randomizer.App.csproj +++ b/src/Randomizer.App/Randomizer.App.csproj @@ -5,7 +5,7 @@ net7.0-windows true chozo20.ico - 9.4.0-beta.1 + 9.4.0 SMZ3 Cas' Randomizer SMZ3 Cas' Randomizer Vivelin @@ -68,10 +68,15 @@ + + + + + diff --git a/src/Randomizer.App/RomGenerator.cs b/src/Randomizer.App/RomGenerator.cs index da62f7e3d..cdbb63088 100644 --- a/src/Randomizer.App/RomGenerator.cs +++ b/src/Randomizer.App/RomGenerator.cs @@ -170,7 +170,7 @@ private async Task GenerateRomInternalAsync(SeedData seed, R var fileSuffix = $"{DateTimeOffset.Now:yyyyMMdd-HHmmss}_{safeSeed}"; var romFileName = $"SMZ3_Cas_{fileSuffix}.sfc"; var romPath = Path.Combine(folderPath, romFileName); - _msuGeneratorService.EnableMsu1Support(options.PatchOptions.Msu1Path, romPath, out var msuError); + _msuGeneratorService.ApplyMsuOptions(romPath); Rom.UpdateChecksum(bytes); await File.WriteAllBytesAsync(romPath, bytes); @@ -194,7 +194,7 @@ private async Task GenerateRomInternalAsync(SeedData seed, R return new GenerateRomResults() { Rom = rom, - MsuError = msuError + MsuError = "" }; } @@ -426,6 +426,8 @@ protected async Task SaveSeedToDatabaseAsync(RandomizerOptions opt Settings = settingsString, GeneratorVersion = Smz3Randomizer.Version.Major, MultiplayerGameDetails = multiplayerGameDetails, + MsuRandomizationStyle = options.PatchOptions.MsuRandomizationStyle, + MsuPaths = string.Join("|", options.PatchOptions.MsuPaths) }; _dbContext.GeneratedRoms.Add(rom); diff --git a/src/Randomizer.App/Windows/GenerateRomWindow.xaml b/src/Randomizer.App/Windows/GenerateRomWindow.xaml index 3dec50f11..009b6ea1a 100644 --- a/src/Randomizer.App/Windows/GenerateRomWindow.xaml +++ b/src/Randomizer.App/Windows/GenerateRomWindow.xaml @@ -10,6 +10,7 @@ xmlns:options="clr-namespace:Randomizer.Data.Options;assembly=Randomizer.Data" xmlns:windows="clr-namespace:Randomizer.App.Windows" mc:Ignorable="d" + Loaded="GenerateRomWindow_OnLoaded" Closing="Window_Closing" Title="SMZ3 Cas' Randomizer" Width="{Binding WindowWidth, Mode=TwoWay}" @@ -307,16 +308,37 @@ - - + + + + + + + + + + + - - @@ -342,6 +364,7 @@ diff --git a/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs b/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs index 7bd459162..8be01faee 100644 --- a/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs +++ b/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs @@ -16,6 +16,8 @@ using System.Windows.Controls; using System.Windows.Threading; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Win32; +using MSURandomizerLibrary; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.Data.Options; using Randomizer.Data.Services; @@ -37,17 +39,20 @@ public partial class GenerateRomWindow : Window private readonly RomGenerator _romGenerator; private readonly LocationConfig _locations; private readonly IMetadataService _metadataService; + private readonly MsuGeneratorService _msuGeneratorService; private RandomizerOptions? _options; public GenerateRomWindow(IServiceProvider serviceProvider, RomGenerator romGenerator, LocationConfig locations, - IMetadataService metadataService) + IMetadataService metadataService, + MsuGeneratorService msuGeneratorService) { _serviceProvider = serviceProvider; _romGenerator = romGenerator; _locations = locations; _metadataService = metadataService; + _msuGeneratorService = msuGeneratorService; InitializeComponent(); SamusSprites.Add(Sprite.DefaultSamus); @@ -726,5 +731,105 @@ private void SubmitConfigsButton_OnClick(object sender, RoutedEventArgs e) DialogResult = true; Close(); } + + private void SelectMsuButton_OnClick(object sender, RoutedEventArgs e) + { + _msuGeneratorService.OpenMsuWindow(this, SelectionMode.Single, null); + UpdateMsuTextBox(); + } + + private void MsuOptionsButton_OnClick(object sender, RoutedEventArgs e) + { + if (MsuOptionsButton.ContextMenu == null) return; + MsuOptionsButton.ContextMenu.IsOpen = true; + } + + private void RandomMsuMenuItem_OnClick(object sender, RoutedEventArgs e) + { + _msuGeneratorService.OpenMsuWindow(this, SelectionMode.Multiple, MsuRandomizationStyle.Single); + UpdateMsuTextBox(); + } + + private void ShuffledMsuMenuItem_OnClick(object sender, RoutedEventArgs e) + { + _msuGeneratorService.OpenMsuWindow(this, SelectionMode.Multiple, MsuRandomizationStyle.Shuffled); + UpdateMsuTextBox(); + } + + private void ContinuousShuffleMsuMenuItem_OnClick(object sender, RoutedEventArgs e) + { + _msuGeneratorService.OpenMsuWindow(this, SelectionMode.Multiple, MsuRandomizationStyle.Continuous); + UpdateMsuTextBox(); + } + + private void UpdateMsuTextBox() + { + + if (Options.PatchOptions.MsuRandomizationStyle == null && Options.PatchOptions.MsuPaths.Any()) + { + if (!string.IsNullOrEmpty(Options.GeneralOptions.MsuPath)) + { + SelectedMsuTextBox.Text = Path.GetRelativePath(Options.GeneralOptions.MsuPath, Options.PatchOptions.MsuPaths.First()); + } + else + { + SelectedMsuTextBox.Text = Options.PatchOptions.MsuPaths.First(); + } + } + else if (Options.PatchOptions.MsuRandomizationStyle == MsuRandomizationStyle.Single) + { + SelectedMsuTextBox.Text = $"Random MSU from {Options.PatchOptions.MsuPaths.Count} MSUs"; + } + else if (Options.PatchOptions.MsuRandomizationStyle == MsuRandomizationStyle.Shuffled) + { + SelectedMsuTextBox.Text = $"Shuffled MSU from {Options.PatchOptions.MsuPaths.Count} MSUs"; + } + else if (Options.PatchOptions.MsuRandomizationStyle == MsuRandomizationStyle.Continuous) + { + SelectedMsuTextBox.Text = $"Continuously Shuffle {Options.PatchOptions.MsuPaths.Count} MSUs"; + } + else + { + SelectedMsuTextBox.Text = "None"; + } + } + + private void GenerateRomWindow_OnLoaded(object sender, RoutedEventArgs e) + { + if (!string.IsNullOrEmpty(Options.PatchOptions.Msu1Path) && !Options.PatchOptions.MsuPaths.Any() && + Options.PatchOptions.MsuRandomizationStyle == null && File.Exists(Options.PatchOptions.Msu1Path)) + { + Options.PatchOptions.MsuPaths.Add(Options.PatchOptions.Msu1Path); + Options.PatchOptions.Msu1Path = ""; + } + + UpdateMsuTextBox(); + _msuGeneratorService.LookupMsus(); + } + + private void SelectMsuFileMenuItem_OnClick(object sender, RoutedEventArgs e) + { + var dialog = new OpenFileDialog + { + CheckFileExists = true, + Filter = "MSU-1 files (*.msu)|*.msu|All files (*.*)|*.*", + Title = "Select MSU-1 file", + InitialDirectory = Directory.Exists(Options.GeneralOptions.MsuPath) ? Options.GeneralOptions.MsuPath : null + }; + + if (dialog.ShowDialog(this) == true && File.Exists(dialog.FileName)) + { + Options.PatchOptions.MsuPaths = new List() { dialog.FileName }; + Options.PatchOptions.MsuRandomizationStyle = null; + UpdateMsuTextBox(); + } + } + + private void VanillaMusicMenuItem_OnClick(object sender, RoutedEventArgs e) + { + Options.PatchOptions.MsuPaths = new List(); + Options.PatchOptions.MsuRandomizationStyle = null; + UpdateMsuTextBox(); + } } } diff --git a/src/Randomizer.App/Windows/OptionsWindow.xaml b/src/Randomizer.App/Windows/OptionsWindow.xaml index cf8a22851..040eba4c0 100644 --- a/src/Randomizer.App/Windows/OptionsWindow.xaml +++ b/src/Randomizer.App/Windows/OptionsWindow.xaml @@ -55,6 +55,14 @@ IsFolderPicker="True" DialogTitle="Select ROM output folder" /> + + + + diff --git a/src/Randomizer.App/Windows/OptionsWindow.xaml.cs b/src/Randomizer.App/Windows/OptionsWindow.xaml.cs index aa9fcc07e..82d63ff8d 100644 --- a/src/Randomizer.App/Windows/OptionsWindow.xaml.cs +++ b/src/Randomizer.App/Windows/OptionsWindow.xaml.cs @@ -256,5 +256,25 @@ private void UndoExpirationTimeTextBox_OnLostFocus(object sender, RoutedEventArg _ = TryParse(new string(UndoExpirationTimeTextBox.Text.Where(char.IsDigit).ToArray()), out var number); UndoExpirationTimeTextBox.Text = Math.Max(1, number).ToString(); } + + private void FileSystemInput_OnOnPathUpdated(object? sender, EventArgs e) + { + var outputPath = Options.RomOutputPath; + var msuPath = Options.MsuPath; + + if (string.IsNullOrEmpty(outputPath) || string.IsNullOrEmpty(msuPath)) + { + return; + } + + var outputDrive = new DriveInfo(outputPath); + var msuDrive = new DriveInfo(msuPath); + if (outputDrive.Name != msuDrive.Name) + { + MessageBox.Show(this, + "To preserve drive space, it is recommended that the Rom Output and MSU folders be on the same drive.", + "SMZ3 Cas' Randomizer", MessageBoxButton.OK, MessageBoxImage.Warning); + } + } } } diff --git a/src/Randomizer.App/Windows/TrackerWindow.xaml.cs b/src/Randomizer.App/Windows/TrackerWindow.xaml.cs index e7d4b1464..9603a25b1 100644 --- a/src/Randomizer.App/Windows/TrackerWindow.xaml.cs +++ b/src/Randomizer.App/Windows/TrackerWindow.xaml.cs @@ -15,6 +15,7 @@ using System.Windows.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using MSURandomizerLibrary.Services; using Randomizer.Data.Configuration.ConfigTypes; using Randomizer.Data.Options; using Randomizer.Data.WorldData; @@ -47,6 +48,7 @@ public partial class TrackerWindow : Window private readonly IWorldAccessor _world; private readonly RandomizerOptions _options; private readonly Smz3GeneratedRomLoader _romLoader; + private readonly IMsuLookupService _msuLookupService; private bool _pegWorldMode; private TrackerLocationsWindow? _locationsWindow; private TrackerHelpWindow? _trackerHelpWindow; @@ -68,8 +70,8 @@ public TrackerWindow(IServiceProvider serviceProvider, IUIService uiService, OptionsFactory optionsFactory, IWorldAccessor world, - ITrackerTimerService timerService - ) + ITrackerTimerService timerService, + IMsuLookupService msuLookupService) { InitializeComponent(); @@ -82,6 +84,7 @@ ITrackerTimerService timerService _layout = uiService.GetLayout(_options.GeneralOptions.SelectedLayout); _defaultLayout = _layout; _world = world; + _msuLookupService = msuLookupService; foreach (var layout in uiService.SelectableLayouts) { @@ -693,12 +696,18 @@ private void InitializeTracker() if (_options == null) throw new InvalidOperationException("Cannot initialize Tracker before assigning " + nameof(_options)); + Task.Run(() => + { + _msuLookupService.LookupMsus(_options.GeneralOptions.MsuPath); + }); + _tracker = _serviceProvider.GetRequiredService(); // If a rom was passed in with a valid tracker state, reload the state from the database if (GeneratedRom.IsValid(Rom)) { - Tracker.Load(Rom); + var romPath = Path.Combine(_options.RomOutputPath, Rom.RomPath); + Tracker.Load(Rom, romPath); } Tracker.SpeechRecognized += (sender, e) => Dispatcher.Invoke(() => @@ -971,7 +980,8 @@ private async void LoadSavedStateMenuItem_Click(object sender, RoutedEventArgs e // If there is a valid rom, then load the state from the db if (GeneratedRom.IsValid(Rom)) { - await Task.Run(() => Tracker.Load(Rom)); + var romPath = Path.Combine(_options.RomOutputPath, Rom.RomPath); + await Task.Run(() => Tracker.Load(Rom, romPath)); Tracker.StartTimer(true); if (_dispatcherTimer.IsEnabled) diff --git a/src/Randomizer.App/msu-randomizer-settings.yml b/src/Randomizer.App/msu-randomizer-settings.yml new file mode 100644 index 000000000..aec69138d --- /dev/null +++ b/src/Randomizer.App/msu-randomizer-settings.yml @@ -0,0 +1,10 @@ +UserOptionsFilePath: "%LocalAppData%\\SMZ3CasRandomizer\\msu-user-settings.yml" +MsuCachePath: "%LocalAppData%\\SMZ3CasRandomizer" +ContinuousReshuffleSeconds: 60 +MsuWindowDisplayRandomButton: false +MsuWindowDisplayShuffleButton: false +MsuWindowDisplayContinuousButton: false +MsuWindowDisplayOptionsButton: false +MsuWindowDisplaySelectButton: true +ForcedMsuType: "Super Metroid / A Link to the Past Combination Randomizer" +MsuWindowTitle: SMZ3 Cas' Randomizer diff --git a/src/Randomizer.App/msu-randomizer-types.json b/src/Randomizer.App/msu-randomizer-types.json new file mode 100644 index 000000000..05847e171 --- /dev/null +++ b/src/Randomizer.App/msu-randomizer-types.json @@ -0,0 +1,502 @@ +[ + { + "meta": { + "name": "The Legend of Zelda: A Link to the Past" + }, + "tracks": { + "basic": [ + { + "title": "Opening Theme", + "name": "Link to the Past", + "nonlooping": true + }, + { + "title": "Light World Overworld", + "name": "Hyrule Field" + }, + { + "title": "Rain State Overworld", + "name": "Time of Falling Rain" + }, + { + "title": "Bunny Overworld", + "name": "The Silly Pink Rabbit" + }, + { + "title": "Light World Lost Woods", + "name": "Forest of Mystery" + }, + { + "title": "Opening Story Credits", + "name": "Seal of Seven Maidens" + }, + { + "title": "Kakariko Village" + }, + { + "title": "Mirror Portal", + "name": "Dimensional Shift", + "nonlooping": true + }, + { + "title": "Dark World Overworld", + "name": "Dark Golden Land" + }, + { + "title": "Pedestal Pull Sequence", + "name": "Unsealing the Master Sword", + "nonlooping": true + }, + { + "title": "File Select/Game Over", + "name": "Beginning of the Journey" + }, + { + "title": "Kakariko Guards Summoned", + "name": "Soldiers of Kakariko Village" + }, + { + "title": "Dark Death Mountain", + "name": "Black Mist" + }, + { + "title": "Money Making Game" + }, + { + "title": "Dark World Lost Woods", + "extended": true, + "remap": 13 + }, + { + "title": "Hyrule Castle", + "name": "Majestic Castle" + }, + { + "title": "Generic Light World Dungeon", + "name": "Lost Ancient Ruins" + }, + { + "title": "Cave", + "name": "Dank Dungeons" + }, + { + "title": "Boss Victory", + "name": "Great Victory!", + "nonlooping": true + }, + { + "title": "Sanctuary", + "name": "Safety in the Sanctuary" + }, + { + "title": "Generic Boss Battle", + "name": "Anger of the Guardians" + }, + { + "title": "Generic Dark World Dungeon", + "name": "Dungeon of Shadows" + }, + { + "title": "Shop/Fortune Teller" + }, + { + "title": "Cave 2" + }, + { + "title": "Princess Zelda's Rescue" + }, + { + "title": "Crystal Get", + "name": "Meeting the Maidens" + }, + { + "title": "Fairy Fountain", + "name": "The Goddess Appears" + }, + { + "title": "Agahnim's Theme", + "name": "Priest of the Dark Order" + }, + { + "title": "Ganon Appears", + "name": "Release of Ganon", + "nonlooping": true + }, + { + "title": "Ganon's Theme", + "name": "Ganon's Message" + }, + { + "title": "Ganon Fight", + "name": "The Prince of Darkness" + }, + { + "title": "Triforce Room", + "name": "Power of the Gods" + }, + { + "title": "Triumphant Return", + "name": "Epilogue ~ Beautiful Hyrule", + "nonlooping": true + }, + { + "title": "Credits", + "name": "Staff Roll", + "nonlooping": true + } + ], + "extended": [ + { + "title": "Eastern Palace", + "fallback": 17 + }, + { + "title": "Desert Palace", + "fallback": 17 + }, + { + "title": "Agahnim's Tower", + "fallback": 16 + }, + { + "title": "Swamp Palace", + "fallback": 22 + }, + { + "title": "Palace of Darkness", + "fallback": 22 + }, + { + "title": "Misery Mire", + "fallback": 22 + }, + { + "title": "Skull Woods", + "fallback": 22 + }, + { + "title": "Ice Palace", + "fallback": 22 + }, + { + "title": "Tower of Hera", + "fallback": 17 + }, + { + "title": "Thieves' Town", + "fallback": 22 + }, + { + "title": "Turtle Rock", + "fallback": 22 + }, + { + "title": "Ganon's Tower", + "fallback": 22 + }, + { + "title": "Eastern Palace Boss", + "fallback": 21 + }, + { + "title": "Desert Palace Boss", + "fallback": 21 + }, + { + "title": "Agahnim's Tower Boss", + "fallback": 21 + }, + { + "title": "Swamp Palace Boss", + "fallback": 21 + }, + { + "title": "Palace of Darkness Boss", + "fallback": 21 + }, + { + "title": "Misery Mire Boss", + "fallback": 21 + }, + { + "title": "Skull Woods Boss", + "fallback": 21 + }, + { + "title": "Ice Palace Boss", + "fallback": 21 + }, + { + "title": "Tower of Hera Boss", + "fallback": 21 + }, + { + "title": "Thieves' Town Boss", + "fallback": 21 + }, + { + "title": "Turtle Rock Boss", + "fallback": 21 + }, + { + "title": "Ganon's Tower Boss", + "fallback": 21 + }, + { + "title": "Ganon's Tower 2", + "fallback": 22, + "remap": 46 + }, + { + "title": "Light World 2", + "fallback": 2 + }, + { + "title": "Dark World 2", + "fallback": 9 + } + ], + "longest": 50, + "src": "https://www.romhacking.net/hacks/2483/" + } + }, + { + "meta": { + "name": "Super Metroid" + }, + "tracks": { + "basic": [ + { + "title": "File Start", + "name": "Samus Aran's Appearance Fanfare", + "nonlooping": true + }, + { + "title": "Item Acquisition Fanfare", + "nonlooping": true + }, + { + "title": "Item room" + }, + { + "title": "Opening with intro", + "pair": 5 + }, + { + "title": "Opening without intro", + "pair": 4 + }, + { + "title": "Crateria (First Landing Thunder)", + "name": "Arrival on Crateria (with thunder FX)" + }, + { + "title": "Crateria (First Landing No Thunder)", + "name": "Arrival on Crateria (without thunder FX)" + }, + { + "title": "Crateria", + "name": "The Space Pirates Appear" + }, + { + "title": "Crateria Statue Room", + "name": "Statue Room" + }, + { + "title": "Samus' Ship", + "name": "Theme of Samus Aran" + }, + { + "title": "Brinstar with vegetation" + }, + { + "title": "Brinstar Red Soil" + }, + { + "title": "Upper Norfair" + }, + { + "title": "Lower Norfair" + }, + { + "title": "Upper Maridia" + }, + { + "title": "Lower Maridia" + }, + { + "title": "Tourian" + }, + { + "title": "Mother Brain Battle" + }, + { + "title": "Big Boss Battle 1" + }, + { + "title": "Evacuation" + }, + { + "title": "Mysterious Statue Chamber", + "name": "Chozo Statue Awakens" + }, + { + "title": "Big Boss Battle 2", + "pair": 23 + }, + { + "title": "Tension / Hostile Incoming", + "pair": 22 + }, + { + "title": "Plant Miniboss" + }, + { + "title": "Ceres Station" + }, + { + "title": "Wrecked Ship Power Off" + }, + { + "title": "Wrecked Ship Power On" + }, + { + "title": "Theme of Super Metroid" + }, + { + "title": "Death Cry", + "nonlooping": true + }, + { + "title": "Ending", + "nonlooping": true + } + ], + "extended": [ + { + "title": "Kraid Incoming", + "pair": 32, + "fallback": 23 + }, + { + "title": "Kraid Battle", + "pair": 31, + "fallback": 22 + }, + { + "title": "Phantoon Incoming", + "pair": 34, + "fallback": 23 + }, + { + "title": "Phantoon Battle", + "pair": 33, + "fallback": 22 + }, + { + "title": "Draygon Battle", + "fallback": 19 + }, + { + "title": "Ridley Battle", + "fallback": 19 + }, + { + "title": "Baby Incoming", + "pair": 38, + "fallback": 23 + }, + { + "title": "The Baby", + "pair": 37, + "fallback": 22 + }, + { + "title": "Hyper Beam", + "fallback": 10 + }, + { + "title": "Game Over" + } + ] + } + }, + { + "meta": { + "name": "Super Metroid / A Link to the Past Combination Randomizer" + }, + "tracks": { + "basic": [ + { + "num": 99, + "title": "SMZ3 Credits", + "nonlooping": true + } + ] + }, + "copy": [ + { + "msu": "The Legend of Zelda: A Link to the Past", + "modifier": 0, + "ignore": [ + 1, + 3, + 6, + 25, + 34 + ] + }, + { + "msu": "Super Metroid", + "modifier": 100, + "ignore": [ + 102, + 107, + 125, + 128 + ] + } + ] + }, + { + "meta": { + "name": "Super Metroid / A Link to the Past Combination Randomizer Legacy", + "selectable": false, + "exactmatches": [ + "Super Metroid / A Link to the Past Combination Randomizer" + ] + }, + "tracks": { + "basic": [ + { + "num": 99, + "title": "SMZ3 Credits", + "nonlooping": true + } + ] + }, + "copy": [ + { + "msu": "The Legend of Zelda: A Link to the Past", + "modifier": 100, + "ignore": [ + 101, + 103, + 106, + 125, + 134 + ] + }, + { + "msu": "Super Metroid", + "modifier": 0, + "ignore": [ + 2, + 7, + 25, + 28 + ] + } + ] + } +] diff --git a/src/Randomizer.Data/Configuration/ConfigFiles/MsuConfig.cs b/src/Randomizer.Data/Configuration/ConfigFiles/MsuConfig.cs new file mode 100644 index 000000000..e824d8d3a --- /dev/null +++ b/src/Randomizer.Data/Configuration/ConfigFiles/MsuConfig.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using Randomizer.Data.Configuration.ConfigTypes; + +namespace Randomizer.Data.Configuration.ConfigFiles; + +public class MsuConfig : IMergeable, IConfigFile +{ + /// + /// Names for various tracks when asking "what's the current song for" + /// + public Dictionary TrackLocations = new Dictionary() + { + { 2, new SchrodingersString("light world", "hyrule field") }, + { 4, new SchrodingersString("bunny") }, + { 5, new SchrodingersString("lost woods") }, + { 7, new SchrodingersString("kakariko", "kakariko village", "link's house") }, + { 8, new SchrodingersString("mirror") }, + { 9, new SchrodingersString("dark world") }, + { 10, new SchrodingersString("pedestal pull", "master sword pedestal", "pedestal") }, + { 11, new SchrodingersString("zelda game over") }, + { 12, new SchrodingersString("guards") }, + { 13, new SchrodingersString("dark death mountain") }, + { 14, new SchrodingersString("minigame") }, + { 15, new SchrodingersString("dark woods") }, + { 16, new SchrodingersString("hyrule castle") }, + { 17, new SchrodingersString("pendant dungeon") }, + { 18, new SchrodingersString("cave 1", "mini moldorm cave", "superbunny cave", "paradox cave", "spike cave", "spiral cave", "Kakariko well", "bumper cave", "hype cave", "link's uncle", "checkerboard cave", "hookshot cave", "Mire shed", "hammer pegs") }, + { 19, new SchrodingersString("boss victory", "Boss fanfare") }, + { 20, new SchrodingersString("sanctuary") }, + { 21, new SchrodingersString("zelda boss battle") }, + { 22, new SchrodingersString("crystal dungeon") }, + { 23, new SchrodingersString("shop") }, + { 24, new SchrodingersString("cave 2", "lost woods hideout", "king's tomb", "dam", "swamp ruins", "waterfall fairy", "magic bat", "graveyard ledge", "sahasrahla", "pyramid fair", "aginah", "cave 45", "cave number 45") }, + { 26, new SchrodingersString("crystal retrieved", "crystal get") }, + { 27, new SchrodingersString("fairy", "great fairy", "ice rod cave") }, + { 28, new SchrodingersString("agahnims floor") }, + { 29, new SchrodingersString("ganon reveal", "ganon bat") }, + { 30, new SchrodingersString("ganons message", "ganon intro") }, + { 31, new SchrodingersString("ganon battle", "ganon fight") }, + { 32, new SchrodingersString("triforce room") }, + { 33, new SchrodingersString("epilogue", "zelda epilogue", "zelda credits") }, + { 35, new SchrodingersString("eastern palace") }, + { 36, new SchrodingersString("desert palace") }, + { 37, new SchrodingersString("agahnims tower") }, + { 38, new SchrodingersString("swamp palace") }, + { 39, new SchrodingersString("palace of darkness", "dark palace") }, + { 40, new SchrodingersString("misery mire") }, + { 41, new SchrodingersString("skull woods") }, + { 42, new SchrodingersString("ice palace") }, + { 43, new SchrodingersString("tower of hera") }, + { 44, new SchrodingersString("thieves town") }, + { 45, new SchrodingersString("turtle rock") }, + { 46, new SchrodingersString("ganons tower") }, + { 47, new SchrodingersString("armos knights") }, + { 48, new SchrodingersString("lanmolas") }, + { 49, new SchrodingersString("agahnim 1") }, + { 50, new SchrodingersString("arrghus") }, + { 51, new SchrodingersString("helmasaur king") }, + { 52, new SchrodingersString("vitreous") }, + { 53, new SchrodingersString("mothula") }, + { 54, new SchrodingersString("kholdstare") }, + { 55, new SchrodingersString("moldorm") }, + { 56, new SchrodingersString("blind") }, + { 57, new SchrodingersString("trinexx") }, + { 58, new SchrodingersString("agahnim 2") }, + { 59, new SchrodingersString("ganons tower climb", "g t climb") }, + { 60, new SchrodingersString("light world 2") }, + { 61, new SchrodingersString("dark world 2") }, + { 99, new SchrodingersString("s m z 3 credits") }, + { 101, new SchrodingersString("samus fanfare") }, + { 102, new SchrodingersString("item acquired") }, + { 103, new SchrodingersString("item room", "elevator room") }, + { 104, new SchrodingersString("metroid opening with intro") }, + { 105, new SchrodingersString("metroid opening without intro") }, + { 106, new SchrodingersString("crateria landing with thunder", "crateria landing") }, + { 107, new SchrodingersString("crateria landing without thunder") }, + { 108, new SchrodingersString("crateria space pirates appear", "blue brinstar", "crateria 1", "space pirates appear") }, + { 109, new SchrodingersString("golden statues", "golden four") }, + { 110, new SchrodingersString("samus aran theme", "crateria 2") }, + { 111, new SchrodingersString("green brinstar", "pink brinstar") }, + { 112, new SchrodingersString("red brinstar", "kraid's lair") }, + { 113, new SchrodingersString("upper norfair") }, + { 114, new SchrodingersString("lower norfair") }, + { 115, new SchrodingersString("inner maridia") }, + { 116, new SchrodingersString("outer maridia") }, + { 117, new SchrodingersString("tourian") }, + { 118, new SchrodingersString("mother brain") }, + { 119, new SchrodingersString("big boss battle 1", "bozo", "bomb torizo", "golden torizo") }, + { 120, new SchrodingersString("evacuation", "escape") }, + { 121, new SchrodingersString("chozo statue awakens", "mysterious statue chamber") }, + { 122, new SchrodingersString("big boss battle 2", "crocomire") }, + { 123, new SchrodingersString("tension", "boss incoming", "hostile incoming") }, + { 124, new SchrodingersString("plant miniboss", "spore spawn", "botwoon") }, + { 126, new SchrodingersString("wrecked ship powered off") }, + { 127, new SchrodingersString("wrecked ship powered on") }, + { 128, new SchrodingersString("theme of super metroid") }, + { 129, new SchrodingersString("death cry", "samus death") }, + { 130, new SchrodingersString("metroid credits") }, + { 131, new SchrodingersString("kraid incoming") }, + { 132, new SchrodingersString("kraid battle", "kraid fight") }, + { 133, new SchrodingersString("phantoon incoming") }, + { 134, new SchrodingersString("phantoon battle", "phantoon fight") }, + { 135, new SchrodingersString("draygon battle", "draygon fight") }, + { 136, new SchrodingersString("ridley battle", "ridley fight") }, + { 137, new SchrodingersString("baby incoming") }, + { 138, new SchrodingersString("the baby") }, + { 139, new SchrodingersString("hyper beam") }, + { 140, new SchrodingersString("metroid game over", "super metroid game over") }, + }; + + /// + /// Gets the phrases for what song is playing + /// + /// + /// {0} is a placeholder for the song details + /// + public SchrodingersString? CurrentSong { get; init; } = new("That's {0}", "That song is {0}"); + + /// + /// Gets the phrases for what msu the current song is from + /// + /// + /// {0} is a placeholder for the msu details + /// + public SchrodingersString? CurrentMsu { get; init; } = new("The song is from the MSU pack {0}", "That song is from {0}"); + + /// + /// Gets the phrases for not being able to determine the playing song + /// + public SchrodingersString? UnknownSong { get; init; } = new("Sorry, I could not get the song details"); + + /// + /// Returns default response information + /// + /// + public static MsuConfig Default() + { + return new MsuConfig(); + } +} diff --git a/src/Randomizer.Data/Configuration/ConfigProvider.cs b/src/Randomizer.Data/Configuration/ConfigProvider.cs index 5509041b8..0e91b0b8d 100644 --- a/src/Randomizer.Data/Configuration/ConfigProvider.cs +++ b/src/Randomizer.Data/Configuration/ConfigProvider.cs @@ -173,6 +173,14 @@ public virtual UIConfig GetUIConfig(params string?[] profiles) => public virtual GameLinesConfig GetGameConfig(params string?[] profiles) => LoadYamlConfigs("game.yml", profiles); + /// + /// Returns the configs with msus + /// + /// The selected tracker profile(s) to load + /// + public virtual MsuConfig GetMsuConfig(params string?[] profiles) => + LoadYamlConfigs("msu.yml", profiles); + /// /// Returns a collection of all possible config profiles to /// select from diff --git a/src/Randomizer.Data/Configuration/ConfigServiceCollectionExtensions.cs b/src/Randomizer.Data/Configuration/ConfigServiceCollectionExtensions.cs index a09e884f1..3e584b5b0 100644 --- a/src/Randomizer.Data/Configuration/ConfigServiceCollectionExtensions.cs +++ b/src/Randomizer.Data/Configuration/ConfigServiceCollectionExtensions.cs @@ -88,6 +88,12 @@ public static IServiceCollection AddConfigs(this IServiceCollection services) return configs.GameLines; }); + services.AddScoped(serviceProvider => + { + var configs = serviceProvider.GetRequiredService(); + return configs.MsuConfig; + }); + return services; } } diff --git a/src/Randomizer.Data/Configuration/Configs.cs b/src/Randomizer.Data/Configuration/Configs.cs index 5c7c79a24..c3409f750 100644 --- a/src/Randomizer.Data/Configuration/Configs.cs +++ b/src/Randomizer.Data/Configuration/Configs.cs @@ -34,6 +34,7 @@ public Configs(OptionsFactory optionsFactory, ConfigProvider provider) Rewards = provider.GetRewardConfig(profiles); UILayouts = provider.GetUIConfig(profiles); GameLines = provider.GetGameConfig(profiles); + MsuConfig = provider.GetMsuConfig(profiles); } /// @@ -120,5 +121,10 @@ public Configs(OptionsFactory optionsFactory, ConfigProvider provider) /// Gets the in game lines /// public GameLinesConfig GameLines { get; } + + /// + /// Gets the msu config + /// + public MsuConfig MsuConfig { get; } } } diff --git a/src/Randomizer.Data/Configuration/Yaml/README.md b/src/Randomizer.Data/Configuration/Yaml/README.md index 3b6ab9464..25fd6e67e 100644 --- a/src/Randomizer.Data/Configuration/Yaml/README.md +++ b/src/Randomizer.Data/Configuration/Yaml/README.md @@ -23,6 +23,7 @@ Next you can then either copy the yml files from the Templates folder or create - **responses.yml** - Responses for specific actions performed by the user - **rewards.yml** - Additional reward information - **rooms.yml** - Additional room information +- **msu.yml** - Responses and configurations for MSU details **NOTE:** Contained inside each of the template yaml files is additional details on what can be modified for Tracker to say or understand. diff --git a/src/Randomizer.Data/Options/GeneralOptions.cs b/src/Randomizer.Data/Options/GeneralOptions.cs index 41aa86e88..617a2bbb7 100644 --- a/src/Randomizer.Data/Options/GeneralOptions.cs +++ b/src/Randomizer.Data/Options/GeneralOptions.cs @@ -71,6 +71,8 @@ public static IEnumerable TrackerVoiceFrequencyOptions public string RomOutputPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "Seeds"); + public string? MsuPath { get; set; } + public string AutoTrackerScriptsOutputPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SMZ3CasRandomizer", "AutoTrackerScripts"); diff --git a/src/Randomizer.Data/Options/PatchOptions.cs b/src/Randomizer.Data/Options/PatchOptions.cs index 89646e9ac..640481644 100644 --- a/src/Randomizer.Data/Options/PatchOptions.cs +++ b/src/Randomizer.Data/Options/PatchOptions.cs @@ -1,7 +1,9 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using MSURandomizerLibrary; namespace Randomizer.Data.Options { @@ -12,6 +14,10 @@ namespace Randomizer.Data.Options public class PatchOptions : INotifyPropertyChanged { private string _msu1Path = ""; + private string _msuName = ""; + private MsuRandomizationStyle? _msuRandomizationStyle; + private List _msuPaths = new List(); + public event PropertyChangedEventHandler? PropertyChanged; @@ -38,6 +44,45 @@ public string Msu1Path } } + public string MsuName + { + get => _msuName; + set + { + if (value != _msuName) + { + _msuName = value; + OnPropertyChanged(nameof(MsuName)); + } + } + } + + public List MsuPaths + { + get => _msuPaths; + set + { + if (value != _msuPaths) + { + _msuPaths = value; + OnPropertyChanged(nameof(MsuPaths)); + } + } + } + + public MsuRandomizationStyle? MsuRandomizationStyle + { + get => _msuRandomizationStyle; + set + { + if (value != _msuRandomizationStyle) + { + _msuRandomizationStyle = value; + OnPropertyChanged(nameof(MsuRandomizationStyle)); + } + } + } + public bool EnableExtendedSoundtrack { get; set; } public MusicShuffleMode ShuffleDungeonMusic { get; set; } diff --git a/src/Randomizer.Data/Options/RandomizerOptions.cs b/src/Randomizer.Data/Options/RandomizerOptions.cs index d5cfcaa90..7587d274d 100644 --- a/src/Randomizer.Data/Options/RandomizerOptions.cs +++ b/src/Randomizer.Data/Options/RandomizerOptions.cs @@ -59,6 +59,7 @@ public RandomizerOptions(GeneralOptions generalOptions, public bool EarlyItemsExpanded { get; set; } = false; public bool CustomizationExpanded { get; set; } = false; + public bool LocationExpanded { get; set; } = false; public bool LogicExpanded { get; set; } = false; public bool CommonExpanded { get; set; } = false; diff --git a/src/Randomizer.Data/Randomizer.Data.csproj b/src/Randomizer.Data/Randomizer.Data.csproj index 1a6f64c24..1b49bafd0 100644 --- a/src/Randomizer.Data/Randomizer.Data.csproj +++ b/src/Randomizer.Data/Randomizer.Data.csproj @@ -58,6 +58,7 @@ + diff --git a/src/Randomizer.Multiplayer.Server/Randomizer.Multiplayer.Server.csproj b/src/Randomizer.Multiplayer.Server/Randomizer.Multiplayer.Server.csproj index 11c770477..ec8087d2d 100644 --- a/src/Randomizer.Multiplayer.Server/Randomizer.Multiplayer.Server.csproj +++ b/src/Randomizer.Multiplayer.Server/Randomizer.Multiplayer.Server.csproj @@ -4,7 +4,7 @@ net7.0 enable enable - 2.1.1 + 2.1.2 False diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs index f8118537b..e25d9a599 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs @@ -17,9 +17,9 @@ public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, Au { var inCorrectLocation = currentState is { CurrentRoom: 55, IsOnBottomHalfOfRoom: true, IsOnRightHalfOfRoom: false }; - var prevInWall = prevState.LinkY is >= 1845 and <= 1855; + var prevInWall = prevState.LinkY is >= 1840 and <= 1863; var nowBelowWall = currentState is - { LinkX: >= 3665, LinkX: <= 3695, LinkY: >= 1858, LinkY: <= 1875, CurrentRoom: 55 }; + { LinkX: >= 3650, LinkX: <= 3696, LinkY: >= 1864, LinkY: <= 1872, CurrentRoom: 55 }; if (inCorrectLocation && prevInWall && nowBelowWall) { tracker.Say(x => x.AutoTracker.SpeckyClip); diff --git a/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj b/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj index 270718d34..10938a382 100644 --- a/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj +++ b/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Randomizer.SMZ3.Tracking/Tracker.cs b/src/Randomizer.SMZ3.Tracking/Tracker.cs index 8c8214caa..90d757564 100644 --- a/src/Randomizer.SMZ3.Tracking/Tracker.cs +++ b/src/Randomizer.SMZ3.Tracking/Tracker.cs @@ -294,6 +294,11 @@ public Tracker(ConfigProvider configProvider, /// public GeneratedRom? Rom { get; private set; } + /// + /// The path to the generated rom + /// + public string? RomPath { get; private set; } + /// /// The region the player is currently in according to the Auto Tracker /// @@ -406,11 +411,13 @@ public bool InitializeMicrophone() /// Loads the state from the database for a given rom /// /// The rom to load + /// The full path to the rom to load /// True or false if the load was successful - public bool Load(GeneratedRom rom) + public bool Load(GeneratedRom rom, string romPath) { IsDirty = false; Rom = rom; + RomPath = romPath; var trackerState = _stateService.LoadState(_worldAccessor.Worlds, rom); if (trackerState != null) diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs new file mode 100644 index 000000000..920b0a089 --- /dev/null +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Speech.Recognition; +using System.Timers; +using Microsoft.Extensions.Logging; +using MSURandomizerLibrary; +using MSURandomizerLibrary.Configs; +using MSURandomizerLibrary.Models; +using MSURandomizerLibrary.Services; +using Randomizer.Data.Configuration.ConfigFiles; +using Randomizer.Data.Configuration.ConfigTypes; +using Randomizer.SMZ3.Tracking.Services; + +namespace Randomizer.SMZ3.Tracking.VoiceCommands; + +/// +/// Module for tracker stating what the current song is +/// +public class MsuModule : TrackerModule, IDisposable +{ + private readonly IMsuSelectorService _msuSelectorService; + private Msu? _currentMsu; + private readonly string? _msuPath; + private readonly ICollection? _inputMsuPaths; + private readonly Timer? _timer; + private readonly MsuType? _msuType; + private readonly MsuConfig _msuConfig; + private readonly string MsuKey = "MsuKey"; + + /// + /// Constructor + /// + /// + /// Service to get item information + /// Service to get world information + /// + /// + /// + /// + /// + public MsuModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, IMsuLookupService msuLookupService, IMsuSelectorService msuSelectorService, IMsuTypeService msuTypeService, MsuConfig msuConfig) + : base(tracker, itemService, worldService, logger) + { + _msuSelectorService = msuSelectorService; + _msuType = msuTypeService.GetSMZ3MsuType(); + _msuConfig = msuConfig; + + if (!File.Exists(tracker.RomPath)) + { + throw new InvalidOperationException("No tracker rom file found"); + } + + var romFileInfo = new FileInfo(tracker.RomPath); + _msuPath = romFileInfo.FullName.Replace(romFileInfo.Extension, ".msu"); + + if (!File.Exists(_msuPath)) + { + return; + } + + try + { + _currentMsu = msuLookupService.LoadMsu(_msuPath, _msuType, false, true, true); + } + catch (Exception e) + { + Logger.LogError(e, "Error loading MSU {Path}", _msuPath); + return; + } + + if (_currentMsu == null) + { + logger.LogWarning("MSU file found but unable to load MSU"); + return; + } + + // Start reshuffling every minute if requested + if (tracker.Rom!.MsuRandomizationStyle == MsuRandomizationStyle.Continuous) + { + _inputMsuPaths = tracker.Rom!.MsuPaths?.Split("|"); + _timer = new Timer(TimeSpan.FromSeconds(60)); + _timer.Elapsed += TimerOnElapsed; + _timer.Start(); + } + + AddCommand("location song", GetLocationSongRules(), (result) => + { + if (_currentMsu == null) + { + Tracker.Say(_msuConfig.UnknownSong); + return; + } + + var trackNumber = (int)result.Semantics[MsuKey].Value; + var track = _currentMsu.GetTrackFor(trackNumber); + if (track != null) + { + var parts = new List() { track.SongName }; + if (!string.IsNullOrEmpty(track.DisplayAlbum)) + { + parts.Add($"from the album {track.DisplayAlbum}"); + } + else if (!string.IsNullOrEmpty(track.DisplayArtist)) + { + parts.Add($"by {track.DisplayArtist}"); + } + if (!string.IsNullOrEmpty(track.MsuName) && tracker.Rom!.MsuRandomizationStyle != null) + { + parts.Add($"from MSU Pack {track.MsuName}"); + if (!string.IsNullOrEmpty(track.MsuCreator)) parts.Add($"by {track.MsuCreator}"); + } + + Tracker.Say(_msuConfig.CurrentSong, string.Join("; ", parts)); + } + else + { + Tracker.Say(_msuConfig.UnknownSong); + } + }); + + AddCommand("location msu", GetLocationMsuRules(), (result) => + { + if (_currentMsu == null) + { + Tracker.Say(_msuConfig.UnknownSong); + return; + } + + var trackNumber = (int)result.Semantics[MsuKey].Value; + var track = _currentMsu.GetTrackFor(trackNumber); + if (track?.GetMsuName() != null) + { + Tracker.Say(_msuConfig.CurrentMsu, track.GetMsuName()); + } + else + { + Tracker.Say(_msuConfig.UnknownSong); + } + }); + } + + private GrammarBuilder GetLocationSongRules() + { + var msuLocations = new Choices(); + + foreach (var track in _msuConfig.TrackLocations) + { + foreach (var name in track.Value) + { + msuLocations.Add(new SemanticResultValue(name, track.Key)); + } + } + + var option1 = new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("what's the current song for", "what's the song for", "what's the current theme for", "what's the theme for") + .Optional("the") + .Append(MsuKey, msuLocations); + + var option2 = new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("what's the current", "what's the") + .Append(MsuKey, msuLocations) + .OneOf("song", "theme"); + + return GrammarBuilder.Combine(option1, option2); + + } + + private GrammarBuilder GetLocationMsuRules() + { + var msuLocations = new Choices(); + + foreach (var track in _msuConfig.TrackLocations) + { + foreach (var name in track.Value) + { + msuLocations.Add(new SemanticResultValue(name, track.Key)); + } + } + + var option1 = new GrammarBuilder() + .Append("Hey tracker,") + .Append("what MSU pack is") + .OneOf("the current song for", "the song for", "the current theme for", "the theme for") + .Optional("the") + .Append(MsuKey, msuLocations) + .Append("from"); + + var option2 = new GrammarBuilder() + .Append("Hey tracker,") + .Append("what MSU pack is") + .OneOf("the current", "the") + .Append(MsuKey, msuLocations) + .OneOf("song", "theme") + .Append("from"); + + return GrammarBuilder.Combine(option1, option2); + + } + + private void TimerOnElapsed(object? sender, ElapsedEventArgs e) + { + try + { + var response = _msuSelectorService.CreateShuffledMsu(new MsuSelectorRequest() + { + MsuPaths = _inputMsuPaths, OutputMsuType = _msuType, OutputPath = _msuPath, PrevMsu = _currentMsu + }); + _currentMsu = response.Msu; + } + catch (Exception exception) + { + Logger.LogError(exception, "Error creating MSU"); + } + + } + + public void Dispose() + { + if (_timer != null) + { + _timer.Stop(); + _timer.Dispose(); + } + } +} diff --git a/src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.Designer.cs b/src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.Designer.cs new file mode 100644 index 000000000..d6b217338 --- /dev/null +++ b/src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.Designer.cs @@ -0,0 +1,490 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Randomizer.Shared.Models; + +#nullable disable + +namespace Randomizer.Shared.Migrations +{ + [DbContext(typeof(RandomizerContext))] + [Migration("20230819021736_MsuPaths")] + partial class MsuPaths + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.9"); + + modelBuilder.Entity("Randomizer.Shared.Models.GeneratedRom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("GeneratorVersion") + .HasColumnType("INTEGER"); + + b.Property("Label") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MsuPaths") + .HasColumnType("TEXT"); + + b.Property("MultiplayerGameDetailsId") + .HasColumnType("INTEGER"); + + b.Property("RomPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Seed") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SpoilerPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MultiplayerGameDetailsId"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("GeneratedRoms"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.MultiplayerGameDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConnectionUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameGuid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GeneratedRomId") + .HasColumnType("INTEGER"); + + b.Property("JoinedDate") + .HasColumnType("TEXT"); + + b.Property("PlayerGuid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlayerKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GeneratedRomId") + .IsUnique(); + + b.ToTable("MultiplayerGames"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerBossState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoTracked") + .HasColumnType("INTEGER"); + + b.Property("BossName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Defeated") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerBossStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerDungeonState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoTracked") + .HasColumnType("INTEGER"); + + b.Property("Cleared") + .HasColumnType("INTEGER"); + + b.Property("HasManuallyClearedTreasure") + .HasColumnType("INTEGER"); + + b.Property("MarkedMedallion") + .HasColumnType("INTEGER"); + + b.Property("MarkedReward") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemainingTreasure") + .HasColumnType("INTEGER"); + + b.Property("RequiredMedallion") + .HasColumnType("INTEGER"); + + b.Property("Reward") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerDungeonStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerHistoryEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsImportant") + .HasColumnType("INTEGER"); + + b.Property("IsUndone") + .HasColumnType("INTEGER"); + + b.Property("LocationId") + .HasColumnType("INTEGER"); + + b.Property("LocationName") + .HasColumnType("TEXT"); + + b.Property("ObjectName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("REAL"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerHistoryEvents"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerItemState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ItemName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("TrackingState") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerItemStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerLocationState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Autotracked") + .HasColumnType("INTEGER"); + + b.Property("Cleared") + .HasColumnType("INTEGER"); + + b.Property("Item") + .HasColumnType("INTEGER"); + + b.Property("ItemWorldId") + .HasColumnType("INTEGER"); + + b.Property("LocationId") + .HasColumnType("INTEGER"); + + b.Property("MarkedItem") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerLocationStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerMarkedLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ItemName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LocationId") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerMarkedLocations"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerRegionState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Medallion") + .HasColumnType("INTEGER"); + + b.Property("Reward") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerRegionStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalWorldId") + .HasColumnType("INTEGER"); + + b.Property("PercentageCleared") + .HasColumnType("INTEGER"); + + b.Property("SecondsElapsed") + .HasColumnType("REAL"); + + b.Property("StartDateTime") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TrackerStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.GeneratedRom", b => + { + b.HasOne("Randomizer.Shared.Models.MultiplayerGameDetails", "MultiplayerGameDetails") + .WithMany() + .HasForeignKey("MultiplayerGameDetailsId"); + + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany() + .HasForeignKey("TrackerStateId"); + + b.Navigation("MultiplayerGameDetails"); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.MultiplayerGameDetails", b => + { + b.HasOne("Randomizer.Shared.Models.GeneratedRom", "GeneratedRom") + .WithOne() + .HasForeignKey("Randomizer.Shared.Models.MultiplayerGameDetails", "GeneratedRomId"); + + b.Navigation("GeneratedRom"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerBossState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("BossStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerDungeonState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("DungeonStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerHistoryEvent", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("History") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerItemState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("ItemStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerLocationState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("LocationStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerMarkedLocation", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("MarkedLocations") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerRegionState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("RegionStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerState", b => + { + b.Navigation("BossStates"); + + b.Navigation("DungeonStates"); + + b.Navigation("History"); + + b.Navigation("ItemStates"); + + b.Navigation("LocationStates"); + + b.Navigation("MarkedLocations"); + + b.Navigation("RegionStates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.cs b/src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.cs new file mode 100644 index 000000000..73817796d --- /dev/null +++ b/src/Randomizer.Shared/Migrations/20230819021736_MsuPaths.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Randomizer.Shared.Migrations +{ + /// + public partial class MsuPaths : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MsuPaths", + table: "GeneratedRoms", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MsuPaths", + table: "GeneratedRoms"); + } + } +} diff --git a/src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.Designer.cs b/src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.Designer.cs new file mode 100644 index 000000000..8aae2c7b6 --- /dev/null +++ b/src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.Designer.cs @@ -0,0 +1,493 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Randomizer.Shared.Models; + +#nullable disable + +namespace Randomizer.Shared.Migrations +{ + [DbContext(typeof(RandomizerContext))] + [Migration("20230819130906_MsuRandomizationStyle")] + partial class MsuRandomizationStyle + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.9"); + + modelBuilder.Entity("Randomizer.Shared.Models.GeneratedRom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("GeneratorVersion") + .HasColumnType("INTEGER"); + + b.Property("Label") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MsuPaths") + .HasColumnType("TEXT"); + + b.Property("MsuRandomizationStyle") + .HasColumnType("INTEGER"); + + b.Property("MultiplayerGameDetailsId") + .HasColumnType("INTEGER"); + + b.Property("RomPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Seed") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SpoilerPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MultiplayerGameDetailsId"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("GeneratedRoms"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.MultiplayerGameDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConnectionUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameGuid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GeneratedRomId") + .HasColumnType("INTEGER"); + + b.Property("JoinedDate") + .HasColumnType("TEXT"); + + b.Property("PlayerGuid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlayerKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GeneratedRomId") + .IsUnique(); + + b.ToTable("MultiplayerGames"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerBossState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoTracked") + .HasColumnType("INTEGER"); + + b.Property("BossName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Defeated") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerBossStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerDungeonState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoTracked") + .HasColumnType("INTEGER"); + + b.Property("Cleared") + .HasColumnType("INTEGER"); + + b.Property("HasManuallyClearedTreasure") + .HasColumnType("INTEGER"); + + b.Property("MarkedMedallion") + .HasColumnType("INTEGER"); + + b.Property("MarkedReward") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemainingTreasure") + .HasColumnType("INTEGER"); + + b.Property("RequiredMedallion") + .HasColumnType("INTEGER"); + + b.Property("Reward") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerDungeonStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerHistoryEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsImportant") + .HasColumnType("INTEGER"); + + b.Property("IsUndone") + .HasColumnType("INTEGER"); + + b.Property("LocationId") + .HasColumnType("INTEGER"); + + b.Property("LocationName") + .HasColumnType("TEXT"); + + b.Property("ObjectName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("REAL"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerHistoryEvents"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerItemState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ItemName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("TrackingState") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerItemStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerLocationState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Autotracked") + .HasColumnType("INTEGER"); + + b.Property("Cleared") + .HasColumnType("INTEGER"); + + b.Property("Item") + .HasColumnType("INTEGER"); + + b.Property("ItemWorldId") + .HasColumnType("INTEGER"); + + b.Property("LocationId") + .HasColumnType("INTEGER"); + + b.Property("MarkedItem") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerLocationStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerMarkedLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ItemName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LocationId") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerMarkedLocations"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerRegionState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Medallion") + .HasColumnType("INTEGER"); + + b.Property("Reward") + .HasColumnType("INTEGER"); + + b.Property("TrackerStateId") + .HasColumnType("INTEGER"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TrackerStateId"); + + b.ToTable("TrackerRegionStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalWorldId") + .HasColumnType("INTEGER"); + + b.Property("PercentageCleared") + .HasColumnType("INTEGER"); + + b.Property("SecondsElapsed") + .HasColumnType("REAL"); + + b.Property("StartDateTime") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TrackerStates"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.GeneratedRom", b => + { + b.HasOne("Randomizer.Shared.Models.MultiplayerGameDetails", "MultiplayerGameDetails") + .WithMany() + .HasForeignKey("MultiplayerGameDetailsId"); + + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany() + .HasForeignKey("TrackerStateId"); + + b.Navigation("MultiplayerGameDetails"); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.MultiplayerGameDetails", b => + { + b.HasOne("Randomizer.Shared.Models.GeneratedRom", "GeneratedRom") + .WithOne() + .HasForeignKey("Randomizer.Shared.Models.MultiplayerGameDetails", "GeneratedRomId"); + + b.Navigation("GeneratedRom"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerBossState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("BossStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerDungeonState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("DungeonStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerHistoryEvent", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("History") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerItemState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("ItemStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerLocationState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("LocationStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerMarkedLocation", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("MarkedLocations") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerRegionState", b => + { + b.HasOne("Randomizer.Shared.Models.TrackerState", "TrackerState") + .WithMany("RegionStates") + .HasForeignKey("TrackerStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("TrackerState"); + }); + + modelBuilder.Entity("Randomizer.Shared.Models.TrackerState", b => + { + b.Navigation("BossStates"); + + b.Navigation("DungeonStates"); + + b.Navigation("History"); + + b.Navigation("ItemStates"); + + b.Navigation("LocationStates"); + + b.Navigation("MarkedLocations"); + + b.Navigation("RegionStates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.cs b/src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.cs new file mode 100644 index 000000000..c62bec988 --- /dev/null +++ b/src/Randomizer.Shared/Migrations/20230819130906_MsuRandomizationStyle.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Randomizer.Shared.Migrations +{ + /// + public partial class MsuRandomizationStyle : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MsuRandomizationStyle", + table: "GeneratedRoms", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MsuRandomizationStyle", + table: "GeneratedRoms"); + } + } +} diff --git a/src/Randomizer.Shared/Migrations/RandomizerContextModelSnapshot.cs b/src/Randomizer.Shared/Migrations/RandomizerContextModelSnapshot.cs index b470df301..a906a5573 100644 --- a/src/Randomizer.Shared/Migrations/RandomizerContextModelSnapshot.cs +++ b/src/Randomizer.Shared/Migrations/RandomizerContextModelSnapshot.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Randomizer.Shared.Models; +#nullable disable + namespace Randomizer.Shared.Migrations { [DbContext(typeof(RandomizerContext))] @@ -13,8 +15,7 @@ partial class RandomizerContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.12"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.9"); modelBuilder.Entity("Randomizer.Shared.Models.GeneratedRom", b => { @@ -32,6 +33,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("MsuPaths") + .HasColumnType("TEXT"); + + b.Property("MsuRandomizationStyle") + .HasColumnType("INTEGER"); + b.Property("MultiplayerGameDetailsId") .HasColumnType("INTEGER"); diff --git a/src/Randomizer.Shared/Models/GeneratedRom.cs b/src/Randomizer.Shared/Models/GeneratedRom.cs index 1c7be2020..0b45cff84 100644 --- a/src/Randomizer.Shared/Models/GeneratedRom.cs +++ b/src/Randomizer.Shared/Models/GeneratedRom.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; +using MSURandomizerLibrary; using Randomizer.Shared.Multiplayer; namespace Randomizer.Shared.Models @@ -20,6 +21,8 @@ public class GeneratedRom public string SpoilerPath { get; init; } = ""; [ForeignKey("MultiplayerGameDetails")] public long? MultiplayerGameDetailsId { get; set; } + public string? MsuPaths { get; set; } + public MsuRandomizationStyle? MsuRandomizationStyle { get; set; } public virtual MultiplayerGameDetails? MultiplayerGameDetails { get; set; } public TrackerState? TrackerState { get; set; } diff --git a/src/Randomizer.Shared/Randomizer.Shared.csproj b/src/Randomizer.Shared/Randomizer.Shared.csproj index 7fec6722d..5d8ba4748 100644 --- a/src/Randomizer.Shared/Randomizer.Shared.csproj +++ b/src/Randomizer.Shared/Randomizer.Shared.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Randomizer.Tools/RomGenerator.cs b/src/Randomizer.Tools/RomGenerator.cs index 710a73328..a8e8e8588 100644 --- a/src/Randomizer.Tools/RomGenerator.cs +++ b/src/Randomizer.Tools/RomGenerator.cs @@ -147,11 +147,11 @@ public static string GenerateRom(string[] args) if (copyMsu) { - var msuGenerator = new MsuGeneratorService(); + /*var msuGenerator = new MsuGeneratorService(); if (!msuGenerator.EnableMsu1Support(MsuPath, romPath, out var error)) { throw new Exception(error); - } + }*/ } if (openRom)