diff --git a/src/Randomizer.App/Windows/OptionsWindow.xaml b/src/Randomizer.App/Windows/OptionsWindow.xaml index 7eccc908e..703dbf029 100644 --- a/src/Randomizer.App/Windows/OptionsWindow.xaml +++ b/src/Randomizer.App/Windows/OptionsWindow.xaml @@ -7,6 +7,7 @@ xmlns:vm="clr-namespace:Randomizer.App.ViewModels" xmlns:options="clr-namespace:Randomizer.Data.Options;assembly=Randomizer.Data" xmlns:app="clr-namespace:Randomizer.App" + xmlns:enums="clr-namespace:Randomizer.Shared.Enums;assembly=Randomizer.Shared" x:Name="Self" ResizeMode="NoResize" mc:Ignorable="d" @@ -177,10 +178,9 @@ - + @@ -192,10 +192,9 @@ - + @@ -218,10 +217,9 @@ - + diff --git a/src/Randomizer.App/Windows/TrackerWindow.xaml.cs b/src/Randomizer.App/Windows/TrackerWindow.xaml.cs index a6c682a3f..066f52a2d 100644 --- a/src/Randomizer.App/Windows/TrackerWindow.xaml.cs +++ b/src/Randomizer.App/Windows/TrackerWindow.xaml.cs @@ -1048,9 +1048,9 @@ private async void LoadSavedStateMenuItem_Click(object sender, RoutedEventArgs e private async Task SaveStateAsync() { // If there is a rom, save it to the database - if (GeneratedRom.IsValid(Rom)) + if (GeneratedRom.IsValid(Tracker.Rom)) { - await Tracker.SaveAsync(Rom); + await Tracker.SaveAsync(); } SavedState?.Invoke(this, EventArgs.Empty); diff --git a/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs b/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs new file mode 100644 index 000000000..ee0541345 --- /dev/null +++ b/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs @@ -0,0 +1,284 @@ +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Randomizer.Data.Options; +using Randomizer.Data.WorldData; +using Randomizer.Data.WorldData.Regions; +using Randomizer.Shared; +using Randomizer.Shared.Enums; +using Randomizer.Shared.Models; +using Randomizer.SMZ3.Generation; +using Randomizer.SMZ3.Tracking; +using Randomizer.SMZ3.Tracking.AutoTracking; +using Randomizer.SMZ3.Tracking.Services; + +namespace Randomizer.CrossPlatform; + +public class ConsoleTrackerDisplayService +{ + private readonly System.Timers.Timer _timer; + private readonly Smz3GeneratedRomLoader _romLoaderService; + private readonly TrackerOptionsAccessor _trackerOptionsAccessor; + private readonly RandomizerOptions _options; + private readonly IServiceProvider _serviceProvider; + private Tracker _tracker = null!; + private World _world = null!; + private IWorldService _worldService = null!; + private Region _lastRegion = null!; + + public ConsoleTrackerDisplayService(IServiceProvider serviceProvider, Smz3GeneratedRomLoader romLoaderService, TrackerOptionsAccessor trackerOptionsAccessor, OptionsFactory optionsFactory) + { + _romLoaderService = romLoaderService; + _trackerOptionsAccessor = trackerOptionsAccessor; + _options = optionsFactory.Create(); + _serviceProvider = serviceProvider; + _timer = new System.Timers.Timer(TimeSpan.FromMilliseconds(250)); + _timer.Elapsed += delegate { UpdateScreen(); }; + } + + public async Task StartTracking(GeneratedRom rom, string romPath) + { + _trackerOptionsAccessor.Options = _options.GeneralOptions.GetTrackerOptions(); + _world = _romLoaderService.LoadGeneratedRom(rom).First(x => x.IsLocalWorld); + _worldService = _serviceProvider.GetRequiredService(); + _tracker = _serviceProvider.GetRequiredService(); + _tracker.Load(rom, romPath); + _tracker.TryStartTracking(); + _tracker.AutoTracker?.SetConnector(_options.AutoTrackerDefaultConnector, _options.AutoTrackerQUsb2SnesIp); + + if (_tracker.AutoTracker != null) + { + _tracker.AutoTracker.AutoTrackerConnected += delegate + { + UpdateScreen(); + if (!_timer.Enabled) + { + _timer.Start(); + } + }; + + _tracker.LocationCleared += delegate(object? _, LocationClearedEventArgs args) + { + _lastRegion = args.Location.Region; + }; + } + + while (true) + { + Console.ReadKey(); + _timer.Stop(); + Console.Clear(); + Console.Write("Do you want to quit? (y/n) "); + var response = Console.ReadLine(); + + if ("y".Equals(response, StringComparison.OrdinalIgnoreCase)) + { + await _tracker.SaveAsync(); + break; + } + + _timer.Start(); + UpdateScreen(); + } + } + + private void UpdateScreen() + { + var columnWidth = (Console.WindowWidth-6)/2; + + var topLines = GetHeaderLines(columnWidth); + + var leftColumnLines = new List(); + leftColumnLines.AddRange(GetInventoryLines(columnWidth)); + leftColumnLines.Add(""); + leftColumnLines.AddRange(GetDungeonLines(columnWidth)); + + var rightColumnLines = new List(); + rightColumnLines.AddRange(GetLocationLines(columnWidth)); + + while (leftColumnLines.Count < Console.WindowHeight - 1) + { + leftColumnLines.Add(""); + } + + while (rightColumnLines.Count < Console.WindowHeight - 1) + { + rightColumnLines.Add(""); + } + + var sb = new StringBuilder(); + + foreach (var line in topLines) + { + sb.AppendLine(line); + } + + for (var i = 0; i < leftColumnLines.Count && i < Console.WindowHeight - topLines.Count - 1; i++) + { + // Retrieve and pad left column, trimming if needed + var leftColumn = leftColumnLines[i]; + if (leftColumn.Length > columnWidth) + { + leftColumn = leftColumn[..(columnWidth - 3)] + "... "; + } + else + { + leftColumn = leftColumn.PadRight(columnWidth); + } + + // Retrieve right column, trimming if needed + var rightColumn = rightColumnLines[i]; + if (rightColumn.Length > columnWidth) + { + rightColumn = rightColumn[..(columnWidth - 3)] + "..."; + } + + sb.AppendLine($"{leftColumn} {rightColumn}"); + } + + Console.Clear(); + Console.Write(sb.ToString()); + } + + private List GetHeaderLines(int columnWidth) + { + var lines = new List(); + + var connected = $"Connected: {_tracker.AutoTracker?.IsConnected == true}"; + + switch (_tracker.AutoTracker?.CurrentGame) + { + case Game.Zelda: + lines.Add($"{connected} | {_tracker.AutoTracker.ZeldaState}"); + break; + case Game.SM: + lines.Add($"{connected} | {_tracker.AutoTracker.MetroidState}"); + break; + default: + lines.Add(connected); + break; + } + + lines.Add(new string('-', columnWidth * 2 + 5)); + + return lines; + } + + private IEnumerable GetDungeonLines(int columnWidth) + { + var lines = new List { "Dungeons", new('-', columnWidth) }; + + var dungeons = _world.Dungeons + .Select(x => GetDungeonDetails(x).PadRight(18)) + .ToList(); + + var dungeonLine = ""; + foreach (var dungeon in dungeons) + { + if (dungeonLine == "") + { + dungeonLine = dungeon; + } + else if ($"{dungeonLine}{dungeon}".Length > columnWidth) + { + lines.Add(dungeonLine); + dungeonLine = dungeon; + } + else + { + dungeonLine += dungeon; + } + } + lines.Add(dungeonLine); + + return lines; + } + + private IEnumerable GetLocationLines(int columnWidth) + { + var lines = new List { "Locations", new('-', columnWidth) }; + + var locations = _worldService.Locations(unclearedOnly: true, outOfLogic: false, assumeKeys: true, + sortByTopRegion: true, regionFilter: RegionFilter.None).ToList(); + + var regionCounts = locations + .GroupBy(x => x.Region) + .OrderByDescending(x => x.Count()) + .ToDictionary(x => x.Key, x => x.Count()); + + var locationNames = locations + .OrderByDescending(x => x.Region == _lastRegion) + .ThenByDescending(x => regionCounts[x.Region]) + .ThenBy(x => x.ToString()) + .Select(x => x.ToString()); + lines.AddRange(locationNames); + + return lines; + } + + private IEnumerable GetInventoryLines(int columnWidth) + { + var lines = new List { "Inventory", new('-', columnWidth) }; + + var itemNames = _world.AllItems.Where(x => x.State.TrackingState > 0) + .DistinctBy(x => x.Type) + .Where(x => !x.Type.IsInAnyCategory(ItemCategory.Junk, ItemCategory.Map, ItemCategory.Compass, ItemCategory.Keycard, ItemCategory.BigKey, ItemCategory.SmallKey) || x.Type is ItemType.Missile or ItemType.Super or ItemType.PowerBomb or ItemType.ETank) + .OrderBy(x => x.Type.IsInCategory(ItemCategory.Metroid)) + .ThenBy(x => x.Name) + .Select(x => x.Metadata.HasStages || x.Metadata.Multiple + ? $"{x.Name} ({x.State.TrackingState})" + : x.Name) + .ToList(); + + for (var i = 0; i < itemNames.Count; i += 2) + { + if (i < itemNames.Count - 1) + { + lines.Add(itemNames[i].PadRight(columnWidth/2) + itemNames[i+1]); + } + else + { + lines.Add(itemNames[i]); + } + } + + return lines; + } + + private string GetDungeonDetails(IDungeon dungeon) + { + var state = dungeon.DungeonState.Cleared ? "\u2713" : "\u274c"; + var abbreviation = dungeon.DungeonMetadata.Abbreviation; + + var reward = dungeon is IHasReward + ? dungeon.MarkedReward switch + { + RewardType.None => "??", + RewardType.CrystalBlue => "BC", + RewardType.CrystalRed => "RC", + RewardType.PendantBlue => "BP", + RewardType.PendantGreen => "GP", + RewardType.PendantRed => "RP", + _ => null + } + : null; + + var requirement = dungeon is INeedsMedallion + ? dungeon.MarkedMedallion switch + { + ItemType.Quake => "Q", + ItemType.Bombos => "B", + ItemType.Ether => "E", + _ => "?" + } + : null; + + var rewardRequirement = ""; + if (reward != null) + { + rewardRequirement = requirement == null ? $" ({reward})" : $" ({reward}/{requirement})"; + } + + return $"{state} {abbreviation}{rewardRequirement}: {dungeon.DungeonState.RemainingTreasure}"; + } + +} diff --git a/src/Randomizer.CrossPlatform/Program.cs b/src/Randomizer.CrossPlatform/Program.cs index c9fe6be5b..31b5ca080 100644 --- a/src/Randomizer.CrossPlatform/Program.cs +++ b/src/Randomizer.CrossPlatform/Program.cs @@ -7,7 +7,6 @@ using Randomizer.Data.Services; using Randomizer.SMZ3.Generation; using Serilog; -using Serilog.Events; namespace Randomizer.CrossPlatform; @@ -17,13 +16,8 @@ public static class Program public static void Main(string[] args) { - var logLevel = LogEventLevel.Warning; - if (args.Any(x => x == "-v")) - logLevel = LogEventLevel.Information; - Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.Console(logLevel) .WriteTo.File(LogPath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30) .CreateLogger(); @@ -46,6 +40,8 @@ public static void Main(string[] args) return; } + Console.WriteLine($"Using Randomizer Options file at {optionsFile.FullName}"); + var result = DisplayMenu("What do you want to do?", new List() { "Generate & Play a Rom", @@ -82,6 +78,7 @@ public static void Main(string[] args) var romPath = Path.Combine(randomizerOptions.RomOutputPath, results.Rom!.RomPath); Console.WriteLine($"Rom generated successfully: {romPath}"); Launch(romPath, randomizerOptions); + _ = s_services.GetRequiredService().StartTracking(results.Rom, romPath); } else { @@ -103,7 +100,9 @@ public static void Main(string[] args) var selectedRom = roms[result.Value.Item1]; var romPath = Path.Combine(randomizerOptions.RomOutputPath, selectedRom.RomPath); Launch(romPath, randomizerOptions); + _ = s_services.GetRequiredService().StartTracking(selectedRom, romPath); } + } // Deletes rom(s) else if (result.Value.Item1 == 2) diff --git a/src/Randomizer.CrossPlatform/Randomizer.CrossPlatform.csproj b/src/Randomizer.CrossPlatform/Randomizer.CrossPlatform.csproj index fe1311fd2..bdba4ab03 100644 --- a/src/Randomizer.CrossPlatform/Randomizer.CrossPlatform.csproj +++ b/src/Randomizer.CrossPlatform/Randomizer.CrossPlatform.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs b/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs index c507f17c9..e21742e6c 100644 --- a/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs +++ b/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs @@ -3,7 +3,11 @@ using Randomizer.Data.Configuration; using Randomizer.Data.Options; using Randomizer.Data.Services; +using Randomizer.Multiplayer.Client; using Randomizer.SMZ3.ChatIntegration; +using Randomizer.SMZ3.Tracking; +using Randomizer.SMZ3.Tracking.AutoTracking; +using Randomizer.SMZ3.Tracking.VoiceCommands; using Randomizer.SMZ3.Twitch; using Randomizer.Sprites; @@ -16,8 +20,16 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi // Randomizer + Tracker services.AddConfigs(); services.AddRandomizerServices(); - + services.AddTracker() + .AddOptionalModule() + .AddOptionalModule() + .AddOptionalModule() + .AddOptionalModule() + .AddOptionalModule(); + services.AddScoped(); services.AddSingleton(); + services.AddMultiplayerServices(); + services.AddSingleton(); // Chat @@ -32,6 +44,7 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/src/Randomizer.Data/Options/GeneralOptions.cs b/src/Randomizer.Data/Options/GeneralOptions.cs index ce9004254..fa99bfb34 100644 --- a/src/Randomizer.Data/Options/GeneralOptions.cs +++ b/src/Randomizer.Data/Options/GeneralOptions.cs @@ -3,11 +3,9 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text.Json.Serialization; -using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Randomizer.Shared.Enums; +using YamlDotNet.Serialization; namespace Randomizer.Data.Options { @@ -22,48 +20,6 @@ public class GeneralOptions : INotifyPropertyChanged private string? _twitchChannel; private string? _twitchId; - /// - /// Converts the enum descriptions into a string array for displaying in a dropdown - /// - public static IEnumerable QuickLaunchOptions - { - get - { - var attributes = typeof(LaunchButtonOptions).GetMembers() - .SelectMany(member => member.GetCustomAttributes(typeof(DescriptionAttribute), true).Cast()) - .ToList(); - return attributes.Select(x => x.Description); - } - } - - /// - /// Converts the enum descriptions into a string array for displaying in a dropdown - /// - public static IEnumerable AutoTrackerConnectorOptions - { - get - { - var attributes = typeof(EmulatorConnectorType).GetMembers() - .SelectMany(member => member.GetCustomAttributes(typeof(DescriptionAttribute), true).Cast()) - .ToList(); - return attributes.Select(x => x.Description); - } - } - - /// - /// Converts the enum descriptions into a string array for displaying in a dropdown - /// - public static IEnumerable TrackerVoiceFrequencyOptions - { - get - { - var attributes = typeof(TrackerVoiceFrequency).GetMembers() - .SelectMany(member => member.GetCustomAttributes(typeof(DescriptionAttribute), true).Cast()) - .ToList(); - return attributes.Select(x => x.Description); - } - } - public string? Z3RomPath { get; set; } public string? SMRomPath { get; set; } @@ -93,16 +49,25 @@ public static IEnumerable TrackerVoiceFrequencyOptions public bool TrackerShadows { get; set; } = true; + [YamlIgnore] public int LaunchButton { get; set; } = (int)LaunchButtonOptions.PlayAndTrack; + public LaunchButtonOptions LaunchButtonOption { get; set; } + + [YamlIgnore] public int VoiceFrequency { get; set; } = (int)TrackerVoiceFrequency.All; + public TrackerVoiceFrequency TrackerVoiceFrequency { get; set; } + public bool TrackerHintsEnabled { get; set; } public bool TrackerSpoilersEnabled { get; set; } + [YamlIgnore] public int AutoTrackerDefaultConnector { get; set; } = (int)EmulatorConnectorType.None; + public EmulatorConnectorType AutoTrackerDefaultConnectionType { get; set; } + public string? AutoTrackerQUsb2SnesIp { get; set; } public bool AutoTrackerChangeMap { get; set; } @@ -178,7 +143,7 @@ public string? TwitchId public bool EnablePollCreation { get; set; } = true; - public int ChatGreetingTimeLimit { get; set; } = 0; + public int ChatGreetingTimeLimit { get; set; } public ICollection SelectedProfiles { get; set; } = new List { "Sassy" }; @@ -209,6 +174,11 @@ public string? TwitchId /// public string? IgnoredUpdateUrl { get; set; } + /// + /// Automatically tracks the map and other "hey tracker, look at this" events when viewing + /// + public bool AutoSaveLookAtEvents { get; set; } + public event PropertyChangedEventHandler? PropertyChanged; public bool Validate() @@ -230,11 +200,12 @@ public bool Validate() ChatGreetingTimeLimit = ChatGreetingTimeLimit, PollCreationEnabled = EnablePollCreation, AutoTrackerChangeMap = AutoTrackerChangeMap, - VoiceFrequency = (TrackerVoiceFrequency)VoiceFrequency, + VoiceFrequency = TrackerVoiceFrequency, TrackerProfiles = SelectedProfiles, UndoExpirationTime = UndoExpirationTime, MsuTrackDisplayStyle = MsuTrackDisplayStyle, - MsuTrackOutputPath = MsuTrackOutputPath + MsuTrackOutputPath = MsuTrackOutputPath, + AutoSaveLookAtEvents = AutoSaveLookAtEvents }; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) @@ -254,7 +225,10 @@ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName return twitchUri.AbsolutePath.TrimStart('/'); } } - catch { } + catch + { + // ignored + } return value; } diff --git a/src/Randomizer.Data/Options/RandomizerOptions.cs b/src/Randomizer.Data/Options/RandomizerOptions.cs index 4ac73121c..6b9adba5d 100644 --- a/src/Randomizer.Data/Options/RandomizerOptions.cs +++ b/src/Randomizer.Data/Options/RandomizerOptions.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Randomizer.Data.Logic; +using Randomizer.Shared.Enums; using YamlDotNet.Serialization; namespace Randomizer.Data.Options @@ -91,7 +92,7 @@ public string AutoTrackerScriptsOutputPath [YamlIgnore] public EmulatorConnectorType AutoTrackerDefaultConnector { - get => (EmulatorConnectorType)GeneralOptions.AutoTrackerDefaultConnector; + get => GeneralOptions.AutoTrackerDefaultConnectionType; } [YamlIgnore] @@ -115,6 +116,12 @@ public static RandomizerOptions Load(string loadPath, string savePath, bool isYa { var options = JsonSerializer.Deserialize(fileText, s_jsonOptions) ?? new(); options.FilePath = savePath; + options.GeneralOptions.AutoTrackerDefaultConnectionType = + (EmulatorConnectorType)options.GeneralOptions.AutoTrackerDefaultConnector; + options.GeneralOptions.TrackerVoiceFrequency = + (TrackerVoiceFrequency)options.GeneralOptions.VoiceFrequency; + options.GeneralOptions.LaunchButtonOption = + (LaunchButtonOptions)options.GeneralOptions.LaunchButton; return options; } } diff --git a/src/Randomizer.Data/Options/TrackerOptions.cs b/src/Randomizer.Data/Options/TrackerOptions.cs index 3b666e04d..86fb30478 100644 --- a/src/Randomizer.Data/Options/TrackerOptions.cs +++ b/src/Randomizer.Data/Options/TrackerOptions.cs @@ -110,5 +110,10 @@ public record TrackerOptions /// The file to write the current song to /// public string? MsuTrackOutputPath { get; set; } + + /// + /// Automatically tracks the map and other "hey tracker, look at this" events when viewing + /// + public bool AutoSaveLookAtEvents { get; set; } } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs index 91dd2073d..43db26e99 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs @@ -583,5 +583,10 @@ private bool IsInGame(Game game = Game.Both) } return false; } + + public override void AddCommands() + { + + } } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs index 4e516e9a0..2fa5d26d1 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs @@ -52,10 +52,18 @@ public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, Au if (currentRegion is LightWorldNorthWest or LightWorldNorthEast or LightWorldSouth or LightWorldDeathMountainEast or LightWorldDeathMountainWest) { tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(UpdateLightWorldRewards); + if (tracker.Options.AutoSaveLookAtEvents) + { + tracker.AutoTracker.LatestViewAction.Invoke(); + } } else if (currentRegion is DarkWorldNorthWest or DarkWorldNorthEast or DarkWorldSouth or DarkWorldMire or DarkWorldDeathMountainEast or DarkWorldDeathMountainWest) { tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(UpdateDarkWorldRewards); + if (tracker.Options.AutoSaveLookAtEvents) + { + tracker.AutoTracker.LatestViewAction.Invoke(); + } } return true; diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs index 0590605b1..132d9e5ca 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs @@ -31,7 +31,7 @@ public ViewedMedallion(IWorldAccessor worldAccessor, IItemService itemService) /// True if the check was identified, false otherwise public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - if (tracker.AutoTracker == null) return false; + if (tracker.AutoTracker == null || tracker.AutoTracker.LatestViewAction?.IsValid == true) return false; _tracker = tracker; @@ -41,11 +41,19 @@ public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, Au if (currentState.OverworldScreen == 112 && x is >= 172 and <= 438 && y is >= 3200 and <= 3432) { tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(MarkMiseryMireMedallion); + if (tracker.Options.AutoSaveLookAtEvents) + { + tracker.AutoTracker.LatestViewAction.Invoke(); + } return true; } else if (currentState.OverworldScreen == 71 && x is >= 3708 and <= 4016 && y is >= 128 and <= 368) { tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(MarkTurtleRockMedallion); + if (tracker.Options.AutoSaveLookAtEvents) + { + tracker.AutoTracker.LatestViewAction.Invoke(); + } return true; } diff --git a/src/Randomizer.SMZ3.Tracking/Services/ISpeechRecognitionService.cs b/src/Randomizer.SMZ3.Tracking/Services/ISpeechRecognitionService.cs new file mode 100644 index 000000000..f4691be68 --- /dev/null +++ b/src/Randomizer.SMZ3.Tracking/Services/ISpeechRecognitionService.cs @@ -0,0 +1,46 @@ +using System.Speech.Recognition; + +namespace Randomizer.SMZ3.Tracking.Services; + +/// +/// Service for recognizing speech from a microphone +/// +public interface ISpeechRecognitionService +{ + /// + /// Event fired when speech was successfully understood + /// + public event System.EventHandler? SpeechRecognized; + + /// + /// Updates the microphone to be the default Windows mic + /// + public void SetInputToDefaultAudioDevice(); + + /// + /// The internal speech recognition engine used by the service, if applicable + /// + public SpeechRecognitionEngine? RecognitionEngine { get; } + + /// + /// Stop recognizing speech + /// + public void RecognizeAsyncStop(); + + /// + /// Start recognizing speech + /// + /// The recognition mode (single/multiple) + public void RecognizeAsync(RecognizeMode Mode); + + /// + /// Initializes the microphone to the default Windows mic + /// + /// True if successful, false otherwise + public bool InitializeMicrophone(); + + /// + /// Disposes of the service and speech recognition engine + /// + public void Dispose(); +} diff --git a/src/Randomizer.SMZ3.Tracking/Services/SpeechRecognitionServiceDisabled.cs b/src/Randomizer.SMZ3.Tracking/Services/SpeechRecognitionServiceDisabled.cs new file mode 100644 index 000000000..3022fe870 --- /dev/null +++ b/src/Randomizer.SMZ3.Tracking/Services/SpeechRecognitionServiceDisabled.cs @@ -0,0 +1,35 @@ +using System; +using System.Speech.Recognition; + +namespace Randomizer.SMZ3.Tracking.Services; + +public class SpeechRecognitionServiceDisabled : ISpeechRecognitionService +{ + public event EventHandler? SpeechRecognized; + + public void SetInputToDefaultAudioDevice() + { + + } + + public SpeechRecognitionEngine? RecognitionEngine => null; + + public void RecognizeAsyncStop() + { + } + + public void RecognizeAsync(RecognizeMode Mode) + { + + } + + public bool InitializeMicrophone() + { + return false; + } + + public void Dispose() + { + + } +} diff --git a/src/Randomizer.SMZ3.Tracking/Services/SpeechRecognitionServiceEnabled.cs b/src/Randomizer.SMZ3.Tracking/Services/SpeechRecognitionServiceEnabled.cs new file mode 100644 index 000000000..62c748ad1 --- /dev/null +++ b/src/Randomizer.SMZ3.Tracking/Services/SpeechRecognitionServiceEnabled.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Speech.Recognition; +using Microsoft.Extensions.Logging; + +namespace Randomizer.SMZ3.Tracking.Services; + +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] +public class SpeechRecognitionServiceEnabled : ISpeechRecognitionService +{ + private readonly ILogger _logger; + private readonly SpeechRecognitionEngine _recognizer = new(); + + /// + /// Dll to get the number of microphones + /// + /// + [DllImport("winmm.dll")] + private static extern int waveInGetNumDevs(); + + public SpeechRecognitionServiceEnabled(ILogger logger) + { + _logger = logger; + _recognizer.SpeechRecognized += RecognizerOnSpeechRecognized; + } + + private void RecognizerOnSpeechRecognized(object? sender, SpeechRecognizedEventArgs e) + { + SpeechRecognized?.Invoke(sender, e); + } + + public event EventHandler? SpeechRecognized; + + public void SetInputToDefaultAudioDevice() => _recognizer.SetInputToDefaultAudioDevice(); + + public SpeechRecognitionEngine? RecognitionEngine => _recognizer; + + public void RecognizeAsyncStop() =>_recognizer.RecognizeAsyncStop(); + + public void RecognizeAsync(RecognizeMode mode) => _recognizer.RecognizeAsync(mode); + + public bool InitializeMicrophone() + { + try + { + if (waveInGetNumDevs() == 0) + { + _logger.LogWarning("No microphone device found."); + return false; + } + _recognizer.SetInputToDefaultAudioDevice(); + return true; + } + catch (Exception e) + { + _logger.LogError(e, "Error initializing microphone"); + return false; + } + } + + public void Dispose() => _recognizer.Dispose(); +} diff --git a/src/Randomizer.SMZ3.Tracking/Services/TextToSpeechCommunicator.cs b/src/Randomizer.SMZ3.Tracking/Services/TextToSpeechCommunicator.cs index fd18f119d..6be8348cf 100644 --- a/src/Randomizer.SMZ3.Tracking/Services/TextToSpeechCommunicator.cs +++ b/src/Randomizer.SMZ3.Tracking/Services/TextToSpeechCommunicator.cs @@ -1,6 +1,5 @@ using System; using System.Speech.Synthesis; -using Randomizer.Data; using Randomizer.Data.Options; namespace Randomizer.SMZ3.Tracking.Services @@ -11,7 +10,7 @@ namespace Randomizer.SMZ3.Tracking.Services /// public class TextToSpeechCommunicator : ICommunicator, IDisposable { - private readonly SpeechSynthesizer _tts; + private readonly SpeechSynthesizer _tts = null!; private bool _canSpeak; /// @@ -20,6 +19,11 @@ public class TextToSpeechCommunicator : ICommunicator, IDisposable /// public TextToSpeechCommunicator(TrackerOptionsAccessor trackerOptionsAccessor) { + if (!OperatingSystem.IsWindows()) + { + return; + } + _tts = new SpeechSynthesizer(); _tts.SelectVoiceByHints(VoiceGender.Female); _canSpeak = trackerOptionsAccessor.Options?.VoiceFrequency != Shared.Enums.TrackerVoiceFrequency.Disabled; @@ -29,13 +33,26 @@ public TextToSpeechCommunicator(TrackerOptionsAccessor trackerOptionsAccessor) /// Selects a different text-to-speech voice. /// public void UseAlternateVoice() - => _tts.SelectVoiceByHints(VoiceGender.Male); + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + _tts.SelectVoiceByHints(VoiceGender.Male); + } /// /// Aborts all current and queued up text-to-speech actions. /// public void Abort() - => _tts.SpeakAsyncCancelAll(); + { + if (!OperatingSystem.IsWindows()) + { + return; + } + _tts.SpeakAsyncCancelAll(); + } /// /// Speaks the specified text or SSML. @@ -45,9 +62,17 @@ public void Abort() /// public void Say(string text) { + if (!OperatingSystem.IsWindows()) + { + return; + } + if (!_canSpeak) return; var prompt = GetPromptFromText(text); - _tts.SpeakAsync(prompt); + if (prompt != null) + { + _tts.SpeakAsync(prompt); + } } /// @@ -59,9 +84,17 @@ public void Say(string text) /// public void SayWait(string text) { + if (!OperatingSystem.IsWindows()) + { + return; + } + if (!_canSpeak) return; var prompt = GetPromptFromText(text); - _tts.Speak(prompt); + if (prompt != null) + { + _tts.Speak(prompt); + } } /// @@ -69,6 +102,11 @@ public void SayWait(string text) /// public void SpeedUp() { + if (!OperatingSystem.IsWindows()) + { + return; + } + _tts.Rate += 2; } @@ -77,6 +115,11 @@ public void SpeedUp() /// public void SlowDown() { + if (!OperatingSystem.IsWindows()) + { + return; + } + _tts.Rate -= 2; } @@ -92,8 +135,13 @@ public void Dispose() /// /// The plain text or SSML markup to parse. /// A new . - protected static Prompt GetPromptFromText(string text) + protected static Prompt? GetPromptFromText(string text) { + if (!OperatingSystem.IsWindows()) + { + return null; + } + // If text does not contain any XML elements, just interpret it as // text if (!text.Contains("<") && !text.Contains("/>")) @@ -110,7 +158,7 @@ protected static Prompt GetPromptFromText(string text) /// protected virtual void Dispose(bool disposing) { - if (disposing) + if (disposing && OperatingSystem.IsWindows()) { _tts.Dispose(); } diff --git a/src/Randomizer.SMZ3.Tracking/Tracker.cs b/src/Randomizer.SMZ3.Tracking/Tracker.cs index 16379d0ee..63daffd4a 100644 --- a/src/Randomizer.SMZ3.Tracking/Tracker.cs +++ b/src/Randomizer.SMZ3.Tracking/Tracker.cs @@ -42,7 +42,7 @@ public class Tracker : IDisposable private const int RepeatRateModifier = 2; private static readonly Random s_random = new(); - private readonly SpeechRecognitionEngine _recognizer; + private readonly IWorldAccessor _worldAccessor; private readonly TrackerModuleFactory _moduleFactory; private readonly IChatClient _chatClient; @@ -55,6 +55,7 @@ public class Tracker : IDisposable private readonly ITrackerStateService _stateService; private readonly IWorldService _worldService; private readonly ITrackerTimerService _timerService; + private readonly ISpeechRecognitionService _recognizer; private bool _disposed; private string? _mood; private string? _lastSpokenText; @@ -64,13 +65,6 @@ public class Tracker : IDisposable private bool _beatenGame; private IEnumerable? _previousMissingItems; - /// - /// Dll to get the number of microphones - /// - /// - [DllImport("winmm.dll")] - private static extern int waveInGetNumDevs(); - /// /// Initializes a new instance of the class. /// @@ -107,7 +101,8 @@ public Tracker(ConfigProvider configProvider, IMetadataService metadataService, ITrackerStateService stateService, IWorldService worldService, - ITrackerTimerService timerService) + ITrackerTimerService timerService, + ISpeechRecognitionService speechRecognitionService) { if (trackerOptions.Options == null) throw new InvalidOperationException("Tracker options have not yet been activated."); @@ -145,7 +140,7 @@ public Tracker(ConfigProvider configProvider, } // Initialize the speech recognition engine - _recognizer = new SpeechRecognitionEngine(); + _recognizer = speechRecognitionService; _recognizer.SpeechRecognized += Recognizer_SpeechRecognized; InitializeMicrophone(); } @@ -413,23 +408,8 @@ public string CorrectUserNamePronunciation(string userName) public bool InitializeMicrophone() { if (MicrophoneInitialized) return true; - - try - { - if (waveInGetNumDevs() == 0) - { - _logger.LogWarning("No microphone device found."); - return false; - } - _recognizer.SetInputToDefaultAudioDevice(); - MicrophoneInitialized = true; - return true; - } - catch (Exception e) - { - _logger.LogError(e, "Error initializing microphone"); - return false; - } + MicrophoneInitialized = _recognizer.InitializeMicrophone(); + return MicrophoneInitialized; } /// @@ -457,12 +437,15 @@ public bool Load(GeneratedRom rom, string romPath) /// /// Saves the state of the tracker to the database /// - /// The rom to save /// - public async Task SaveAsync(GeneratedRom rom) + public async Task SaveAsync() { + if (Rom == null) + { + throw new NullReferenceException("No rom loaded into tracker"); + } IsDirty = false; - await _stateService.SaveStateAsync(_worldAccessor.Worlds, rom, _timerService.SecondsElapsed); + await _stateService.SaveStateAsync(_worldAccessor.Worlds, Rom, _timerService.SecondsElapsed); } /// @@ -711,7 +694,17 @@ public virtual bool TryStartTracking() { // Load the modules for voice recognition StartTimer(true); - Syntax = _moduleFactory.LoadAll(this, _recognizer, out var loadError); + + var loadError = false; + try + { + Syntax = _moduleFactory.LoadAll(this, _recognizer.RecognitionEngine, out loadError); + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load modules"); + loadError = true; + } try { diff --git a/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs b/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs index 4182b09be..56d05e26d 100644 --- a/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs +++ b/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs @@ -42,6 +42,15 @@ public static IServiceCollection AddTracker(this IServiceCollection services) services.AddScoped(); services.AddScoped(); + if (OperatingSystem.IsWindows()) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } + var assemblies = new[] { Assembly.GetExecutingAssembly() }; var zeldaStateChecks = assemblies @@ -63,7 +72,7 @@ public static IServiceCollection AddTracker(this IServiceCollection services) return services; } - + /// /// Enables the specified tracker module. diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs index b2ad59efe..fb1ee2c0c 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs @@ -27,11 +27,6 @@ public AutoTrackerModule(Tracker tracker, IItemService itemService, IWorldServic { Tracker.AutoTracker = autoTracker; _autoTracker = autoTracker; - - AddCommand("Look at this", GetLookAtGameRule(), (result) => - { - LookAtGame(); - }); } private GrammarBuilder GetLookAtGameRule() @@ -58,6 +53,14 @@ public void Dispose() { _autoTracker.SetConnector(EmulatorConnectorType.None, ""); } + + public override void AddCommands() + { + AddCommand("Look at this", GetLookAtGameRule(), (result) => + { + LookAtGame(); + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs index c6c58e50e..e1a197698 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Randomizer.Shared; @@ -21,24 +22,89 @@ public class BossTrackingModule : TrackerModule /// Used to write logging information. public BossTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) + { + + } + + private GrammarBuilder GetMarkBossAsDefeatedRule() + { + var bossNames = GetBossNames(); + var beatBoss = new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("track", "I beat", "I defeated", "I beat off", "I killed") + .Append(BossKey, bossNames); + + var markBoss = new GrammarBuilder() + .Append("Hey tracker,") + .Append("mark") + .Append(BossKey, bossNames) + .Append("as") + .OneOf("beaten", "beaten off", "dead", "fucking dead", "defeated"); + + var bossIsDead = new GrammarBuilder() + .Append("Hey tracker,") + .Append(BossKey, bossNames) + .OneOf("is dead", "is fucking dead"); + + return GrammarBuilder.Combine(beatBoss, markBoss, bossIsDead); + } + + private GrammarBuilder GetMarkBossAsNotDefeatedRule() + { + var bossNames = GetBossNames(); + + var markBoss = new GrammarBuilder() + .Append("Hey tracker,") + .Append("mark") + .Append(BossKey, bossNames) + .Append("as") + .OneOf("alive", "not defeated", "undefeated"); + + var bossIsAlive = new GrammarBuilder() + .Append("Hey tracker,") + .Append(BossKey, bossNames) + .OneOf("is alive", "is still alive"); + + return GrammarBuilder.Combine(markBoss, bossIsAlive); + } + + private GrammarBuilder GetBossDefeatedWithContentRule() + { + var bossNames = GetBossNames(); + var beatBoss = new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("I beat", "I defeated", "I beat off", "I killed") + .Append(BossKey, bossNames) + .OneOf("Without getting hit", "Without taking damage", "And didn't get hit", "And didn't take damage", "In one cycle"); + + var oneCycled = new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("I one cycled", "I quick killed") + .Append(BossKey, bossNames); + + return GrammarBuilder.Combine(beatBoss, oneCycled); + } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() { AddCommand("Mark boss as defeated", GetMarkBossAsDefeatedRule(), (result) => { - var dungeon = GetBossDungeonFromResult(tracker, result); + var dungeon = GetBossDungeonFromResult(Tracker, result); if (dungeon != null) { // Track boss with associated dungeon - tracker.MarkDungeonAsCleared(dungeon, result.Confidence); + Tracker.MarkDungeonAsCleared(dungeon, result.Confidence); return; } - var boss = GetBossFromResult(tracker, result); + var boss = GetBossFromResult(Tracker, result); if (boss != null) { // Track standalone boss var admittedGuilt = result.Text.ContainsAny("killed", "beat", "defeated", "dead") && !result.Text.ContainsAny("beat off", "beaten off"); - tracker.MarkBossAsDefeated(boss, admittedGuilt, result.Confidence); + Tracker.MarkBossAsDefeated(boss, admittedGuilt, result.Confidence); return; } @@ -47,19 +113,19 @@ public BossTrackingModule(Tracker tracker, IItemService itemService, IWorldServi AddCommand("Mark boss as alive", GetMarkBossAsNotDefeatedRule(), (result) => { - var dungeon = GetBossDungeonFromResult(tracker, result); + var dungeon = GetBossDungeonFromResult(Tracker, result); if (dungeon != null) { // Untrack boss with associated dungeon - tracker.MarkDungeonAsIncomplete(dungeon, result.Confidence); + Tracker.MarkDungeonAsIncomplete(dungeon, result.Confidence); return; } - var boss = GetBossFromResult(tracker, result); + var boss = GetBossFromResult(Tracker, result); if (boss != null) { // Untrack standalone boss - tracker.MarkBossAsNotDefeated(boss, result.Confidence); + Tracker.MarkBossAsNotDefeated(boss, result.Confidence); return; } @@ -68,9 +134,9 @@ public BossTrackingModule(Tracker tracker, IItemService itemService, IWorldServi AddCommand("Mark boss as defeated with content", GetBossDefeatedWithContentRule(), (result) => { - var contentItemData = itemService.FirstOrDefault("Content"); + var contentItemData = ItemService.FirstOrDefault("Content"); - var dungeon = GetBossDungeonFromResult(tracker, result); + var dungeon = GetBossDungeonFromResult(Tracker, result); if (dungeon != null) { if (contentItemData != null) @@ -80,11 +146,11 @@ public BossTrackingModule(Tracker tracker, IItemService itemService, IWorldServi } // Track boss with associated dungeon - tracker.MarkDungeonAsCleared(dungeon, result.Confidence); + Tracker.MarkDungeonAsCleared(dungeon, result.Confidence); return; } - var boss = GetBossFromResult(tracker, result); + var boss = GetBossFromResult(Tracker, result); if (boss != null) { if (contentItemData != null) @@ -96,71 +162,12 @@ public BossTrackingModule(Tracker tracker, IItemService itemService, IWorldServi // Track standalone boss var admittedGuilt = result.Text.ContainsAny("killed", "beat", "defeated", "dead") && !result.Text.ContainsAny("beat off", "beaten off"); - tracker.MarkBossAsDefeated(boss, admittedGuilt, result.Confidence); + Tracker.MarkBossAsDefeated(boss, admittedGuilt, result.Confidence); return; } throw new Exception($"Could not find boss or dungeon in command: '{result.Text}'"); }); } - - private GrammarBuilder GetMarkBossAsDefeatedRule() - { - var bossNames = GetBossNames(); - var beatBoss = new GrammarBuilder() - .Append("Hey tracker,") - .OneOf("track", "I beat", "I defeated", "I beat off", "I killed") - .Append(BossKey, bossNames); - - var markBoss = new GrammarBuilder() - .Append("Hey tracker,") - .Append("mark") - .Append(BossKey, bossNames) - .Append("as") - .OneOf("beaten", "beaten off", "dead", "fucking dead", "defeated"); - - var bossIsDead = new GrammarBuilder() - .Append("Hey tracker,") - .Append(BossKey, bossNames) - .OneOf("is dead", "is fucking dead"); - - return GrammarBuilder.Combine(beatBoss, markBoss, bossIsDead); - } - - private GrammarBuilder GetMarkBossAsNotDefeatedRule() - { - var bossNames = GetBossNames(); - - var markBoss = new GrammarBuilder() - .Append("Hey tracker,") - .Append("mark") - .Append(BossKey, bossNames) - .Append("as") - .OneOf("alive", "not defeated", "undefeated"); - - var bossIsAlive = new GrammarBuilder() - .Append("Hey tracker,") - .Append(BossKey, bossNames) - .OneOf("is alive", "is still alive"); - - return GrammarBuilder.Combine(markBoss, bossIsAlive); - } - - private GrammarBuilder GetBossDefeatedWithContentRule() - { - var bossNames = GetBossNames(); - var beatBoss = new GrammarBuilder() - .Append("Hey tracker,") - .OneOf("I beat", "I defeated", "I beat off", "I killed") - .Append(BossKey, bossNames) - .OneOf("Without getting hit", "Without taking damage", "And didn't get hit", "And didn't take damage", "In one cycle"); - - var oneCycled = new GrammarBuilder() - .Append("Hey tracker,") - .OneOf("I one cycled", "I quick killed") - .Append(BossKey, bossNames); - - return GrammarBuilder.Combine(beatBoss, oneCycled); - } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs index b6416b717..1ea0acef3 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Speech.Recognition; using System.Text.RegularExpressions; @@ -54,27 +55,6 @@ public ChatIntegrationModule(Tracker tracker, IChatClient chatClient, IItemServi ChatClient.MessageReceived += ChatClient_MessageReceived; ChatClient.Disconnected += ChatClient_Disconnected; ChatClient.SendMessageFailure += ChatClient_SendMessageFailure; - - AddCommand("Start Ganon's Tower Big Key Guessing Game", GetStartGuessingGameRule(), async (result) => - { - await StartGanonsTowerGuessingGame(); - }); - - AddCommand("Close Ganon's Tower Big Key Guessing Game", GetStopGuessingGameGuessesRule(), async (result) => - { - await CloseGanonsTowerGuessingGameGuesses(); - }); - - AddCommand("Declare Ganon's Tower Big Key Guessing Game Winner", GetRevealGuessingGameWinnerRule(), async (result) => - { - var winningNumber = (int)result.Semantics[WinningGuessKey].Value; - await DeclareGanonsTowerGuessingGameWinner(winningNumber); - }); - - AddCommand("Track Content", GetTrackContent(), async (result) => - { - await AskChatAboutContent(); - }); } /// @@ -546,5 +526,30 @@ private GrammarBuilder GetTrackContent() .OneOf("track", "add") .OneOf("content", "con-tent"); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + AddCommand("Start Ganon's Tower Big Key Guessing Game", GetStartGuessingGameRule(), async (result) => + { + await StartGanonsTowerGuessingGame(); + }); + + AddCommand("Close Ganon's Tower Big Key Guessing Game", GetStopGuessingGameGuessesRule(), async (result) => + { + await CloseGanonsTowerGuessingGameGuesses(); + }); + + AddCommand("Declare Ganon's Tower Big Key Guessing Game Winner", GetRevealGuessingGameWinnerRule(), async (result) => + { + var winningNumber = (int)result.Semantics[WinningGuessKey].Value; + await DeclareGanonsTowerGuessingGameWinner(winningNumber); + }); + + AddCommand("Track Content", GetTrackContent(), async (result) => + { + await AskChatAboutContent(); + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs index bf732bd9b..5d5c041c9 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Speech.Recognition; using Microsoft.Extensions.Logging; @@ -36,59 +37,7 @@ public class CheatsModule : TrackerModule public CheatsModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { - if (tracker.World.Config.Race || tracker.World.Config.DisableCheats) return; - AddCommand("Enable cheats", GetEnableCheatsRule(), (result) => - { - _cheatsEnabled = true; - Tracker.Say(x => x.Cheats.EnabledCheats); - }); - - AddCommand("Disable cheats", GetDisableHintsRule(), (result) => - { - _cheatsEnabled = false; - Tracker.Say(x => x.Cheats.DisabledCheats); - }); - - AddCommand("Fill rule", FillRule(), (result) => - { - var fillType = result.Semantics.ContainsKey(s_fillCheatKey) ? (string)result.Semantics[s_fillCheatKey].Value : s_fillHealthChoices.First(); - Fill(fillType); - }); - - AddCommand("Give item", GiveItemRule(), (result) => - { - var item = GetItemFromResult(tracker, result, out var itemName); - GiveItem(item); - }); - - AddCommand("Kill player", KillPlayerRule(), (result) => - { - if (!PlayerCanCheat()) return; - - if (Tracker.GameService?.TryKillPlayer() == true) - { - Tracker.Say(x => x.Cheats.CheatPerformed); - } - else - { - Tracker.Say(x => x.Cheats.CheatFailed); - } - }); - - AddCommand("Setup Crystal Flash", SetupCrystalFlashRule(), (result) => - { - if (!PlayerCanCheat()) return; - - if (Tracker.GameService?.TrySetupCrystalFlash() == true) - { - Tracker.Say(x => x.Cheats.CheatPerformed); - } - else - { - Tracker.Say(x => x.Cheats.CheatFailed); - } - }); } private bool PlayerCanCheat() @@ -192,6 +141,7 @@ private static GrammarBuilder GetDisableHintsRule() .OneOf("cheats", "cheat codes", "sv_cheats"); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private static GrammarBuilder FillRule() { var fillChoices = new Choices(); @@ -244,6 +194,64 @@ private GrammarBuilder SetupCrystalFlashRule() .Optional("please", "would you kindly") .OneOf("setup crystal flash requirements", "ready a crystal flash"); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + if (Tracker.World.Config.Race || Tracker.World.Config.DisableCheats) return; + + AddCommand("Enable cheats", GetEnableCheatsRule(), (result) => + { + _cheatsEnabled = true; + Tracker.Say(x => x.Cheats.EnabledCheats); + }); + + AddCommand("Disable cheats", GetDisableHintsRule(), (result) => + { + _cheatsEnabled = false; + Tracker.Say(x => x.Cheats.DisabledCheats); + }); + + AddCommand("Fill rule", FillRule(), (result) => + { + var fillType = result.Semantics.ContainsKey(s_fillCheatKey) ? (string)result.Semantics[s_fillCheatKey].Value : s_fillHealthChoices.First(); + Fill(fillType); + }); + + AddCommand("Give item", GiveItemRule(), (result) => + { + var item = GetItemFromResult(Tracker, result, out var itemName); + GiveItem(item); + }); + + AddCommand("Kill player", KillPlayerRule(), (result) => + { + if (!PlayerCanCheat()) return; + + if (Tracker.GameService?.TryKillPlayer() == true) + { + Tracker.Say(x => x.Cheats.CheatPerformed); + } + else + { + Tracker.Say(x => x.Cheats.CheatFailed); + } + }); + + AddCommand("Setup Crystal Flash", SetupCrystalFlashRule(), (result) => + { + if (!PlayerCanCheat()) return; + + if (Tracker.GameService?.TrySetupCrystalFlash() == true) + { + Tracker.Say(x => x.Cheats.CheatPerformed); + } + else + { + Tracker.Say(x => x.Cheats.CheatFailed); + } + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs index aa823913c..02bfbb8da 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.SMZ3.Tracking.Services; @@ -11,6 +11,7 @@ namespace Randomizer.SMZ3.Tracking.VoiceCommands /// public class GoModeModule : TrackerModule { + private ResponseConfig _responseConfig; /// /// Initializes a new instance of the class. @@ -23,10 +24,7 @@ public class GoModeModule : TrackerModule public GoModeModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, ResponseConfig responseConfig) : base(tracker, itemService, worldService, logger) { - AddCommand("Toggle Go Mode", GetGoModeRule(responseConfig.GoModePrompts), (result) => - { - tracker.ToggleGoMode(result.Confidence); - }); + _responseConfig = responseConfig; } private GrammarBuilder GetGoModeRule(List prompts) @@ -35,5 +33,14 @@ private GrammarBuilder GetGoModeRule(List prompts) .Append("Hey tracker,") .OneOf(prompts.ToArray()); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + AddCommand("Toggle Go Mode", GetGoModeRule(_responseConfig.GoModePrompts), (result) => + { + Tracker.ToggleGoMode(result.Confidence); + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/GrammarBuilder.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/GrammarBuilder.cs index 833403aae..436c8b1fa 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/GrammarBuilder.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/GrammarBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Speech.Recognition; @@ -9,7 +10,7 @@ namespace Randomizer.SMZ3.Tracking.VoiceCommands /// public class GrammarBuilder { - private readonly System.Speech.Recognition.GrammarBuilder _grammar; + private readonly System.Speech.Recognition.GrammarBuilder _grammar = null!; private readonly List _elements; /// @@ -18,7 +19,10 @@ public class GrammarBuilder /// public GrammarBuilder() { - _grammar = new(); + if (OperatingSystem.IsWindows()) + { + _grammar = new(); + } _elements = new(); } @@ -30,6 +34,10 @@ public GrammarBuilder() public GrammarBuilder(IEnumerable choices) : this() { + if (!OperatingSystem.IsWindows()) + { + return; + } _grammar.Append(new Choices(choices.Select(x => (System.Speech.Recognition.GrammarBuilder)x).ToArray())); foreach (var choice in choices) _elements.Add(choice + "\n"); @@ -63,6 +71,10 @@ public static GrammarBuilder Combine(params GrammarBuilder[] choices) /// This instance. public GrammarBuilder Append(string text) { + if (!OperatingSystem.IsWindows()) + { + return this; + } _grammar.Append(text); _elements.Add(text); return this; @@ -81,6 +93,10 @@ public GrammarBuilder Append(string text) /// This instance. public GrammarBuilder Append(string key, Choices choices) { + if (!OperatingSystem.IsWindows()) + { + return this; + } _grammar.Append(new SemanticResultKey(key, choices)); _elements.Add($"<{key}>"); return this; @@ -95,6 +111,10 @@ public GrammarBuilder Append(string key, Choices choices) /// This instance. public GrammarBuilder OneOf(params string[] choices) { + if (!OperatingSystem.IsWindows()) + { + return this; + } _grammar.Append(new Choices(choices)); _elements.Add($"[{string.Join('/', choices)}]"); return this; @@ -109,6 +129,10 @@ public GrammarBuilder OneOf(params string[] choices) /// This instance. public GrammarBuilder Optional(string text) { + if (!OperatingSystem.IsWindows()) + { + return this; + } _grammar.Append(text, 0, 1); _elements.Add($"({text})"); return this; @@ -121,6 +145,10 @@ public GrammarBuilder Optional(string text) /// This instance. public GrammarBuilder Optional(params string[] choices) { + if (!OperatingSystem.IsWindows()) + { + return this; + } _grammar.Append(new Choices(choices), 0, 1); _elements.Add($"({string.Join('/', choices)})"); return this; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs index 11b36a87e..bb7c4cf38 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs @@ -1,4 +1,5 @@ -using System.Speech.Recognition; +using System.Diagnostics.CodeAnalysis; +using System.Speech.Recognition; using Microsoft.Extensions.Logging; @@ -24,112 +25,7 @@ public class ItemTrackingModule : TrackerModule public ItemTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { - var isMultiworld = worldService.World.Config.MultiWorld; - AddCommand("Track item", GetTrackItemRule(isMultiworld), (result) => - { - var item = GetItemFromResult(tracker, result, out var itemName); - - if (result.Semantics.ContainsKey(DungeonKey)) - { - var dungeon = GetDungeonFromResult(tracker, result); - tracker.TrackItem(item, dungeon, - trackedAs: itemName, - confidence: result.Confidence); - } - else if (result.Semantics.ContainsKey(RoomKey)) - { - var room = GetRoomFromResult(tracker, result); - tracker.TrackItem(item, room, - trackedAs: itemName, - confidence: result.Confidence); - } - else if (result.Semantics.ContainsKey(LocationKey)) - { - var location = GetLocationFromResult(tracker, result); - tracker.TrackItem(item: item, - trackedAs: itemName, - confidence: result.Confidence, - tryClear: true, - autoTracked: false, - location: location); - } - else - { - tracker.TrackItem(item, - trackedAs: itemName, - confidence: result.Confidence); - } - }); - - AddCommand("Track death", GetTrackDeathRule(), (result) => - { - var death = itemService.FirstOrDefault("Death"); - if (death == null) - { - Logger.LogError("Tried to track death, but could not find an item named 'Death'."); - tracker.Say(x => x.Error); - return; - } - - tracker.TrackItem(death, confidence: result.Confidence, tryClear: false); - }); - - if (!isMultiworld) - { - AddCommand("Track available items in an area", GetTrackEverythingRule(), (result) => - { - if (result.Semantics.ContainsKey(RoomKey)) - { - var room = GetRoomFromResult(tracker, result); - tracker.ClearArea(room, - trackItems: true, - includeUnavailable: false, - confidence: result.Confidence); - } - else if (result.Semantics.ContainsKey(RegionKey)) - { - var region = GetRegionFromResult(tracker, result); - tracker.ClearArea(region, - trackItems: true, - includeUnavailable: false, - confidence: result.Confidence); - } - }); - - AddCommand("Track all items in an area (including out-of-logic)", GetTrackEverythingIncludingOutOfLogicRule(), (result) => - { - if (result.Semantics.ContainsKey(RoomKey)) - { - var room = GetRoomFromResult(tracker, result); - tracker.ClearArea(room, - trackItems: true, - includeUnavailable: true, - confidence: result.Confidence); - } - else if (result.Semantics.ContainsKey(RegionKey)) - { - var region = GetRegionFromResult(tracker, result); - tracker.ClearArea(region, - trackItems: true, - includeUnavailable: true, - confidence: result.Confidence); - } - }); - } - - AddCommand("Untrack an item", GetUntrackItemRule(), (result) => - { - var item = GetItemFromResult(tracker, result, out _); - tracker.UntrackItem(item, result.Confidence); - }); - - AddCommand("Set item count", GetSetItemCountRule(), (result) => - { - var item = GetItemFromResult(tracker, result, out _); - var count = (int)result.Semantics[ItemCountKey].Value; - tracker.TrackItemAmount(item, count, result.Confidence); - }); } private GrammarBuilder GetTrackDeathRule() @@ -269,6 +165,7 @@ private GrammarBuilder GetUntrackItemRule() return GrammarBuilder.Combine(untrackItem, toggleItemOff); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private GrammarBuilder GetSetItemCountRule() { var itemNames = GetPluralItemNames(); @@ -282,5 +179,116 @@ private GrammarBuilder GetSetItemCountRule() .Append(ItemCountKey, numbers) .Append(ItemNameKey, itemNames); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + var isMultiworld = WorldService.World.Config.MultiWorld; + + AddCommand("Track item", GetTrackItemRule(isMultiworld), (result) => + { + var item = GetItemFromResult(Tracker, result, out var itemName); + + if (result.Semantics.ContainsKey(DungeonKey)) + { + var dungeon = GetDungeonFromResult(Tracker, result); + Tracker.TrackItem(item, dungeon, + trackedAs: itemName, + confidence: result.Confidence); + } + else if (result.Semantics.ContainsKey(RoomKey)) + { + var room = GetRoomFromResult(Tracker, result); + Tracker.TrackItem(item, room, + trackedAs: itemName, + confidence: result.Confidence); + } + else if (result.Semantics.ContainsKey(LocationKey)) + { + var location = GetLocationFromResult(Tracker, result); + Tracker.TrackItem(item: item, + trackedAs: itemName, + confidence: result.Confidence, + tryClear: true, + autoTracked: false, + location: location); + } + else + { + Tracker.TrackItem(item, + trackedAs: itemName, + confidence: result.Confidence); + } + }); + + AddCommand("Track death", GetTrackDeathRule(), (result) => + { + var death = ItemService.FirstOrDefault("Death"); + if (death == null) + { + Logger.LogError("Tried to track death, but could not find an item named 'Death'."); + Tracker.Say(x => x.Error); + return; + } + + Tracker.TrackItem(death, confidence: result.Confidence, tryClear: false); + }); + + if (!isMultiworld) + { + AddCommand("Track available items in an area", GetTrackEverythingRule(), (result) => + { + if (result.Semantics.ContainsKey(RoomKey)) + { + var room = GetRoomFromResult(Tracker, result); + Tracker.ClearArea(room, + trackItems: true, + includeUnavailable: false, + confidence: result.Confidence); + } + else if (result.Semantics.ContainsKey(RegionKey)) + { + var region = GetRegionFromResult(Tracker, result); + Tracker.ClearArea(region, + trackItems: true, + includeUnavailable: false, + confidence: result.Confidence); + } + }); + + AddCommand("Track all items in an area (including out-of-logic)", GetTrackEverythingIncludingOutOfLogicRule(), (result) => + { + if (result.Semantics.ContainsKey(RoomKey)) + { + var room = GetRoomFromResult(Tracker, result); + Tracker.ClearArea(room, + trackItems: true, + includeUnavailable: true, + confidence: result.Confidence); + } + else if (result.Semantics.ContainsKey(RegionKey)) + { + var region = GetRegionFromResult(Tracker, result); + Tracker.ClearArea(region, + trackItems: true, + includeUnavailable: true, + confidence: result.Confidence); + } + }); + } + + AddCommand("Untrack an item", GetUntrackItemRule(), (result) => + { + var item = GetItemFromResult(Tracker, result, out _); + Tracker.UntrackItem(item, result.Confidence); + }); + + AddCommand("Set item count", GetSetItemCountRule(), (result) => + { + var item = GetItemFromResult(Tracker, result, out _); + var count = (int)result.Semantics[ItemCountKey].Value; + Tracker.TrackItemAmount(item, count, result.Confidence); + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs index 1edb29b27..fa845ceab 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; using Randomizer.SMZ3.Tracking.Services; @@ -20,68 +21,7 @@ public class LocationTrackingModule : TrackerModule public LocationTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { - AddCommand("Mark item at specific location", GetMarkItemAtLocationRule(), (result) => - { - var item = GetItemFromResult(tracker, result, out _); - var location = GetLocationFromResult(tracker, result); - tracker.MarkLocation(location, item, result.Confidence); - }); - - AddCommand("Clear specific item location", GetClearLocationRule(), (result) => - { - var location = GetLocationFromResult(tracker, result); - tracker.Clear(location, result.Confidence); - }); - - AddCommand("Clear available items in an area", GetClearAreaRule(), (result) => - { - if (result.Semantics.ContainsKey(RoomKey)) - { - var room = GetRoomFromResult(tracker, result); - tracker.ClearArea(room, - trackItems: false, - includeUnavailable: false, - confidence: result.Confidence); - } - else if (result.Semantics.ContainsKey(DungeonKey)) - { - var dungeon = GetDungeonFromResult(tracker, result); - tracker.ClearDungeon(dungeon, result.Confidence); - } - else if (result.Semantics.ContainsKey(RegionKey)) - { - var region = GetRegionFromResult(tracker, result); - tracker.ClearArea(region, - trackItems:false, - includeUnavailable: false, - confidence: result.Confidence); - } - }); - AddCommand("Clear all items in an area (including out-of-logic)", GetClearAreaIncludingOutOfLogicRule(), (result) => - { - if (result.Semantics.ContainsKey(RoomKey)) - { - var room = GetRoomFromResult(tracker, result); - tracker.ClearArea(room, - trackItems: false, - includeUnavailable: true, - confidence: result.Confidence); - } - else if (result.Semantics.ContainsKey(DungeonKey)) - { - var dungeon = GetDungeonFromResult(tracker, result); - tracker.ClearDungeon(dungeon, result.Confidence); - } - else if (result.Semantics.ContainsKey(RegionKey)) - { - var region = GetRegionFromResult(tracker, result); - tracker.ClearArea(region, - trackItems:false, - includeUnavailable: true, - confidence: result.Confidence); - } - }); } private GrammarBuilder GetMarkItemAtLocationRule() @@ -187,5 +127,72 @@ private GrammarBuilder GetClearAreaIncludingOutOfLogicRule() return GrammarBuilder.Combine(clearDungeon, clearRoom, clearRegion); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + AddCommand("Mark item at specific location", GetMarkItemAtLocationRule(), (result) => + { + var item = GetItemFromResult(Tracker, result, out _); + var location = GetLocationFromResult(Tracker, result); + Tracker.MarkLocation(location, item, result.Confidence); + }); + + AddCommand("Clear specific item location", GetClearLocationRule(), (result) => + { + var location = GetLocationFromResult(Tracker, result); + Tracker.Clear(location, result.Confidence); + }); + + AddCommand("Clear available items in an area", GetClearAreaRule(), (result) => + { + if (result.Semantics.ContainsKey(RoomKey)) + { + var room = GetRoomFromResult(Tracker, result); + Tracker.ClearArea(room, + trackItems: false, + includeUnavailable: false, + confidence: result.Confidence); + } + else if (result.Semantics.ContainsKey(DungeonKey)) + { + var dungeon = GetDungeonFromResult(Tracker, result); + Tracker.ClearDungeon(dungeon, result.Confidence); + } + else if (result.Semantics.ContainsKey(RegionKey)) + { + var region = GetRegionFromResult(Tracker, result); + Tracker.ClearArea(region, + trackItems:false, + includeUnavailable: false, + confidence: result.Confidence); + } + }); + + AddCommand("Clear all items in an area (including out-of-logic)", GetClearAreaIncludingOutOfLogicRule(), (result) => + { + if (result.Semantics.ContainsKey(RoomKey)) + { + var room = GetRoomFromResult(Tracker, result); + Tracker.ClearArea(room, + trackItems: false, + includeUnavailable: true, + confidence: result.Confidence); + } + else if (result.Semantics.ContainsKey(DungeonKey)) + { + var dungeon = GetDungeonFromResult(Tracker, result); + Tracker.ClearDungeon(dungeon, result.Confidence); + } + else if (result.Semantics.ContainsKey(RegionKey)) + { + var region = GetRegionFromResult(Tracker, result); + Tracker.ClearArea(region, + trackItems:false, + includeUnavailable: true, + confidence: result.Confidence); + } + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs index 8663ba9c3..8da986550 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs @@ -28,8 +28,58 @@ public MapModule(Tracker tracker, IItemService itemService, ILogger l { _logger = logger; _config = config; + } + + private GrammarBuilder GetChangeMapRule() + { + var dungeonNames = GetDungeonNames(includeDungeonsWithoutReward: true); + var itemNames = GetItemNames(x => x.Name != "Content"); + var locationNames = GetLocationNames(); + var roomNames = GetRoomNames(); + + var maps = new Choices(); + foreach (var map in _config.Maps) + { + foreach (var name in map.Name) + { + maps.Add(new SemanticResultValue(name, map.ToString())); + } + } + + var version1 = new GrammarBuilder() + .Append("Hey tracker,") + .Optional("please", "would you kindly") + .OneOf("update my map to", "change my map to") + .Append(MapKey, maps); + + var version2 = new GrammarBuilder() + .Append("Hey tracker,") + .Optional("please", "would you kindly") + .OneOf("show me") + .Optional("the") + .Append(MapKey, maps) + .Optional("map"); + + return GrammarBuilder.Combine(version1, version2); + } + + private GrammarBuilder DarkRoomRule() + { + return new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("it's dark in here", "I can't see", "show me this dark room map"); + } + + private GrammarBuilder CanSeeRule() + { + return new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("I can see now", "I can see clearly now", "it's no longer dark", "I'm out of the dark room", "stop showing me the dark room map"); + } - var darkRoomMaps = config.Maps.Where(x => x.IsDarkRoomMap == true && x.MemoryRoomNumbers?.Count > 0).ToList(); + public override void AddCommands() + { + var darkRoomMaps = _config.Maps.Where(x => x.IsDarkRoomMap == true && x.MemoryRoomNumbers?.Count > 0).ToList(); AddCommand("Update map", GetChangeMapRule(), (result) => { @@ -53,7 +103,7 @@ public MapModule(Tracker tracker, IItemService itemService, ILogger l if (map != null) { - if (itemService.IsTracked(Shared.ItemType.Lamp)) + if (ItemService.IsTracked(Shared.ItemType.Lamp)) { Tracker.Say(x => x.Map.HasLamp); return; @@ -87,52 +137,5 @@ public MapModule(Tracker tracker, IItemService itemService, ILogger l } }); } - - private GrammarBuilder GetChangeMapRule() - { - var dungeonNames = GetDungeonNames(includeDungeonsWithoutReward: true); - var itemNames = GetItemNames(x => x.Name != "Content"); - var locationNames = GetLocationNames(); - var roomNames = GetRoomNames(); - - var maps = new Choices(); - foreach (var map in _config.Maps) - { - foreach (var name in map.Name) - { - maps.Add(new SemanticResultValue(name, map.ToString())); - } - } - - var version1 = new GrammarBuilder() - .Append("Hey tracker,") - .Optional("please", "would you kindly") - .OneOf("update my map to", "change my map to") - .Append(MapKey, maps); - - var version2 = new GrammarBuilder() - .Append("Hey tracker,") - .Optional("please", "would you kindly") - .OneOf("show me") - .Optional("the") - .Append(MapKey, maps) - .Optional("map"); - - return GrammarBuilder.Combine(version1, version2); - } - - private GrammarBuilder DarkRoomRule() - { - return new GrammarBuilder() - .Append("Hey tracker,") - .OneOf("it's dark in here", "I can't see", "show me this dark room map"); - } - - private GrammarBuilder CanSeeRule() - { - return new GrammarBuilder() - .Append("Hey tracker,") - .OneOf("I can see now", "I can see clearly now", "it's no longer dark", "I'm out of the dark room", "stop showing me the dark room map"); - } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs index ae91e9bc9..f78967682 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Speech.Recognition; using Microsoft.Extensions.Logging; @@ -12,6 +13,7 @@ namespace Randomizer.SMZ3.Tracking.VoiceCommands /// public class MetaModule : TrackerModule { + private readonly ICommunicator _communicator; private const string ModifierKey = "Increase/Decrease"; private const string ThresholdSettingKey = "ThresholdSetting"; private const string ValueKey = "Value"; @@ -30,100 +32,10 @@ public class MetaModule : TrackerModule public MetaModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, ICommunicator communicator) : base(tracker, itemService, worldService, logger) { - AddCommand("Repeat that", GetRepeatThatRule(), (result) => - { - tracker.Repeat(); - }); - - AddCommand("Shut up", GetShutUpRule(), (result) => - { - tracker.ShutUp(); - }); - - AddCommand("Temporarily change threshold setting", GetIncreaseThresholdGrammar(), (result) => - { - var modifier = (int)result.Semantics[ModifierKey].Value; - var thresholdSetting = (int)result.Semantics[ThresholdSettingKey].Value; - var value = (float)(double)result.Semantics[ValueKey].Value; - - var adjustment = modifier * value; - switch (thresholdSetting) - { - case ThresholdSetting_Recognition: - Tracker.Options.MinimumRecognitionConfidence += adjustment; - Tracker.Say(Tracker.Responses.TrackerSettingChanged.Format( - "recognition threshold", $"{Tracker.Options.MinimumRecognitionConfidence:P0}")); - logger.LogInformation("Temporarily changed recognition threshold to {newValue}", Tracker.Options.MinimumRecognitionConfidence); - break; - - case ThresholdSetting_Execution: - Tracker.Options.MinimumExecutionConfidence += adjustment; - Tracker.Say(Tracker.Responses.TrackerSettingChanged.Format( - "execution threshold", $"{Tracker.Options.MinimumExecutionConfidence:P0}")); - logger.LogInformation("Temporarily changed execution threshold to {newValue}", Tracker.Options.MinimumExecutionConfidence); - break; - - case ThresholdSetting_Sass: - Tracker.Options.MinimumSassConfidence += adjustment; - Tracker.Say(Tracker.Responses.TrackerSettingChanged.Format( - "sass threshold", $"{Tracker.Options.MinimumSassConfidence:P0}")); - logger.LogInformation("Temporarily changed sass threshold to {newValue}", Tracker.Options.MinimumSassConfidence); - break; - - default: - throw new ArgumentException($"The threshold setting '{thresholdSetting}' was not recognized."); - } - }); - - AddCommand("Pause timer", GetPauseTimerRule(), (result) => - { - tracker.PauseTimer(); - }); - - AddCommand("Start timer", GetResumeTimerRule(), (result) => - { - tracker.StartTimer(); - }); - - AddCommand("Reset timer", GetResetTimerRule(), (result) => - { - tracker.ResetTimer(); - }); - - AddCommand("Mute", GetMuteRule(), (result) => - { - if (communicator.IsEnabled) - { - tracker.Say(x => x.Muted); - communicator.Disable(); - tracker.AddUndo(() => - { - communicator.Enable(); - Tracker.Say(x => x.ActionUndone); - }); - } - - }); - - AddCommand("Unmute", GetUnmuteRule(), (result) => - { - if (!communicator.IsEnabled) - { - communicator.Enable(); - tracker.Say(x => x.Unmuted); - tracker.AddUndo(() => - { - communicator.Disable(); - }); - } - }); - - AddCommand("Beat game", GetBeatGameRule(), (result) => - { - tracker.GameBeaten(false); - }); + _communicator = communicator; } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private static Choices GetIncreaseDecrease() { var modifiers = new Choices(); @@ -132,6 +44,7 @@ private static Choices GetIncreaseDecrease() return modifiers; } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private static Choices GetOneThroughTenPercent() { var values = new Choices(); @@ -148,6 +61,7 @@ private static Choices GetOneThroughTenPercent() return values; } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private static Choices GetThresholdSettings() { var settings = new Choices(); @@ -238,5 +152,102 @@ private GrammarBuilder GetBeatGameRule() .OneOf("I beat", "I finished") .OneOf("the game", "the seed"); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + AddCommand("Repeat that", GetRepeatThatRule(), (_) => + { + Tracker.Repeat(); + }); + + AddCommand("Shut up", GetShutUpRule(), (_) => + { + Tracker.ShutUp(); + }); + + AddCommand("Temporarily change threshold setting", GetIncreaseThresholdGrammar(), (result) => + { + var modifier = (int)result.Semantics[ModifierKey].Value; + var thresholdSetting = (int)result.Semantics[ThresholdSettingKey].Value; + var value = (float)(double)result.Semantics[ValueKey].Value; + + var adjustment = modifier * value; + switch (thresholdSetting) + { + case ThresholdSetting_Recognition: + Tracker.Options.MinimumRecognitionConfidence += adjustment; + Tracker.Say(Tracker.Responses.TrackerSettingChanged.Format( + "recognition threshold", $"{Tracker.Options.MinimumRecognitionConfidence:P0}")); + Logger.LogInformation("Temporarily changed recognition threshold to {newValue}", Tracker.Options.MinimumRecognitionConfidence); + break; + + case ThresholdSetting_Execution: + Tracker.Options.MinimumExecutionConfidence += adjustment; + Tracker.Say(Tracker.Responses.TrackerSettingChanged.Format( + "execution threshold", $"{Tracker.Options.MinimumExecutionConfidence:P0}")); + Logger.LogInformation("Temporarily changed execution threshold to {newValue}", Tracker.Options.MinimumExecutionConfidence); + break; + + case ThresholdSetting_Sass: + Tracker.Options.MinimumSassConfidence += adjustment; + Tracker.Say(Tracker.Responses.TrackerSettingChanged.Format( + "sass threshold", $"{Tracker.Options.MinimumSassConfidence:P0}")); + Logger.LogInformation("Temporarily changed sass threshold to {newValue}", Tracker.Options.MinimumSassConfidence); + break; + + default: + throw new ArgumentException($"The threshold setting '{thresholdSetting}' was not recognized."); + } + }); + + AddCommand("Pause timer", GetPauseTimerRule(), (_) => + { + Tracker.PauseTimer(); + }); + + AddCommand("Start timer", GetResumeTimerRule(), (_) => + { + Tracker.StartTimer(); + }); + + AddCommand("Reset timer", GetResetTimerRule(), (_) => + { + Tracker.ResetTimer(); + }); + + AddCommand("Mute", GetMuteRule(), (_) => + { + if (_communicator.IsEnabled) + { + Tracker.Say(x => x.Muted); + _communicator.Disable(); + Tracker.AddUndo(() => + { + _communicator.Enable(); + Tracker.Say(x => x.ActionUndone); + }); + } + + }); + + AddCommand("Unmute", GetUnmuteRule(), (_) => + { + if (!_communicator.IsEnabled) + { + _communicator.Enable(); + Tracker.Say(x => x.Unmuted); + Tracker.AddUndo(() => + { + _communicator.Disable(); + }); + } + }); + + AddCommand("Beat game", GetBeatGameRule(), (_) => + { + Tracker.GameBeaten(false); + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs index 46e6049f9..6d142670d 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Speech.Recognition; using System.Timers; @@ -9,7 +10,6 @@ using MSURandomizerLibrary.Models; using MSURandomizerLibrary.Services; using Randomizer.Data.Configuration.ConfigFiles; -using Randomizer.Data.Configuration.ConfigTypes; using Randomizer.Data.Options; using Randomizer.SMZ3.Tracking.Services; @@ -27,8 +27,8 @@ public class MsuModule : TrackerModule, IDisposable private readonly Timer? _timer; private readonly MsuType? _msuType; private readonly MsuConfig _msuConfig; - private readonly string MsuKey = "MsuKey"; - private int _currentTrackNumber = 0; + private readonly string _msuKey = "MsuKey"; + private int _currentTrackNumber; private readonly HashSet _validTrackNumbers; private Track? _currentTrack; @@ -91,65 +91,6 @@ public MsuModule(Tracker tracker, IItemService itemService, IWorldService worldS tracker.TrackNumberUpdated += TrackerOnTrackNumberUpdated; - 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) - { - Tracker.Say(_msuConfig.CurrentSong, GetTrackText(track)); - } - 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); - } - }); - - AddCommand("current song", GetCurrentSongRules(), (result) => - { - if (_currentTrack == null) - { - Tracker.Say(_msuConfig.UnknownSong); - return; - } - Tracker.Say(_msuConfig.CurrentSong, GetTrackText(_currentTrack)); - }); - - AddCommand("current msu", GetCurrentMsuRules(), (result) => - { - if (_currentTrack == null) - { - Tracker.Say(_msuConfig.UnknownSong); - return; - } - Tracker.Say(_msuConfig.CurrentMsu, _currentTrack.GetMsuName()); - }); } private void TrackerOnTrackNumberUpdated(object? sender, TrackNumberEventArgs e) @@ -177,7 +118,7 @@ private void TrackerOnTrackNumberUpdated(object? sender, TrackNumberEventArgs e) } } - Logger.LogInformation("Current Track: {Track}", _currentTrack?.GetDisplayText(true) ?? "Unknown"); + Logger.LogInformation("Current Track: {Track}", _currentTrack?.GetDisplayText() ?? "Unknown"); } private string GetOutputText() @@ -232,6 +173,7 @@ private string GetOutputText() } } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private GrammarBuilder GetLocationSongRules() { var msuLocations = new Choices(); @@ -248,18 +190,19 @@ private GrammarBuilder GetLocationSongRules() .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); + .Append(_msuKey, msuLocations); var option2 = new GrammarBuilder() .Append("Hey tracker,") .OneOf("what's the current", "what's the") - .Append(MsuKey, msuLocations) + .Append(_msuKey, msuLocations) .OneOf("song", "theme"); return GrammarBuilder.Combine(option1, option2); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private GrammarBuilder GetLocationMsuRules() { var msuLocations = new Choices(); @@ -277,14 +220,14 @@ private GrammarBuilder GetLocationMsuRules() .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(_msuKey, msuLocations) .Append("from"); var option2 = new GrammarBuilder() .Append("Hey tracker,") .Append("what MSU pack is") .OneOf("the current", "the") - .Append(MsuKey, msuLocations) + .Append(_msuKey, msuLocations) .OneOf("song", "theme") .Append("from"); @@ -312,6 +255,7 @@ private string GetTrackText(Track track) return string.Join("; ", parts); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private GrammarBuilder GetCurrentSongRules() { var msuLocations = new Choices(); @@ -364,4 +308,68 @@ public void Dispose() _timer.Dispose(); } } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + 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) + { + Tracker.Say(_msuConfig.CurrentSong, GetTrackText(track)); + } + 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); + } + }); + + AddCommand("current song", GetCurrentSongRules(), (_) => + { + if (_currentTrack == null) + { + Tracker.Say(_msuConfig.UnknownSong); + return; + } + Tracker.Say(_msuConfig.CurrentSong, GetTrackText(_currentTrack)); + }); + + AddCommand("current msu", GetCurrentMsuRules(), (_) => + { + if (_currentTrack == null) + { + Tracker.Say(_msuConfig.UnknownSong); + return; + } + Tracker.Say(_msuConfig.CurrentMsu, _currentTrack.GetMsuName()); + }); + } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs index 94087cca3..1a60dda90 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs @@ -257,4 +257,8 @@ private async void TrackerOnPlayerDied(object? sender, TrackerEventArgs e) await _multiplayerGameService.TrackDeath(); } + public override void AddCommands() + { + + } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs index 4dc81faa7..1d0a6f421 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; using Randomizer.SMZ3.Tracking.Services; @@ -21,6 +22,12 @@ public class PegWorldModeModule : TrackerModule, IOptionalModule /// Used to log information. public PegWorldModeModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) + { + + } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() { AddCommand("Toggle Peg World mode on", new[] { "Hey tracker, toggle Peg World Mode on", @@ -28,7 +35,7 @@ public PegWorldModeModule(Tracker tracker, IItemService itemService, IWorldServi "Hey tracker, let's go to Peg World!" }, (result) => { - tracker.StartPegWorldMode(result.Confidence); + Tracker.StartPegWorldMode(result.Confidence); }); AddCommand("Toggle Peg World mode off", new[] { @@ -39,7 +46,7 @@ public PegWorldModeModule(Tracker tracker, IItemService itemService, IWorldServi "Hey tracker, release me from Peg World" }, (result) => { - tracker.StopPegWorldMode(result.Confidence); + Tracker.StopPegWorldMode(result.Confidence); }); AddCommand("Track Peg World peg", new[] { @@ -47,9 +54,9 @@ public PegWorldModeModule(Tracker tracker, IItemService itemService, IWorldServi "Hey tracker, peg." }, (result) => { - if (tracker.PegsPegged < TotalPegs) + if (Tracker.PegsPegged < TotalPegs) { - tracker.Peg(result.Confidence); + Tracker.Peg(result.Confidence); } }); } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs index d030afdc5..45129233f 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs @@ -23,25 +23,30 @@ public class PersonalityModule : TrackerModule public PersonalityModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { - AddCommand("Ask about tracker's mood", GetMoodRule(), (result) => + + } + + public override void AddCommands() + { + AddCommand("Ask about tracker's mood", GetMoodRule(), (_) => { - tracker.Say(tracker.Responses.Moods[tracker.Mood]); + Tracker.Say(Tracker.Responses.Moods[Tracker.Mood]); }); - AddCommand("Hey, ya missed pal", GetYaMissedRule(), (result) => + AddCommand("Hey, ya missed pal", GetYaMissedRule(), (_) => { - tracker.Say("Here Mike. This will explain everything.", wait: true); + Tracker.Say("Here Mike. This will explain everything.", wait: true); OpenInBrowser(new Uri("https://www.youtube.com/watch?v=5P6UirFDdxM")); }); - foreach (var request in tracker.Requests) + foreach (var request in Tracker.Requests) { if (request.Phrases.Count == 0) continue; - AddCommand(request.Phrases.First(), GetRequestRule(request.Phrases), (result) => + AddCommand(request.Phrases.First(), GetRequestRule(request.Phrases), (_) => { - tracker.Say(request.Response); + Tracker.Say(request.Response); }); } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs index 652045f6c..1a7646a55 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs @@ -2,13 +2,11 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; - using Microsoft.Extensions.Logging; using Randomizer.Data.Logic; using Randomizer.Data.WorldData.Regions; using Randomizer.Data.WorldData; using Randomizer.Shared; -using Randomizer.SMZ3.Text; using Randomizer.Data.Configuration; using Randomizer.Data.Configuration.ConfigTypes; using Randomizer.SMZ3.Tracking.Services; @@ -32,10 +30,9 @@ public class SpoilerModule : TrackerModule, IOptionalModule private readonly Dictionary _itemHintsGiven = new(); private readonly Dictionary _locationHintsGiven = new(); - private readonly Playthrough? _playthrough; + private Playthrough? _playthrough; private readonly IRandomizerConfigService _randomizerConfigService; private readonly bool _isMultiworld; - private readonly int _localWorldId; /// /// Initializes a new instance of the class. @@ -48,64 +45,10 @@ public class SpoilerModule : TrackerModule, IOptionalModule public SpoilerModule(Tracker tracker, IItemService itemService, ILogger logger, IWorldService worldService, IRandomizerConfigService randomizerConfigService) : base(tracker, itemService, worldService, logger) { - Tracker.HintsEnabled = !tracker.World.Config.Race && !tracker.World.Config.DisableTrackerHints && tracker.Options.HintsEnabled; - Tracker.SpoilersEnabled = !tracker.World.Config.Race && !tracker.World.Config.DisableTrackerSpoilers && tracker.Options.SpoilersEnabled; + Tracker.HintsEnabled = tracker.World.Config is { Race: false, DisableTrackerHints: false } && tracker.Options.HintsEnabled; + Tracker.SpoilersEnabled = tracker.World.Config is { Race: false, DisableTrackerSpoilers: false } && tracker.Options.SpoilersEnabled; _randomizerConfigService = randomizerConfigService; _isMultiworld = tracker.World.Config.MultiWorld; - _localWorldId = worldService.World.Id; - if (tracker.World.Config.Race) return; - - Playthrough.TryGenerate(new[] { tracker.World }, tracker.World.Config, out _playthrough); - - if (!tracker.World.Config.DisableTrackerHints) - { - AddCommand("Enable hints", GetEnableHintsRule(), (result) => - { - Tracker.HintsEnabled = true; - tracker.Say(x => x.Hints.EnabledHints); - }); - AddCommand("Disable hints", GetDisableHintsRule(), (result) => - { - Tracker.HintsEnabled = false; - tracker.Say(x => x.Hints.DisabledHints); - }); - AddCommand("Give progression hint", GetProgressionHintRule(), (result) => - { - GiveProgressionHint(); - }); - - AddCommand("Give area hint", GetLocationUsefulnessHintRule(), (result) => - { - var area = GetAreaFromResult(tracker, result); - GiveAreaHint(area); - }); - } - - if (!tracker.World.Config.DisableTrackerSpoilers) - { - AddCommand("Enable spoilers", GetEnableSpoilersRule(), (result) => - { - Tracker.SpoilersEnabled = true; - tracker.Say(x => x.Spoilers.EnabledSpoilers); - }); - AddCommand("Disable spoilers", GetDisableSpoilersRule(), (result) => - { - Tracker.SpoilersEnabled = false; - tracker.Say(x => x.Spoilers.DisabledSpoilers); - }); - - AddCommand("Reveal item location", GetItemSpoilerRule(), (result) => - { - var item = GetItemFromResult(tracker, result, out var itemName); - RevealItemLocation(item); - }); - - AddCommand("Reveal location item", GetLocationSpoilerRule(), (result) => - { - var location = GetLocationFromResult(tracker, result); - RevealLocationItem(location); - }); - } } private Config Config => _randomizerConfigService.Config; @@ -113,9 +56,9 @@ public SpoilerModule(Tracker tracker, IItemService itemService, ILogger /// Gives a hint about where to go next. /// - public void GiveProgressionHint() + private void GiveProgressionHint() { - if (!Tracker.HintsEnabled && !Tracker.SpoilersEnabled) + if (Tracker is { HintsEnabled: false, SpoilersEnabled: false }) { Tracker.Say(x => x.Hints.PromptEnableItemHints); return; @@ -138,9 +81,9 @@ public void GiveProgressionHint() /// Gives a hint or spoiler about useful items in an area. /// /// The area to give hints about. - public void GiveAreaHint(IHasLocations area) + private void GiveAreaHint(IHasLocations area) { - if (!Tracker.HintsEnabled && !Tracker.SpoilersEnabled) + if (Tracker is { HintsEnabled: false, SpoilersEnabled: false }) { Tracker.Say(x => x.Hints.PromptEnableItemHints); return; @@ -196,7 +139,7 @@ public void GiveAreaHint(IHasLocations area) /// Gives a hint or spoiler about the location of an item. /// /// The item to find. - public void RevealItemLocation(Item item) + private void RevealItemLocation(Item item) { if (item.Metadata.HasStages && item.State.TrackingState >= item.Metadata.MaxStage) { @@ -221,7 +164,7 @@ public void RevealItemLocation(Item item) } // Now that we're ready to give hints, make sure they're turned on - if (!Tracker.HintsEnabled && !Tracker.SpoilersEnabled) + if (Tracker is { HintsEnabled: false, SpoilersEnabled: false }) { Tracker.Say(x => x.Hints.PromptEnableItemHints); return; @@ -270,7 +213,7 @@ public void RevealItemLocation(Item item) /// Gives a hint or spoiler about the given location. /// /// The location to ask about. - public void RevealLocationItem(Location location) + private void RevealLocationItem(Location location) { var locationName = location.Metadata.Name; if (location.State.Cleared) @@ -570,7 +513,7 @@ private bool GiveItemLocationSpoiler(Item item) private bool GiveItemLocationHint(Item item) { - var itemLocations = WorldService.Locations(outOfLogic: true, itemFilter: item.Type, checkAllWorlds: true); + var itemLocations = WorldService.Locations(outOfLogic: true, itemFilter: item.Type, checkAllWorlds: true).ToList(); if (!itemLocations.Any()) { @@ -692,14 +635,14 @@ private bool GiveItemLocationHint(Item item) // - Is it in another player's ALttP or SM? case 2: { - var randomLocation = GetRandomItemLocationWithFilter(item, x => true); + var randomLocation = GetRandomItemLocationWithFilter(item, _ => true); if (randomLocation?.World.IsLocalWorld == false) { - if (randomLocation?.Region is Z3Region) + if (randomLocation.Region is Z3Region) return GiveItemHint(x => x.ItemInPlayerWorldALttP, item, randomLocation.World.Config.PhoneticName); - else if (randomLocation?.Region is SMRegion) + else if (randomLocation.Region is SMRegion) return GiveItemHint(x => x.ItemInPlayerWorldSuperMetroid, item, randomLocation.World.Config.PhoneticName); } @@ -744,7 +687,7 @@ private bool GiveItemLocationHint(Item item) var randomLocation = GetRandomItemLocationWithFilter(item, location => { - if (location.Room != null && location.Room.Metadata.Hints?.Count > 0) + if (location.Room is { Metadata.Hints.Count: > 0 }) return true; return location.Metadata.Hints?.Count > 0; }); @@ -933,5 +876,62 @@ private GrammarBuilder GetLocationUsefulnessHintRule() return GrammarBuilder.Combine(regionGrammar, roomGrammar); } + + public override void AddCommands() + { + if (Tracker.World.Config.Race) return; + + Playthrough.TryGenerate(new[] { Tracker.World }, Tracker.World.Config, out _playthrough); + + if (!Tracker.World.Config.DisableTrackerHints) + { + AddCommand("Enable hints", GetEnableHintsRule(), (_) => + { + Tracker.HintsEnabled = true; + Tracker.Say(x => x.Hints.EnabledHints); + }); + AddCommand("Disable hints", GetDisableHintsRule(), (_) => + { + Tracker.HintsEnabled = false; + Tracker.Say(x => x.Hints.DisabledHints); + }); + AddCommand("Give progression hint", GetProgressionHintRule(), (_) => + { + GiveProgressionHint(); + }); + + AddCommand("Give area hint", GetLocationUsefulnessHintRule(), (result) => + { + var area = GetAreaFromResult(Tracker, result); + GiveAreaHint(area); + }); + } + + if (!Tracker.World.Config.DisableTrackerSpoilers) + { + AddCommand("Enable spoilers", GetEnableSpoilersRule(), (_) => + { + Tracker.SpoilersEnabled = true; + Tracker.Say(x => x.Spoilers.EnabledSpoilers); + }); + AddCommand("Disable spoilers", GetDisableSpoilersRule(), (_) => + { + Tracker.SpoilersEnabled = false; + Tracker.Say(x => x.Spoilers.DisabledSpoilers); + }); + + AddCommand("Reveal item location", GetItemSpoilerRule(), (result) => + { + var item = GetItemFromResult(Tracker, result, out _); + RevealItemLocation(item); + }); + + AddCommand("Reveal location item", GetLocationSpoilerRule(), (result) => + { + var location = GetLocationFromResult(Tracker, result); + RevealLocationItem(location); + }); + } + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs index b8ada5b0e..14fb845b5 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Speech.Recognition; @@ -73,6 +74,8 @@ protected TrackerModule(Tracker tracker, IItemService itemService, IWorldService Logger = logger; } + public abstract void AddCommands(); + /// /// Gets a dictionary that contains the rule names and their associated /// speech recognition patterns. @@ -104,6 +107,7 @@ public IReadOnlyDictionary> Syntax /// /// Gets a list of speech recognition grammars provided by the module. /// + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] protected IList Grammars { get; } = new List(); @@ -304,6 +308,11 @@ protected void AddCommand(string ruleName, string[] phrases, protected void AddCommand(string ruleName, GrammarBuilder grammarBuilder, Action executeCommand) { + if (!OperatingSystem.IsWindows()) + { + return; + } + _syntax.TryAdd(ruleName, grammarBuilder.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); try diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs index a3775b6da..f833d0c49 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs @@ -44,20 +44,25 @@ public TrackerModuleFactory(IServiceProvider serviceProvider, ILogger /// A dictionary that contains the loaded speech recognition syntax. /// - public IReadOnlyDictionary> LoadAll(Tracker tracker, SpeechRecognitionEngine engine, out bool moduleLoadError) + public IReadOnlyDictionary> LoadAll(Tracker tracker, SpeechRecognitionEngine? engine, out bool moduleLoadError) { moduleLoadError = false; _trackerModules = _serviceProvider.GetServices(); - foreach (var module in _trackerModules) + + if (engine != null && OperatingSystem.IsWindows()) { - try - { - module.LoadInto(engine); - } - catch (InvalidOperationException e) + foreach (var module in _trackerModules) { - _logger.LogError(e, $"Error with loading module {module.GetType().Name}"); - moduleLoadError = true; + try + { + module.AddCommands(); + module.LoadInto(engine); + } + catch (InvalidOperationException e) + { + _logger.LogError(e, $"Error with loading module {module.GetType().Name}"); + moduleLoadError = true; + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs index 254aa3bd9..d5e0135b0 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; using Randomizer.SMZ3.Tracking.Services; @@ -19,10 +20,7 @@ public class UndoModule : TrackerModule public UndoModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { - AddCommand("Undo last operation", GetUndoRule(), (result) => - { - tracker.Undo(result.Confidence); - }); + } private GrammarBuilder GetUndoRule() @@ -31,5 +29,14 @@ private GrammarBuilder GetUndoRule() .Append("Hey tracker,") .OneOf("undo that", "control Z", "that's not what I said", "take backsies"); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + AddCommand("Undo last operation", GetUndoRule(), (result) => + { + Tracker.Undo(result.Confidence); + }); + } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs index d6d6dcf1c..48ce41890 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Speech.Recognition; using Microsoft.Extensions.Logging; @@ -29,43 +30,10 @@ public class ZeldaDungeonTrackingModule : TrackerModule public ZeldaDungeonTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { - AddCommand("Mark dungeon pendant/crystal", GetMarkDungeonRewardRule(), (result) => - { - var dungeon = GetDungeonFromResult(tracker, result); - var reward = (RewardType)result.Semantics[RewardKey].Value; - tracker.SetDungeonReward(dungeon, reward, result.Confidence); - }); - - AddCommand("Mark remaining dungeons", GetMarkRemainingDungeonRewardsRule(), (result) => - { - tracker.SetUnmarkedDungeonReward(RewardType.CrystalBlue, result.Confidence); - }); - - AddCommand("Mark dungeon as cleared", GetClearDungeonRule(), (result) => - { - var dungeon = GetDungeonFromResult(tracker, result); - tracker.MarkDungeonAsCleared(dungeon, result.Confidence); - }); - - AddCommand("Mark dungeon medallion", GetMarkDungeonRequirementRule(), (result) => - { - var dungeon = GetDungeonFromResult(tracker, result); - var medallion = GetItemFromResult(tracker, result, out var itemName); - tracker.SetDungeonRequirement(dungeon, medallion.Type, result.Confidence); - }); - AddCommand("Clear dungeon treasure", GetTreasureTrackingRule(), (result) => - { - var count = result.Semantics.ContainsKey(TreasureCountKey) - ? (int)result.Semantics[TreasureCountKey].Value - : 1; - var dungeon = GetDungeonFromResult(tracker, result); - tracker.TrackDungeonTreasure(dungeon, result.Confidence, amount: count); - - dungeon.DungeonState.HasManuallyClearedTreasure = true; - }); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private GrammarBuilder GetMarkDungeonRewardRule() { var dungeonNames = GetDungeonNames(includeDungeonsWithoutReward: false); @@ -144,6 +112,7 @@ private GrammarBuilder GetMarkDungeonRequirementRule() markDungeon, markItem); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private GrammarBuilder GetTreasureTrackingRule() { var dungeonNames = GetDungeonNames(includeDungeonsWithoutReward: true); @@ -169,5 +138,45 @@ private GrammarBuilder GetTreasureTrackingRule() return GrammarBuilder.Combine(clearOne, clearMultiple); } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public override void AddCommands() + { + AddCommand("Mark dungeon pendant/crystal", GetMarkDungeonRewardRule(), (result) => + { + var dungeon = GetDungeonFromResult(Tracker, result); + var reward = (RewardType)result.Semantics[RewardKey].Value; + Tracker.SetDungeonReward(dungeon, reward, result.Confidence); + }); + + AddCommand("Mark remaining dungeons", GetMarkRemainingDungeonRewardsRule(), (result) => + { + Tracker.SetUnmarkedDungeonReward(RewardType.CrystalBlue, result.Confidence); + }); + + AddCommand("Mark dungeon as cleared", GetClearDungeonRule(), (result) => + { + var dungeon = GetDungeonFromResult(Tracker, result); + Tracker.MarkDungeonAsCleared(dungeon, result.Confidence); + }); + + AddCommand("Mark dungeon medallion", GetMarkDungeonRequirementRule(), (result) => + { + var dungeon = GetDungeonFromResult(Tracker, result); + var medallion = GetItemFromResult(Tracker, result, out _); + Tracker.SetDungeonRequirement(dungeon, medallion.Type, result.Confidence); + }); + + AddCommand("Clear dungeon treasure", GetTreasureTrackingRule(), (result) => + { + var count = result.Semantics.ContainsKey(TreasureCountKey) + ? (int)result.Semantics[TreasureCountKey].Value + : 1; + var dungeon = GetDungeonFromResult(Tracker, result); + Tracker.TrackDungeonTreasure(dungeon, result.Confidence, amount: count); + + dungeon.DungeonState.HasManuallyClearedTreasure = true; + }); + } } } diff --git a/src/Randomizer.SMZ3/Generation/Smz3GeneratedRomLoader.cs b/src/Randomizer.SMZ3/Generation/Smz3GeneratedRomLoader.cs index e28249c3e..101ecc59a 100644 --- a/src/Randomizer.SMZ3/Generation/Smz3GeneratedRomLoader.cs +++ b/src/Randomizer.SMZ3/Generation/Smz3GeneratedRomLoader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Randomizer.Data.Configuration.ConfigTypes; using Randomizer.Data.Options; @@ -33,7 +34,7 @@ public Smz3GeneratedRomLoader(IWorldAccessor worldAccessor, IMetadataService met /// the given GeneratedRom /// /// - public void LoadGeneratedRom(GeneratedRom rom) + public List LoadGeneratedRom(GeneratedRom rom) { var trackerState = rom.TrackerState; @@ -121,9 +122,9 @@ public void LoadGeneratedRom(GeneratedRom rom) } } - _worldAccessor.Worlds = worlds; _worldAccessor.World = worlds.First(x => x.IsLocalWorld); + return worlds; } } } diff --git a/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs b/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs index b9a070d73..2a7732daf 100644 --- a/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs +++ b/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Text; using FluentAssertions; @@ -27,6 +28,12 @@ public class RandomizerTests [InlineData("test", 558598333)] // Smz3Randomizer v4.0 public void StandardFillerWithSameSeedGeneratesSameWorld(string seed, int expectedHash) { + // Apparently RNG in Linux is different than on Windows... + if (OperatingSystem.IsLinux()) + { + expectedHash = 951851411; + } + var filler = new StandardFiller(GetLogger()); var randomizer = GetRandomizer(); var config = new Config();