diff --git a/CasRandomizer.sln b/CasRandomizer.sln index 4f1258537..510fe4df5 100644 --- a/CasRandomizer.sln +++ b/CasRandomizer.sln @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Randomizer.PatchBuilder", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Randomizer.CrossPlatform", "src\Randomizer.CrossPlatform\Randomizer.CrossPlatform.csproj", "{B18DD122-3612-4948-B512-464D410A271B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Randomizer.Abstractions", "src\Randomizer.Abstractions\Randomizer.Abstractions.csproj", "{C5CED4F3-F57F-4651-88FB-5774326654E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,6 +109,10 @@ Global {B18DD122-3612-4948-B512-464D410A271B}.Debug|Any CPU.Build.0 = Debug|Any CPU {B18DD122-3612-4948-B512-464D410A271B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B18DD122-3612-4948-B512-464D410A271B}.Release|Any CPU.Build.0 = Release|Any CPU + {C5CED4F3-F57F-4651-88FB-5774326654E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5CED4F3-F57F-4651-88FB-5774326654E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5CED4F3-F57F-4651-88FB-5774326654E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5CED4F3-F57F-4651-88FB-5774326654E7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Randomizer.Abstractions/IAutoTracker.cs b/src/Randomizer.Abstractions/IAutoTracker.cs new file mode 100644 index 000000000..761cb2b94 --- /dev/null +++ b/src/Randomizer.Abstractions/IAutoTracker.cs @@ -0,0 +1,100 @@ +using Randomizer.Data.Options; +using Randomizer.Data.Tracking; +using Randomizer.Shared.Enums; + +namespace Randomizer.Abstractions; + +public interface IAutoTracker +{ + /// + /// The tracker associated with this auto tracker + /// + public ITracker Tracker { get; } + + /// + /// The type of connector that the auto tracker is currently using + /// + public EmulatorConnectorType ConnectorType { get; } + + /// + /// The game that the player is currently in + /// + public Game CurrentGame { get; } + + /// + /// The latest state that the player in LTTP (location, health, etc.) + /// + public AutoTrackerZeldaState? ZeldaState { get; } + + /// + /// The latest state that the player in Super Metroid (location, health, etc.) + /// + public AutoTrackerMetroidState? MetroidState { get; } + + /// + /// Disables the current connector and creates the requested type + /// + public void SetConnector(EmulatorConnectorType type, string? qusb2SnesIp); + + /// + /// Occurs when the tracker's auto tracker is enabled + /// + public event EventHandler? AutoTrackerEnabled; + + /// + /// Occurs when the tracker's auto tracker is disabled + /// + public event EventHandler? AutoTrackerDisabled; + + /// + /// Occurs when the tracker's auto tracker is connected + /// + public event EventHandler? AutoTrackerConnected; + + /// + /// Occurs when the tracker's auto tracker is disconnected + /// + public event EventHandler? AutoTrackerDisconnected; + + /// + /// The action to run when the player asks Tracker to look at the game + /// + public AutoTrackerViewedAction? LatestViewAction { get; set; } + + /// + /// If a connector is currently enabled + /// + public bool IsEnabled { get; } + + /// + /// If a connector is currently connected to the emulator + /// + public bool IsConnected { get; } + + /// + /// If a connector is currently connected to the emulator and a valid game state is detected + /// + public bool HasValidState { get; } + + /// + /// If the auto tracker is currently sending messages + /// + public bool IsSendingMessages { get; } + + /// + /// If the player currently has a fairy + /// + public bool PlayerHasFairy { get; } + + /// + /// If the user is activately in an SMZ3 rom + /// + public bool IsInSMZ3 { get; } + + /// + /// Writes a particular action to the emulator memory + /// + /// The action to write to memory + public void WriteToMemory(EmulatorAction action); + +} diff --git a/src/Randomizer.Abstractions/IGameService.cs b/src/Randomizer.Abstractions/IGameService.cs new file mode 100644 index 000000000..16a3f8264 --- /dev/null +++ b/src/Randomizer.Abstractions/IGameService.cs @@ -0,0 +1,110 @@ +using Randomizer.Data.Tracking; +using Randomizer.Data.WorldData; +using Randomizer.Shared; + +namespace Randomizer.Abstractions; + +public interface IGameService +{ + /// + /// Updates memory values so both SM and Z3 will cancel any pending MSU resumes and play + /// all tracks from the start until new resume points have been stored. + /// + /// True, even if it didn't do anything + public void TryCancelMsuResume(); + + /// + /// Gives an item to the player + /// + /// The item to give + /// The id of the player giving the item to the player (null for tracker) + /// False if it is currently unable to give an item to the player + public bool TryGiveItem(Item item, int? fromPlayerId); + + /// + /// Gives a series of items to the player + /// + /// The list of items to give to the player + /// The id of the player giving the item to the player + /// False if it is currently unable to give an item to the player + public bool TryGiveItems(List items, int fromPlayerId); + + /// + /// Gives a series of item types from particular players + /// + /// The list of item types and the players that are giving the item to the player + /// False if it is currently unable to give the items to the player + public bool TryGiveItemTypes(List<(ItemType type, int fromPlayerId)> items); + + /// + /// Restores the player to max health + /// + /// False if it is currently unable to give an item to the player + public bool TryHealPlayer(); + + /// + /// Fully fills the player's magic + /// + /// False if it is currently unable to give magic to the player + public bool TryFillMagic(); + + /// + /// Fully fills the player's bombs to capacity + /// + /// False if it is currently unable to give bombs to the player + public bool TryFillZeldaBombs(); + + /// + /// Fully fills the player's arrows + /// + /// False if it is currently unable to give arrows to the player + public bool TryFillArrows(); + + /// + /// Fully fills the player's rupees (sets to 2000) + /// + /// False if it is currently unable to give rupees to the player + public bool TryFillRupees(); + + /// + /// Fully fills the player's missiles + /// + /// False if it is currently unable to give missiles to the player + public bool TryFillMissiles(); + + /// + /// Fully fills the player's super missiles + /// + /// False if it is currently unable to give super missiles to the player + public bool TryFillSuperMissiles(); + + /// + /// Fully fills the player's power bombs + /// + /// False if it is currently unable to give power bombs to the player + public bool TryFillPowerBombs(); + + /// + /// Kills the player by removing their health and dealing damage to them + /// + /// True if successful + public bool TryKillPlayer(); + + /// + /// Sets the player to have the requirements for a crystal flash + /// + /// True if successful + public bool TrySetupCrystalFlash(); + + /// + /// Gives the player any items that tracker thinks they should have but are not in memory as having been gifted + /// + /// + public void SyncItems(EmulatorAction action); + + /// + /// If the player was recently killed by the game service + /// + public bool PlayerRecentlyKilled { get; } + +} diff --git a/src/Randomizer.Abstractions/IHistoryService.cs b/src/Randomizer.Abstractions/IHistoryService.cs new file mode 100644 index 000000000..c034ae848 --- /dev/null +++ b/src/Randomizer.Abstractions/IHistoryService.cs @@ -0,0 +1,44 @@ +using Randomizer.Data.WorldData; +using Randomizer.Shared.Enums; +using Randomizer.Shared.Models; + +namespace Randomizer.Abstractions; + +/// +/// Service for managing the history of events through a playthrough +/// +public interface IHistoryService +{ + /// + /// Adds an event to the history log + /// + /// The type of event + /// If this is an important event or not + /// The name of the event being logged + /// The optional location of where this event happened + /// The created event + public TrackerHistoryEvent AddEvent(HistoryEventType type, bool isImportant, string objectName, Location? location = null); + + /// + /// Adds an event to the history log + /// + /// The event to add + public void AddEvent(TrackerHistoryEvent histEvent); + + /// + /// Removes the event that was added last to the log + /// + public void RemoveLastEvent(); + + /// + /// Removes a specific event from the log + /// + /// The event to log + public void Remove(TrackerHistoryEvent histEvent); + + /// + /// Retrieves the current history log + /// + /// The collection of events + public IReadOnlyCollection GetHistory(); +} diff --git a/src/Randomizer.Abstractions/IItemService.cs b/src/Randomizer.Abstractions/IItemService.cs new file mode 100644 index 000000000..0681fa52d --- /dev/null +++ b/src/Randomizer.Abstractions/IItemService.cs @@ -0,0 +1,167 @@ +using Randomizer.Data.Configuration.ConfigTypes; +using Randomizer.Data.WorldData; +using Randomizer.Data.WorldData.Regions; +using Randomizer.Shared; + +namespace Randomizer.Abstractions; + +/// +/// Defines methods for managing items and their tracking state. +/// +public interface IItemService +{ + /// + /// Enumerates all items that can be tracked for all players. + /// + /// A collection of items. + IEnumerable AllItems(); + + /// + /// Enumerates all items that can be tracked for the local player. + /// + /// A collection of items. + IEnumerable LocalPlayersItems(); + + /// + /// Enumarates all currently tracked items for the local player. + /// + /// + /// A collection of items that have been tracked at least once. + /// + IEnumerable TrackedItems(); + + /// + /// Finds the item with the specified name for the local player. + /// + /// + /// The name of the item or item stage to find. + /// + /// + /// An representing the item with the specified + /// name, or if there is no item that has the + /// specified name. + /// + Item? FirstOrDefault(string name); + + /// + /// Finds an item with the specified item type for the local player. + /// + /// The type of item to find. + /// + /// An representing the item. If there are + /// multiple configured items with the same type, this method returns + /// one at random. If there no configured items with the specified type, + /// this method returns . + /// + Item? FirstOrDefault(ItemType itemType); + + /// + /// Returns a random name for the specified item including article, e.g. + /// "an E-Tank" or "the Book of Mudora". + /// + /// The type of item whose name to get. + /// + /// The name of the type of item, including "a", "an" or "the" if + /// applicable. + /// + string GetName(ItemType itemType); + + /// + /// Indicates whether an item of the specified type has been tracked + /// for the local player. + /// + /// The type of item to check. + /// + /// if an item with the specified type has been + /// tracked at least once; otherwise, . + /// + bool IsTracked(ItemType itemType); + + /// + /// Finds an reward with the specified item type. + /// + /// The type of reward to find. + /// + /// An representing the reward. If there are + /// multiple configured rewards with the same type, this method returns + /// one at random. If there no configured rewards with the specified type, + /// this method returns . + /// + Reward? FirstOrDefault(RewardType rewardType); + + /// + /// Returns a random name for the specified item including article, e.g. + /// "a blue crystal" or "the green pendant". + /// + /// The reward of item whose name to get. + /// + /// The name of the reward of item, including "a", "an" or "the" if + /// applicable. + /// + string GetName(RewardType rewardType); + + /// + /// Enumerates all rewards that can be tracked for the local player. + /// + /// A collection of rewards. + + IEnumerable AllRewards(); + + /// + /// Enumerates all rewards that can be tracked for the local player. + /// + /// A collection of rewards. + + IEnumerable LocalPlayersRewards(); + + /// + /// Enumarates all currently tracked rewards for the local player. + /// + /// + /// A collection of reward that have been tracked. + /// + IEnumerable TrackedRewards(); + + /// + /// Enumerates all bosses that can be tracked for all players. + /// + /// A collection of bosses. + + IEnumerable AllBosses(); + + /// + /// Enumerates all bosses that can be tracked for the local player. + /// + /// A collection of bosses. + + IEnumerable LocalPlayersBosses(); + + /// + /// Enumarates all currently tracked bosses for the local player. + /// + /// + /// A collection of bosses that have been tracked. + /// + IEnumerable TrackedBosses(); + + /// + /// Retrieves the progression containing all of the tracked items, rewards, and bosses + /// for determining in logic locations + /// + /// If it should be assumed that the player has all keys and keycards + /// + Progression GetProgression(bool assumeKeys); + + /// + /// Retrieves the progression containing all of the tracked items, rewards, and bosses + /// for determining in logic locations + /// + /// The area being looked at to see if keys/keycards should be assumed or not + /// + Progression GetProgression(IHasLocations area); + + /// + /// Clears cached progression + /// + void ResetProgression(); +} diff --git a/src/Randomizer.Abstractions/ITracker.cs b/src/Randomizer.Abstractions/ITracker.cs new file mode 100644 index 000000000..7cc28b84a --- /dev/null +++ b/src/Randomizer.Abstractions/ITracker.cs @@ -0,0 +1,761 @@ +using MSURandomizerLibrary.Configs; +using Randomizer.Data; +using Randomizer.Data.Configuration.ConfigFiles; +using Randomizer.Data.Configuration.ConfigTypes; +using Randomizer.Data.Services; +using Randomizer.Data.Tracking; +using Randomizer.Data.WorldData; +using Randomizer.Data.WorldData.Regions; +using Randomizer.Shared; +using Randomizer.Shared.Models; + +namespace Randomizer.Abstractions; + +public interface ITracker +{ + /// + /// Occurs when any speech was recognized, regardless of configured + /// thresholds. + /// + public event EventHandler? SpeechRecognized; + + /// + /// Occurs when one more more items have been tracked. + /// + public event EventHandler? ItemTracked; + + /// + /// Occurs when a location has been cleared. + /// + public event EventHandler? LocationCleared; + + /// + /// Occurs when Peg World mode has been toggled on. + /// + public event EventHandler? ToggledPegWorldModeOn; + + /// + /// Occurs when going to Shaktool + /// + public event EventHandler? ToggledShaktoolMode; + + /// + /// Occurs when a Peg World peg has been pegged. + /// + public event EventHandler? PegPegged; + + /// + /// Occurs when the properties of a dungeon have changed. + /// + public event EventHandler? DungeonUpdated; + + /// + /// Occurs when the properties of a boss have changed. + /// + public event EventHandler? BossUpdated; + + /// + /// Occurs when the marked locations have changed + /// + public event EventHandler? MarkedLocationsUpdated; + + /// + /// Occurs when Go mode has been turned on. + /// + public event EventHandler? GoModeToggledOn; + + /// + /// Occurs when the last action was undone. + /// + public event EventHandler? ActionUndone; + + /// + /// Occurs when the tracker state has been loaded. + /// + public event EventHandler? StateLoaded; + + /// + /// Occurs when the map has been updated + /// + public event EventHandler? MapUpdated; + + /// + /// Occurs when the map has been updated + /// + public event EventHandler? BeatGame; + + /// + /// Occurs when the map has died + /// + public event EventHandler? PlayerDied; + + /// + /// Occurs when the current played track number is updated + /// + public event EventHandler? TrackNumberUpdated; + + /// + /// Occurs when the current track has changed + /// + public event EventHandler? TrackChanged; + + /// + /// Set when the progression needs to be updated for the current tracker + /// instance + /// + public bool UpdateTrackerProgression { get; set; } + + /// + /// Gets extra information about locations. + /// + public IMetadataService Metadata { get; } + + /// + /// Gets a reference to the . + /// + public IItemService ItemService { get; } + + /// + /// The number of pegs that have been pegged for Peg World mode + /// + public int PegsPegged { get; set; } + + /// + /// Gets the world for the currently tracked playthrough. + /// + public World World { get; } + + /// + /// Indicates whether Tracker is in Go Mode. + /// + public bool GoMode { get; } + + /// + /// Indicates whether Tracker is in Peg World mode. + /// + public bool PegWorldMode { get; set; } + + /// + /// Indicates whether Tracker is in Shaktool mode. + /// + public bool ShaktoolMode { get; set; } + + /// + /// If the speech recognition engine was fully initialized + /// + public bool MicrophoneInitialized { get; } + + /// + /// If voice recognition has been enabled or not + /// + public bool VoiceRecognitionEnabled { get; } + + /// + /// Gets the configured responses. + /// + public ResponseConfig Responses { get; } + + /// + /// Gets a collection of basic requests and responses. + /// + public IReadOnlyCollection Requests { get; } + + /// + /// Gets a dictionary containing the rules and the various speech + /// recognition syntaxes. + /// + public IReadOnlyDictionary> Syntax { get; } + + /// + /// Gets the tracking preferences. + /// + public TrackerOptions Options { get;} + + /// + /// The generated rom + /// + public GeneratedRom? Rom { get; } + + /// + /// The path to the generated rom + /// + public string? RomPath { get; } + + /// + /// The region the player is currently in according to the Auto Tracker + /// + public RegionInfo? CurrentRegion { get; } + + /// + /// The map to display for the player + /// + public string CurrentMap { get; } + + /// + /// The current track number being played + /// + public int CurrentTrackNumber { get; } + + /// + /// Gets a string describing tracker's mood. + /// + public string Mood { get;} + /// + /// Get if the Tracker has been updated since it was last saved + /// + public bool IsDirty { get; set; } + + /// + /// The Auto Tracker for the Tracker + /// + public IAutoTracker? AutoTracker { get; set; } + + /// + /// Service that handles modifying the game via auto tracker + /// + public IGameService? GameService { get; set; } + + /// + /// Module that houses the history + /// + public IHistoryService History { get; set; } + + /// + /// Gets or sets a value indicating whether Tracker may give hints when + /// asked about items or locations. + /// + public bool HintsEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether Tracker may give spoilers + /// when asked about items or locations. + /// + public bool SpoilersEnabled { get; set; } + + /// + /// Gets if the local player has beaten the game or not + /// + public bool HasBeatenGame { get; } + + /// + /// Attempts to replace a user name with a pronunciation-corrected + /// version of it. + /// + /// The user name to correct. + /// + /// The corrected user name, or . + /// + public string CorrectUserNamePronunciation(string userName); + + /// + /// Initializes the microphone from the default audio device + /// + /// + /// True if the microphone is initialized, false otherwise + /// + public bool InitializeMicrophone(); + + /// + /// Loads the state from the database for a given rom + /// + /// The rom to load + /// The full path to the rom to load + /// True or false if the load was successful + public bool Load(GeneratedRom rom, string romPath); + + /// + /// Saves the state of the tracker to the database + /// + /// + public Task SaveAsync(); + + /// + /// Undoes the last operation. + /// + /// The speech recognition confidence. + public void Undo(float confidence); + + /// + /// Toggles Go Mode on. + /// + /// The speech recognition confidence. + public void ToggleGoMode(float? confidence = null); + + /// + /// Removes one or more items from the available treasure in the + /// specified dungeon. + /// + /// The dungeon. + /// The number of treasures to track. + /// The speech recognition confidence. + /// If this was called by the auto tracker + /// If tracker should state the treasure ammount + /// + /// true if treasure was tracked; false if there is no + /// treasure left to track. + /// + /// + /// This method adds to the undo history if the return value is + /// true. + /// + /// + /// is less than 1. + /// + public bool TrackDungeonTreasure(IDungeon dungeon, float? confidence = null, int amount = 1, bool autoTracked = false, bool stateResponse = true); + + /// + /// Sets the dungeon's reward to the specific pendant or crystal. + /// + /// The dungeon to mark. + /// + /// The type of pendant or crystal, or null to cycle through the + /// possible rewards. + /// + /// The speech recognition confidence. + /// If this was called by the auto tracker + public void SetDungeonReward(IDungeon dungeon, RewardType? reward = null, float? confidence = null, bool autoTracked = false); + + /// + /// Sets the reward of all unmarked dungeons. + /// + /// The reward to set. + /// The speech recognition confidence. + public void SetUnmarkedDungeonReward(RewardType reward, float? confidence = null); + + /// + /// Sets the dungeon's medallion requirement to the specified item. + /// + /// The dungeon to mark. + /// The medallion that is required. + /// The speech recognition confidence. + public void SetDungeonRequirement(IDungeon dungeon, ItemType? medallion = null, float? confidence = null); + + /// + /// Starts voice recognition. + /// + public bool TryStartTracking(); + + /// + /// Connects Tracker to chat. + /// + /// The user name to connect as. + /// + /// The OAuth token for . + /// + /// + /// The channel to monitor for incoming messages. + /// + /// + /// The is for . + /// + public void ConnectToChat(string? userName, string? oauthToken, string? channel, string? id); + + /// + /// Sets the start time of the timer + /// + public void StartTimer(bool isInitial = false); + + /// + /// Resets the timer to 0 + /// + public void ResetTimer(bool isInitial = false); + + /// + /// Pauses the timer, saving the elapsed time + /// + public Action? PauseTimer(bool addUndo = true); + + /// + /// Pauses or resumes the timer based on if it is + /// currently paused or not + /// + public void ToggleTimer(); + + /// + /// Stops voice recognition. + /// + public void StopTracking(); + + /// + /// Enables the voice recognizer if the microphone is enabled + /// + public void EnableVoiceRecognition(); + + /// + /// Disables voice recognition if it was previously enabled + /// + public void DisableVoiceRecognition(); + + /// + /// Speak a sentence using text-to-speech. + /// + /// The possible sentences to speak. + /// + /// true if a sentence was spoken, false if was null. + /// + public bool Say(SchrodingersString? text); + + /// + /// Speak a sentence using text-to-speech. + /// + /// Selects the response to use. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public bool Say(Func selectResponse); + + /// + /// Speak a sentence using text-to-speech. + /// + /// The possible sentences to speak. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if was null. + /// + public bool Say(SchrodingersString? text, params object?[] args); + + /// + /// Speak a sentence using text-to-speech. + /// + /// Selects the response to use. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public bool Say(Func selectResponse, params object?[] args); + + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// The possible sentences to speak. + /// + /// true if a sentence was spoken, false if was null. + /// + public bool SayOnce(SchrodingersString? text); + + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// Selects the response to use. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public bool SayOnce(Func selectResponse) + { + return SayOnce(selectResponse(Responses)); + } + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// Selects the response to use. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public bool SayOnce(Func selectResponse, params object?[] args); + + /// + /// Speak a sentence using text-to-speech. + /// + /// The phrase to speak. + /// + /// true to wait until the text has been spoken completely or + /// false to immediately return. The default is false. + /// + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public bool Say(string? text, bool wait = false); + + /// + /// Repeats the most recently spoken sentence using text-to-speech at a + /// slower rate. + /// + public void Repeat(); + + /// + /// Makes Tracker stop talking. + /// + public void ShutUp(); + + /// + /// Notifies the user an error occurred. + /// + public void Error(); + + /// + /// Tracks the specifies item. + /// + /// The item data to track. + /// + /// The text that was tracked, when triggered by voice command. + /// + /// The speech recognition confidence. + /// + /// to attempt to clear a location for the + /// tracked item; if that is done by the caller. + /// + /// If this was tracked by the auto tracker + /// The location an item was tracked from + /// If the item was gifted to the player by tracker or another player + /// If tracker should not say anything + /// + /// if the item was actually tracked; if the item could not be tracked, e.g. when + /// tracking Bow twice. + /// + public bool TrackItem(Item item, string? trackedAs = null, float? confidence = null, bool tryClear = true, bool autoTracked = false, Location? location = null, bool giftedItem = false, bool silent = false); + + /// + /// Tracks multiple items at the same time + /// + /// The items to track + /// If the items were tracked via auto tracker + /// If the items were gifted to the player + public void TrackItems(List items, bool autoTracked, bool giftedItem); + + /// + /// Removes an item from the tracker. + /// + /// The item to untrack. + /// The speech recognition confidence. + public void UntrackItem(Item item, float? confidence = null); + + /// + /// Tracks the specifies item and clears it from the specified dungeon. + /// + /// The item data to track. + /// + /// The text that was tracked, when triggered by voice command. + /// + /// The dungeon the item was tracked in. + /// The speech recognition confidence. + public void TrackItem(Item item, IDungeon dungeon, string? trackedAs = null, float? confidence = null); + + /// + /// Tracks the specified item and clears it from the specified room. + /// + /// The item data to track. + /// + /// The text that was tracked, when triggered by voice command. + /// + /// The area the item was found in. + /// The speech recognition confidence. + public void TrackItem(Item item, IHasLocations area, string? trackedAs = null, float? confidence = null); + + /// + /// Sets the item count for the specified item. + /// + /// The item to track. + /// + /// The amount of the item that is in the player's inventory now. + /// + /// The speech recognition confidence. + public void TrackItemAmount(Item item, int count, float confidence); + + /// + /// Clears every item in the specified area, optionally tracking the + /// cleared items. + /// + /// The area whose items to clear. + /// + /// true to track any items found; false to only clear the + /// affected locations. + /// + /// + /// true to include every item in , even + /// those that are not in logic. false to only include chests + /// available with current items. + /// + /// The speech recognition confidence. + /// + /// Set to true to ignore keys when clearing the location. + /// + public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailable = false, float? confidence = null, bool assumeKeys = false); + + /// + /// Marks all locations and treasure within a dungeon as cleared. + /// + /// The dungeon to clear. + /// The speech recognition confidence. + public void ClearDungeon(IDungeon dungeon, float? confidence = null); + + /// + /// Clears an item from the specified location. + /// + /// The location to clear. + /// The speech recognition confidence. + /// If this was tracked by the auto tracker + public void Clear(Location location, float? confidence = null, bool autoTracked = false); + + /// + /// Marks a dungeon as cleared and, if possible, tracks the boss reward. + /// + /// The dungeon that was cleared. + /// The speech recognition confidence. + /// If this was cleared by the auto tracker + public void MarkDungeonAsCleared(IDungeon dungeon, float? confidence = null, bool autoTracked = false); + + /// + /// Marks a boss as defeated. + /// + /// The boss that was defeated. + /// + /// if the command implies the boss was killed; + /// if the boss was simply "tracked". + /// + /// The speech recognition confidence. + /// If this was tracked by the auto tracker + public void MarkBossAsDefeated(Boss boss, bool admittedGuilt = true, float? confidence = null, bool autoTracked = false); + + /// + /// Un-marks a boss as defeated. + /// + /// The boss that should be 'revived'. + /// The speech recognition confidence. + public void MarkBossAsNotDefeated(Boss boss, float? confidence = null); + + /// + /// Un-marks a dungeon as cleared and, if possible, untracks the boss + /// reward. + /// + /// The dungeon that should be un-cleared. + /// The speech recognition confidence. + public void MarkDungeonAsIncomplete(IDungeon dungeon, float? confidence = null); + + /// + /// Marks an item at the specified location. + /// + /// The location to mark. + /// + /// The item that is found at . + /// + /// The speech recognition confidence. + public void MarkLocation(Location location, Item item, float? confidence = null); + + /// + /// Pegs a Peg World peg. + /// + /// The speech recognition confidence. + public void Peg(float? confidence = null); + + /// + /// Starts Peg World mode. + /// + /// The speech recognition confidence. + public void StartPegWorldMode(float? confidence = null); + + /// + /// Turns Peg World mode off. + /// + /// The speech recognition confidence. + public void StopPegWorldMode(float? confidence = null); + + /// + /// Starts Peg World mode. + /// + /// The speech recognition confidence. + public void StartShaktoolMode(float? confidence = null); + + /// + /// Turns Peg World mode off. + /// + /// The speech recognition confidence. + public void StopShaktoolMode(float? confidence = null); + + /// + /// Updates the region that the player is in + /// + /// The region the player is in + /// Set to true to update the map for the player to match the region + /// If the time should be reset if this is the first region update + public void UpdateRegion(Region region, bool updateMap = false, bool resetTime = false); + + /// + /// Updates the region that the player is in + /// + /// The region the player is in + /// Set to true to update the map for the player to match the region + /// If the time should be reset if this is the first region update + public void UpdateRegion(RegionInfo? region, bool updateMap = false, bool resetTime = false); + + /// + /// Updates the map to display for the user + /// + /// The name of the map + public void UpdateMap(string map); + + /// + /// Called when the game is beaten by entering triforce room + /// or entering the ship after beating both bosses + /// + /// If this was triggered by the auto tracker + public void GameBeaten(bool autoTracked); + + /// + /// Called when the player has died + /// + public void TrackDeath(bool autoTracked); + + /// + /// Updates the current track number being played + /// + /// The number of the track + public void UpdateTrackNumber(int number); + + /// + /// Updates the current track being played + /// + /// The current MSU pack + /// The current track + /// Formatted output text matching the requested style + public void UpdateTrack(Msu msu, Track track, string outputText); + + public void RestartIdleTimers(); + + /// + /// Adds an action to be invoked to undo the last operation. + /// + /// + /// The action to invoke to undo the last operation. + /// + public void AddUndo(Action undo); + + /// + /// Determines whether or not the specified reward is worth getting. + /// + /// The dungeon reward. + /// + /// if the reward leads to something good; + /// otherwise, . + /// + public bool IsWorth(RewardType reward); + + /// + /// Formats a string so that it will be pronounced correctly by the + /// text-to-speech engine. + /// + /// The text to correct. + /// A string with the pronunciations replaced. + public static string CorrectPronunciation(string name) + => name.Replace("Samus", "Sammus"); + + /// + /// Determines whether or not the specified item is worth getting. + /// + /// The item whose worth to consider. + /// + /// is the item is worth getting or leads to + /// another item that is worth getting; otherwise, . + /// + public bool IsWorth(Item item); +} diff --git a/src/Randomizer.Abstractions/Randomizer.Abstractions.csproj b/src/Randomizer.Abstractions/Randomizer.Abstractions.csproj new file mode 100644 index 000000000..fd3eefa3e --- /dev/null +++ b/src/Randomizer.Abstractions/Randomizer.Abstractions.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/Randomizer.App/App.xaml.cs b/src/Randomizer.App/App.xaml.cs index ea97f442a..0acb24f78 100644 --- a/src/Randomizer.App/App.xaml.cs +++ b/src/Randomizer.App/App.xaml.cs @@ -14,6 +14,7 @@ using MSURandomizerLibrary.Models; using MSURandomizerLibrary.Services; using MSURandomizerUI; +using Randomizer.Abstractions; using Randomizer.App.Controls; using Randomizer.App.Windows; using Randomizer.Data.Configuration; @@ -112,12 +113,12 @@ protected static void ConfigureServices(IServiceCollection services) .AddOptionalModule() .AddOptionalModule() .AddOptionalModule() - .AddOptionalModule() - .AddOptionalModule(); + .AddOptionalModule(); + services.AddScoped(); services.AddSingleton(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddMultiplayerServices(); services.AddSingleton(); diff --git a/src/Randomizer.App/Controls/RomListPanel.cs b/src/Randomizer.App/Controls/RomListPanel.cs index 01d767b15..6eb96509b 100644 --- a/src/Randomizer.App/Controls/RomListPanel.cs +++ b/src/Randomizer.App/Controls/RomListPanel.cs @@ -14,6 +14,7 @@ using Randomizer.SMZ3.Generation; using Randomizer.SMZ3.Infrastructure; using Randomizer.SMZ3.Tracking; +using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.App.Controls { diff --git a/src/Randomizer.App/TrackerLocationSyncer.cs b/src/Randomizer.App/TrackerLocationSyncer.cs index 7cf761bb0..f735c5ad6 100644 --- a/src/Randomizer.App/TrackerLocationSyncer.cs +++ b/src/Randomizer.App/TrackerLocationSyncer.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Data.WorldData; using Randomizer.SMZ3.Tracking; using Randomizer.SMZ3.Tracking.Services; @@ -24,7 +25,7 @@ public class TrackerLocationSyncer /// Service for retrieving the item data /// Service for retrieving world data /// Logger - public TrackerLocationSyncer(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public TrackerLocationSyncer(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) { Tracker = tracker; ItemService = itemService; @@ -82,7 +83,7 @@ public bool ShowOutOfLogicLocations } } - public Tracker Tracker { get; private set; } + public ITracker Tracker { get; private set; } public IItemService ItemService { get; private set; } diff --git a/src/Randomizer.App/Windows/GenerateRomWindow.xaml b/src/Randomizer.App/Windows/GenerateRomWindow.xaml index 1fcaa3fd1..f5454fab4 100644 --- a/src/Randomizer.App/Windows/GenerateRomWindow.xaml +++ b/src/Randomizer.App/Windows/GenerateRomWindow.xaml @@ -9,6 +9,7 @@ xmlns:shared="clr-namespace:Randomizer.Shared;assembly=Randomizer.Shared" xmlns:options="clr-namespace:Randomizer.Data.Options;assembly=Randomizer.Data" xmlns:windows="clr-namespace:Randomizer.App.Windows" + xmlns:enums="clr-namespace:Randomizer.Shared.Enums;assembly=Randomizer.Shared" mc:Ignorable="d" Loaded="GenerateRomWindow_OnLoaded" Closing="Window_Closing" @@ -66,7 +67,7 @@ diff --git a/src/Randomizer.App/Windows/MsuTrackWindow.xaml.cs b/src/Randomizer.App/Windows/MsuTrackWindow.xaml.cs index 367f15fd5..6775fc4d9 100644 --- a/src/Randomizer.App/Windows/MsuTrackWindow.xaml.cs +++ b/src/Randomizer.App/Windows/MsuTrackWindow.xaml.cs @@ -6,7 +6,9 @@ using System.Windows.Controls; using System.Windows.Media.Animation; using MSURandomizerLibrary.Configs; +using Randomizer.Abstractions; using Randomizer.Data.Options; +using Randomizer.Data.Tracking; using Randomizer.SMZ3.Tracking; namespace Randomizer.App.Windows; @@ -15,7 +17,7 @@ public partial class MsuTrackWindow : Window, IDisposable { private readonly DoubleAnimation _marquee = new(); private CancellationTokenSource _cts = new(); - private Tracker? _tracker; + private ITracker? _tracker; private RandomizerOptions? _options; private Track? _currentTrack; private Msu? _currentMsu; @@ -29,7 +31,7 @@ public MsuTrackWindow() App.RestoreWindowPositionAndSize(this); } - public void Init(Tracker tracker, RandomizerOptions options) + public void Init(ITracker tracker, RandomizerOptions options) { _tracker = tracker; _options = options; diff --git a/src/Randomizer.App/Windows/TrackerHelpWindow.xaml.cs b/src/Randomizer.App/Windows/TrackerHelpWindow.xaml.cs index 36c39cac8..a70ea7dfb 100644 --- a/src/Randomizer.App/Windows/TrackerHelpWindow.xaml.cs +++ b/src/Randomizer.App/Windows/TrackerHelpWindow.xaml.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Windows; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking; namespace Randomizer.App @@ -11,7 +11,7 @@ namespace Randomizer.App /// public partial class TrackerHelpWindow : Window { - public TrackerHelpWindow(Tracker tracker) + public TrackerHelpWindow(ITracker tracker) { SpeechRecognitionSyntax = tracker.Syntax; diff --git a/src/Randomizer.App/Windows/TrackerMapWindow.xaml.cs b/src/Randomizer.App/Windows/TrackerMapWindow.xaml.cs index bd0766e2d..08762345b 100644 --- a/src/Randomizer.App/Windows/TrackerMapWindow.xaml.cs +++ b/src/Randomizer.App/Windows/TrackerMapWindow.xaml.cs @@ -7,6 +7,7 @@ using System.Windows.Input; using System.Windows.Shapes; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.App.ViewModels; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.Data.Configuration.ConfigTypes; @@ -23,7 +24,7 @@ namespace Randomizer.App.Windows public partial class TrackerMapWindow : Window { private readonly ILogger _logger; - private readonly Tracker _tracker; + private readonly ITracker _tracker; private TrackerLocationSyncer _syncer; /// @@ -37,7 +38,7 @@ public partial class TrackerMapWindow : Window /// The config for the map json file with all the location details /// /// Logger for logging - public TrackerMapWindow(Tracker tracker, TrackerLocationSyncer syncer, TrackerMapConfig config, ILogger logger) + public TrackerMapWindow(ITracker tracker, TrackerLocationSyncer syncer, TrackerMapConfig config, ILogger logger) { InitializeComponent(); diff --git a/src/Randomizer.App/Windows/TrackerWindow.xaml.cs b/src/Randomizer.App/Windows/TrackerWindow.xaml.cs index 066f52a2d..89261fe33 100644 --- a/src/Randomizer.App/Windows/TrackerWindow.xaml.cs +++ b/src/Randomizer.App/Windows/TrackerWindow.xaml.cs @@ -16,8 +16,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MSURandomizerLibrary.Services; +using Randomizer.Abstractions; using Randomizer.Data.Configuration.ConfigTypes; using Randomizer.Data.Options; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData; using Randomizer.Data.WorldData.Regions; using Randomizer.Data.WorldData.Regions.Zelda; @@ -64,7 +66,7 @@ public partial class TrackerWindow : Window private UILayout _layout; private readonly UILayout _defaultLayout; private UILayout? _previousLayout; - private Tracker? _tracker; + private ITracker? _tracker; public TrackerWindow(IServiceProvider serviceProvider, IItemService itemService, @@ -118,7 +120,7 @@ protected enum Origin BottomRight = 3 } - public Tracker Tracker => _tracker ?? throw new InvalidOperationException("Tracker not created"); + public ITracker Tracker => _tracker ?? throw new InvalidOperationException("Tracker not created"); public GeneratedRom? Rom { get; set; } @@ -725,7 +727,7 @@ private void InitializeTracker() _msuLookupService.LookupMsus(_options.GeneralOptions.MsuPath); }); - _tracker = _serviceProvider.GetRequiredService(); + _tracker = _serviceProvider.GetRequiredService(); // If a rom was passed in with a valid tracker state, reload the state from the database if (GeneratedRom.IsValid(Rom)) @@ -987,7 +989,11 @@ private void Window_Closed(object sender, EventArgs e) _msuTrackWindow?.Close(true); _msuTrackWindow = null; _msuTrackWindow?.Dispose(); - _tracker?.Dispose(); + + if (_tracker is IDisposable disposable) + { + disposable.Dispose(); + } } private void LocationsMenuItem_Click(object sender, RoutedEventArgs e) diff --git a/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs b/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs index ee0541345..5f6d9b3c1 100644 --- a/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs +++ b/src/Randomizer.CrossPlatform/ConsoleTrackerDisplayService.cs @@ -1,6 +1,8 @@ using System.Text; using Microsoft.Extensions.DependencyInjection; +using Randomizer.Abstractions; using Randomizer.Data.Options; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData; using Randomizer.Data.WorldData.Regions; using Randomizer.Shared; @@ -20,7 +22,7 @@ public class ConsoleTrackerDisplayService private readonly TrackerOptionsAccessor _trackerOptionsAccessor; private readonly RandomizerOptions _options; private readonly IServiceProvider _serviceProvider; - private Tracker _tracker = null!; + private ITracker _tracker = null!; private World _world = null!; private IWorldService _worldService = null!; private Region _lastRegion = null!; @@ -40,7 +42,7 @@ 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 = _serviceProvider.GetRequiredService(); _tracker.Load(rom, romPath); _tracker.TryStartTracking(); _tracker.AutoTracker?.SetConnector(_options.AutoTrackerDefaultConnector, _options.AutoTrackerQUsb2SnesIp); diff --git a/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs b/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs index e21742e6c..8f05c8422 100644 --- a/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs +++ b/src/Randomizer.CrossPlatform/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using MSURandomizerLibrary; +using Randomizer.Abstractions; using Randomizer.Data.Configuration; using Randomizer.Data.Options; using Randomizer.Data.Services; @@ -24,9 +25,9 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi .AddOptionalModule() .AddOptionalModule() .AddOptionalModule() - .AddOptionalModule() - .AddOptionalModule(); - services.AddScoped(); + .AddOptionalModule(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddMultiplayerServices(); diff --git a/src/Randomizer.Data/Options/Config.cs b/src/Randomizer.Data/Options/Config.cs index 50aadd0ac..39cc51fdc 100644 --- a/src/Randomizer.Data/Options/Config.cs +++ b/src/Randomizer.Data/Options/Config.cs @@ -11,6 +11,7 @@ using Randomizer.Data.Logic; using Randomizer.Data.WorldData.Regions; using Randomizer.Shared; +using Randomizer.Shared.Enums; using Randomizer.Shared.Multiplayer; using JsonSerializer = System.Text.Json.JsonSerializer; diff --git a/src/Randomizer.Data/Options/PlandoConfig.cs b/src/Randomizer.Data/Options/PlandoConfig.cs index 35cc23508..b23a23978 100644 --- a/src/Randomizer.Data/Options/PlandoConfig.cs +++ b/src/Randomizer.Data/Options/PlandoConfig.cs @@ -7,6 +7,7 @@ using YamlDotNet.Serialization; using Randomizer.Data.Logic; +using Randomizer.Shared.Enums; namespace Randomizer.Data.Options { diff --git a/src/Randomizer.Data/Options/RandomizerOptions.cs b/src/Randomizer.Data/Options/RandomizerOptions.cs index 6b9adba5d..018b199e0 100644 --- a/src/Randomizer.Data/Options/RandomizerOptions.cs +++ b/src/Randomizer.Data/Options/RandomizerOptions.cs @@ -107,7 +107,9 @@ public static RandomizerOptions Load(string loadPath, string savePath, bool isYa if (isYaml) { - var serializer = new YamlDotNet.Serialization.Deserializer(); + var serializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); var options = serializer.Deserialize(fileText); options.FilePath = savePath; return options; diff --git a/src/Randomizer.Data/Tracking/AutoTrackerMetroidState.cs b/src/Randomizer.Data/Tracking/AutoTrackerMetroidState.cs new file mode 100644 index 000000000..75054aa67 --- /dev/null +++ b/src/Randomizer.Data/Tracking/AutoTrackerMetroidState.cs @@ -0,0 +1,115 @@ +namespace Randomizer.Data.Tracking; + +/// +/// Used to retrieve certain states based on the memory in Metroid +/// Seee https://jathys.zophar.net/supermetroid/kejardon/RAMMap.txt for details on the memory +/// +public class AutoTrackerMetroidState +{ + private readonly EmulatorMemoryData _data; + + /// + /// Constructor + /// + /// + public AutoTrackerMetroidState(EmulatorMemoryData data) + { + _data = data; + } + + /// + /// The overall room number the player is in + /// + public int CurrentRoom => _data.ReadUInt8(0x7E079B - 0x7E0750); + + /// + /// The region room number the player is in + /// + public int CurrentRoomInRegion => _data.ReadUInt8(0x7E079D - 0x7E0750); + + /// + /// The current region the player is in + /// + public int CurrentRegion => _data.ReadUInt8(0x7E079F - 0x7E0750); + + /// + /// The amount of energy/health + /// + public int Energy => _data.ReadUInt16(0x7E09C2 - 0x7E0750); + + /// + /// The amount currently in reserve tanks + /// + public int ReserveTanks => _data.ReadUInt16(0x7E09D6 - 0x7E0750); + + /// + /// The max of health + /// + public int MaxEnergy => _data.ReadUInt16(0x7E09C4 - 0x7E0750); + + /// + /// The max in reserve tanks + /// + public int MaxReserveTanks => _data.ReadUInt16(0x7E09D4 - 0x7E0750); + + /// + /// Samus's X Location + /// + public int SamusX => _data.ReadUInt16(0x7E0AF6 - 0x7E0750); + + /// + /// Samus's Y Location + /// + public int SamusY => _data.ReadUInt16(0x7E0AFA - 0x7E0750); + + /// + /// Samus's current super missile count + /// + public int SuperMissiles => _data.ReadUInt8(0x7E09CA - 0x7E0750); + + /// + /// Samus's max super missile count + /// + public int MaxSuperMissiles => _data.ReadUInt8(0x7E09CC - 0x7E0750); + + /// + /// Samus's current missile count + /// + public int Missiles => _data.ReadUInt8(0x7E09C6 - 0x7E0750); + + /// + /// Samus's max missile count + /// + public int MaxMissiles => _data.ReadUInt8(0x7E09C8 - 0x7E0750); + + /// + /// Samus's current power bomb count + /// + public int PowerBombs => _data.ReadUInt8(0x7E09CE - 0x7E0750); + + /// + /// Samus's max power bomb count + /// + public int MaxPowerBombs => _data.ReadUInt8(0x7E09D0 - 0x7E0750); + + public bool IsSamusInArea(int minX, int maxX, int minY, int maxY) + { + return SamusX >= minX && SamusX <= maxX && SamusY >= minY && SamusY <= maxY; + } + + /// + /// Checks to make sure that the state is valid and fully loaded. There's a period upon first booting up that + /// all of these are 0s, but some of the memory in the location data can be screwy. + /// + public bool IsValid => CurrentRoom != 0 || CurrentRegion != 0 || CurrentRoomInRegion != 0 || Energy != 0 || + SamusX != 0 || SamusY != 0; + + /// + /// Prints debug data for the state + /// + /// + public override string ToString() + { + return $"CurrentRoom: {CurrentRoom} | CurrentRoomInRegion: {CurrentRoomInRegion} | CurrentRegion: {CurrentRegion} | Health: {Energy},{ReserveTanks} | X,Y {SamusX},{SamusY}"; + } +} diff --git a/src/Randomizer.Data/Tracking/AutoTrackerViewedAction.cs b/src/Randomizer.Data/Tracking/AutoTrackerViewedAction.cs new file mode 100644 index 000000000..a6c075a90 --- /dev/null +++ b/src/Randomizer.Data/Tracking/AutoTrackerViewedAction.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace Randomizer.Data.Tracking; + +/// +/// Class for storing an action from viewing something +/// for a short amount of time +/// +public class AutoTrackerViewedAction +{ + private Action? _action; + + /// + /// Constructor + /// + /// + public AutoTrackerViewedAction(Action action) + { + _action = action; + _ = ExpireAsync(); + } + + /// + /// Expires the action after a period of time + /// + private async Task ExpireAsync() + { + await Task.Delay(TimeSpan.FromSeconds(15)); + _action = null; + } + + /// + /// Invokes the action, if it's still valid + /// + /// + public bool Invoke() + { + if (_action == null) return false; + _action.Invoke(); + return true; + } + + /// + /// If the action is valid + /// + public bool IsValid => _action != null; +} diff --git a/src/Randomizer.Data/Tracking/AutoTrackerZeldaState.cs b/src/Randomizer.Data/Tracking/AutoTrackerZeldaState.cs new file mode 100644 index 000000000..3ce3aeebb --- /dev/null +++ b/src/Randomizer.Data/Tracking/AutoTrackerZeldaState.cs @@ -0,0 +1,102 @@ +namespace Randomizer.Data.Tracking; + +/// +/// Used to retrieve certain states based on the memory in Zelda +/// See http://alttp.run/hacking/index.php?title=RAM:_Bank_0x7E:_Page_0x00 for details on the memory +/// These memory address values are the offset from 0x7E0000 +/// +public class AutoTrackerZeldaState +{ + private readonly EmulatorMemoryData _data; + + /// + /// Constructor + /// + /// + public AutoTrackerZeldaState(EmulatorMemoryData data) + { + _data = data; + } + + /// + /// The current room the player is in + /// + public int CurrentRoom => ReadUInt16(0xA0); + + /// + /// The previous room the player was in + /// + public int PreviousRoom => ReadUInt16(0xA2); + + /// + /// The state of the game (Overworld, Dungeon, etc.) + /// + public int State => ReadUInt8(0x10); + + /// + /// The secondary state value + /// + public int Substate => ReadUInt8(0x11); + + /// + /// The player's Y location + /// + public int LinkY => ReadUInt16(0x20); + + /// + /// The player's X Location + /// + public int LinkX => ReadUInt16(0x22); + + /// + /// What the player is currently doing + /// + public int LinkState => ReadUInt8(0x5D); + + /// + /// Value used to determine if the player is in the light or dark world + /// Apparently this is used for other calculations as well, so need to be a bit careful + /// Transitioning from Super Metroid also seems to break this until you go through a portal + /// + public int OverworldValue => ReadUInt8(0x7B); + + /// + /// True if Link is on the bottom half of the current room + /// + public bool IsOnBottomHalfOfRoom => ReadUInt8(0xAA) == 2; + + /// + /// True if Link is on the right half of the current room + /// + public bool IsOnRightHalfOfRoom => ReadUInt8(0xA9) == 1; + + /// + /// The overworld screen that the player is on + /// + public int OverworldScreen => ReadUInt16(0x8A); + + /// + /// Reads a specific block of memory + /// + /// The address offset from 0x7E0000 + /// + public int ReadUInt8(int address) => _data.ReadUInt8(address); + + /// + /// Reads a specific block of memory + /// + /// The address offset from 0x7E0000 + /// + public int ReadUInt16(int address) => _data.ReadUInt16(address); + + /// + /// Get debug string + /// + /// + public override string ToString() + { + var vertical = IsOnBottomHalfOfRoom ? "Bottom" : "Top"; + var horizontal = IsOnRightHalfOfRoom ? "Right" : "Left"; + return $"Room: {PreviousRoom}->{CurrentRoom} ({vertical}{horizontal}) | State: {State}/{Substate} | X,Y: {LinkX},{LinkY} | LinkState: {LinkState} | OW Screen: {OverworldScreen}"; + } +} diff --git a/src/Randomizer.Data/Tracking/BossTrackedEventArgs.cs b/src/Randomizer.Data/Tracking/BossTrackedEventArgs.cs new file mode 100644 index 000000000..25f3b3cc8 --- /dev/null +++ b/src/Randomizer.Data/Tracking/BossTrackedEventArgs.cs @@ -0,0 +1,27 @@ +using Randomizer.Data.WorldData; + +namespace Randomizer.Data.Tracking; + +/// +/// Provides data for events that occur when clearing a location. +/// +public class BossTrackedEventArgs : TrackerEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The boss that was tracked. + /// The speech recognition confidence. + /// If the location was automatically tracked + public BossTrackedEventArgs(Boss? boss, float? confidence, bool autoTracked) + : base(confidence, autoTracked) + { + Boss = boss; + } + + /// + /// Gets the boss that was tracked. + /// + public Boss? Boss { get; } +} diff --git a/src/Randomizer.Data/Tracking/DungeonTrackedEventArgs.cs b/src/Randomizer.Data/Tracking/DungeonTrackedEventArgs.cs new file mode 100644 index 000000000..2070ef346 --- /dev/null +++ b/src/Randomizer.Data/Tracking/DungeonTrackedEventArgs.cs @@ -0,0 +1,27 @@ +using Randomizer.Data.WorldData.Regions; + +namespace Randomizer.Data.Tracking; + +/// +/// Provides data for events that occur when tracking a dungeon. +/// +public class DungeonTrackedEventArgs : TrackerEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The dungeon that was tracked. + /// The speech recognition confidence. + /// If the location was automatically tracked + public DungeonTrackedEventArgs(IDungeon? dungeon, float? confidence, bool autoTracked) + : base(confidence, autoTracked) + { + Dungeon = dungeon; + } + + /// + /// Gets the boss that was tracked. + /// + public IDungeon? Dungeon { get; } +} diff --git a/src/Randomizer.Data/Tracking/EmulatorAction.cs b/src/Randomizer.Data/Tracking/EmulatorAction.cs new file mode 100644 index 000000000..58116d574 --- /dev/null +++ b/src/Randomizer.Data/Tracking/EmulatorAction.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using Randomizer.Data.WorldData; +using Randomizer.Shared.Enums; + +namespace Randomizer.Data.Tracking; + +/// +/// Class used for communicating between the emulator and tracker +/// +public class EmulatorAction +{ + /// + /// The action to be done by the emulator (read/write/etc) + /// + public EmulatorActionType Type { get; set; } + + /// + /// The starting memory address + /// + public int Address { get; set; } + + /// + /// The number of bytes to capture from the emulator + /// + public int Length { get; set; } + + /// + /// Values for writing + /// + public ICollection? WriteValues { get; set; } + + /// + /// The type of memory to read or modify (WRAM, CARTRAM, CARTROM) + /// + public MemoryDomain Domain { get; set; } + + /// + /// The game this message is for + /// + public Game Game { get; set; } = Game.Both; + + /// + /// Action to perform when getting a response for this from the emulator + /// + public Action? Action { get; set; } + + /// + /// The previous data collected for this action + /// + public EmulatorMemoryData? PreviousData { get; protected set; } + + /// + /// The latest data collected for this action + /// + public EmulatorMemoryData? CurrentData { get; protected set; } + + /// + /// The amount of time that should happen between consecutive reads + /// + public double FrequencySeconds = 0; + + /// + /// When this action was last executed + /// + public DateTime? LastRunTime; + + /// + /// Update the stored data and invoke the action + /// + /// The data collected from the emulator + public void Invoke(EmulatorMemoryData data) + { + PreviousData = CurrentData; + CurrentData = data; + LastRunTime = DateTime.Now; + Action?.Invoke(this); + } + + /// + /// If this message should be sent based on the game the player is currently in + /// + /// The game the player is currently in + /// If the player has actually started the game + /// True if the message should be sent. + public bool ShouldProcess(Game currentGame, bool hasStartedGame) + { + return HasExpired && ((!hasStartedGame && Game == Game.Neither) || (hasStartedGame && Game != Game.Neither && (Game == Game.Both || Game == currentGame))); + } + + /// + /// If the previous action's result has expired + /// + public bool HasExpired + { + get + { + return FrequencySeconds <= 0 || LastRunTime == null || + (DateTime.Now - LastRunTime.Value).TotalSeconds >= FrequencySeconds; + } + } + + /// + /// Checks if the data has changed between the previous and current collections + /// + /// True if the data has changed, false otherwise + public bool HasDataChanged() + { + return CurrentData != null && !CurrentData.Equals(PreviousData); + } + + /// + /// Cached set of locations for this action + /// + public ICollection? Locations { get; set; } + + /// + /// Clears both the previous and current data sets + /// + public void ClearData() + { + PreviousData = null; + CurrentData = null; + } + +} diff --git a/src/Randomizer.Data/Tracking/EmulatorMemoryData.cs b/src/Randomizer.Data/Tracking/EmulatorMemoryData.cs new file mode 100644 index 000000000..c30aadcd7 --- /dev/null +++ b/src/Randomizer.Data/Tracking/EmulatorMemoryData.cs @@ -0,0 +1,129 @@ +using System.Linq; + +namespace Randomizer.Data.Tracking; + +/// +/// Class used to house byte data retrieved from the emulator at a given point in time +/// Used to retrieve data at locations in memory +/// +public class EmulatorMemoryData +{ + private byte[] _bytes; + + /// + /// Constructor + /// + /// + public EmulatorMemoryData(byte[] bytes) + { + _bytes = bytes; + } + + /// + /// The raw byte array of the data + /// + public byte[] Raw + { + get + { + return _bytes; + } + } + + /// + /// Returns the memory value at a location + /// + /// The offset location to check + /// The value from the byte array at that location + public byte ReadUInt8(int location) + { + return _bytes[location]; + } + + /// + /// Gets the memory value for a location and returns if it matches a given flag + /// + /// The offset location to check + /// The flag to check against + /// True if the flag is set for the memory location. + public bool CheckBinary8Bit(int location, int flag) + { + return (ReadUInt8(location) & flag) == flag; + } + + /// + /// Checks if a value in memory matches a flag or has been increased to denote obtaining an item + /// + /// The previous data to compare to + /// The offset location to check + /// The flag to check against + /// True if the value in memory was set or increased + public bool CompareUInt8(EmulatorMemoryData previousData, int location, int? flag) + { + var prevValue = previousData != null && previousData._bytes.Length > location ? previousData.ReadUInt8(location) : -1; + var newValue = ReadUInt8(location); + + if (newValue > prevValue) + { + if (flag != null) + { + if ((newValue & flag) == flag) + { + return true; + } + } + else if (newValue > 0) + { + return true; + } + } + + return false; + } + + /// + /// Returns the memory value of two bytes / sixteen bits. Note that these are flipped + /// from what you may expect if you're thinking of it in binary terms. The second byte + /// is actually multiplied by 0xFF / 256 and added to the first + /// + /// The offset location to check + /// The value from the byte array at that location + public int ReadUInt16(int location) + { + return _bytes[location + 1] * 256 + _bytes[location]; + } + + /// + /// Checks if a binary flag is set for a given set of two bytes / sixteen bits + /// + /// The offset location to check + /// The flag to check against + /// True if the flag is set for the memory location. + public bool CheckUInt16(int location, int flag) + { + var data = ReadUInt16(location); + var adjustedFlag = 1 << flag; + var temp = data & adjustedFlag; + return temp == adjustedFlag; + } + + /// + /// Returns if this EmulatorMemoryData equals another + /// + /// + /// + public override bool Equals(object? other) + { + if (other is not EmulatorMemoryData otherData) return false; + return Enumerable.SequenceEqual(otherData._bytes, _bytes); + } + + /// + /// Returns the hash code of the bytes array + /// + /// + public override int GetHashCode() + { + return _bytes.GetHashCode(); + } +} diff --git a/src/Randomizer.Data/Tracking/ItemTrackedEventArgs.cs b/src/Randomizer.Data/Tracking/ItemTrackedEventArgs.cs new file mode 100644 index 000000000..6a24bf08c --- /dev/null +++ b/src/Randomizer.Data/Tracking/ItemTrackedEventArgs.cs @@ -0,0 +1,36 @@ +using Randomizer.Data.WorldData; + +namespace Randomizer.Data.Tracking; + +/// +/// Contains event data for item tracking events. +/// +public class ItemTrackedEventArgs : TrackerEventArgs +{ + /// + /// Initializes a new instance of the + /// class. + /// + /// The item that was tracked or untracked + /// + /// The name of the item that was tracked. + /// + /// The speech recognition confidence. + /// If the item was auto tracked + public ItemTrackedEventArgs(Item? item, string? trackedAs, float? confidence, bool autoTracked) + : base(confidence, autoTracked) + { + Item = item; + TrackedAs = trackedAs; + } + + /// + /// Gets the name of the item as it was tracked. + /// + public string? TrackedAs { get; } + + /// + /// The item that was tracked or untracked + /// + public Item? Item { get; } +} diff --git a/src/Randomizer.Data/Tracking/LocationClearedEventArgs.cs b/src/Randomizer.Data/Tracking/LocationClearedEventArgs.cs new file mode 100644 index 000000000..b2e06cfa2 --- /dev/null +++ b/src/Randomizer.Data/Tracking/LocationClearedEventArgs.cs @@ -0,0 +1,27 @@ +using Randomizer.Data.WorldData; + +namespace Randomizer.Data.Tracking; + +/// +/// Provides data for events that occur when clearing a location. +/// +public class LocationClearedEventArgs : TrackerEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The location that was cleared. + /// The speech recognition confidence. + /// If the location was automatically tracked + public LocationClearedEventArgs(Location location, float? confidence, bool autoTracked) + : base(confidence, autoTracked) + { + Location = location; + } + + /// + /// Gets the location that was cleared. + /// + public Location Location { get; } +} diff --git a/src/Randomizer.SMZ3.Tracking/TrackChangedEventArgs.cs b/src/Randomizer.Data/Tracking/TrackChangedEventArgs.cs similarity index 91% rename from src/Randomizer.SMZ3.Tracking/TrackChangedEventArgs.cs rename to src/Randomizer.Data/Tracking/TrackChangedEventArgs.cs index fbdbe7ae8..dbded79a6 100644 --- a/src/Randomizer.SMZ3.Tracking/TrackChangedEventArgs.cs +++ b/src/Randomizer.Data/Tracking/TrackChangedEventArgs.cs @@ -1,7 +1,7 @@ using System; using MSURandomizerLibrary.Configs; -namespace Randomizer.SMZ3.Tracking; +namespace Randomizer.Data.Tracking; public class TrackChangedEventArgs : EventArgs { diff --git a/src/Randomizer.SMZ3.Tracking/TrackNumberEventArgs.cs b/src/Randomizer.Data/Tracking/TrackNumberEventArgs.cs similarity index 85% rename from src/Randomizer.SMZ3.Tracking/TrackNumberEventArgs.cs rename to src/Randomizer.Data/Tracking/TrackNumberEventArgs.cs index 398f45d62..644443c27 100644 --- a/src/Randomizer.SMZ3.Tracking/TrackNumberEventArgs.cs +++ b/src/Randomizer.Data/Tracking/TrackNumberEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace Randomizer.SMZ3.Tracking; +namespace Randomizer.Data.Tracking; public class TrackNumberEventArgs : EventArgs { diff --git a/src/Randomizer.Data/Tracking/TrackerEventArgs.cs b/src/Randomizer.Data/Tracking/TrackerEventArgs.cs new file mode 100644 index 000000000..3e1b67f6f --- /dev/null +++ b/src/Randomizer.Data/Tracking/TrackerEventArgs.cs @@ -0,0 +1,60 @@ +using System; + +namespace Randomizer.Data.Tracking; + +/// +/// Contains event data for tracking events. +/// +public class TrackerEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the + /// class. + /// + /// The speech recognition confidence. + /// If the event was triggered by auto tracker + public TrackerEventArgs(float? confidence, bool autoTracked = false) + { + Confidence = confidence; + AutoTracked = autoTracked; + } + + /// + /// Initializes a new instance of the + /// class. + /// + /// The speech recognition confidence. + /// The phrase that was recognized. + public TrackerEventArgs(float? confidence, string? phrase) + { + Confidence = confidence; + Phrase = phrase; + } + + /// + /// Initializes a new instance of the + /// class. + /// + /// If the event was triggered by auto tracker + public TrackerEventArgs(bool autoTracked) + { + AutoTracked = autoTracked; + } + + /// + /// Gets the speech recognition confidence as a value between 0.0 and + /// 1.0, or null if the event was not initiated by speech + /// recognition. + /// + public float? Confidence { get; } + + /// + /// Gets the phrase Tracker recognized, or null. + /// + public string? Phrase { get; } + + /// + /// If the event was triggered by auto tracker + /// + public bool AutoTracked { get; init; } +} diff --git a/src/Randomizer.PatchBuilder/PatchBuilderService.cs b/src/Randomizer.PatchBuilder/PatchBuilderService.cs index 7692748b7..346339279 100644 --- a/src/Randomizer.PatchBuilder/PatchBuilderService.cs +++ b/src/Randomizer.PatchBuilder/PatchBuilderService.cs @@ -4,6 +4,7 @@ using MSURandomizerLibrary.Services; using Randomizer.Data.Options; using Randomizer.SMZ3.Generation; +using Randomizer.SMZ3.Infrastructure; namespace Randomizer.PatchBuilder; @@ -15,10 +16,11 @@ public class PatchBuilderService private readonly IMsuTypeService _msuTypeService; private readonly IMsuLookupService _msuLookupService; private readonly IMsuSelectorService _msuSelectorService; + private readonly RomLauncherService _romLauncherService; private readonly string _solutionPath; private readonly string _randomizerRomPath; - public PatchBuilderService(ILogger logger, RomGenerationService romGenerationService, OptionsFactory optionsFactory, IMsuLookupService msuLookupService, IMsuSelectorService msuSelectorService, IMsuTypeService msuTypeService) + public PatchBuilderService(ILogger logger, RomGenerationService romGenerationService, OptionsFactory optionsFactory, IMsuLookupService msuLookupService, IMsuSelectorService msuSelectorService, IMsuTypeService msuTypeService, RomLauncherService romLauncherService) { _logger = logger; _romGenerationService = romGenerationService; @@ -28,6 +30,7 @@ public PatchBuilderService(ILogger logger, RomGenerationSer _msuLookupService = msuLookupService; _msuSelectorService = msuSelectorService; _msuTypeService = msuTypeService; + _romLauncherService = romLauncherService; } public void CreatePatches(PatchBuilderConfig config) @@ -218,42 +221,8 @@ private void Launch(PatchBuilderConfig config) return; } - var launchApplication = config.EnvironmentSettings.LaunchApplication; - var launchArguments = ""; - if (string.IsNullOrEmpty(launchApplication)) - { - launchApplication = romPath; - } - else - { - if (string.IsNullOrEmpty(config.EnvironmentSettings.LaunchArguments)) - { - launchArguments = $"\"{romPath}\""; - } - else if (config.EnvironmentSettings.LaunchArguments.Contains("%rom%")) - { - launchArguments = config.EnvironmentSettings.LaunchArguments.Replace("%rom%", $"{romPath}"); - } - else - { - launchArguments = $"{config.EnvironmentSettings.LaunchArguments} \"{romPath}\""; - } - } - - try - { - _logger.LogInformation("Executing {FileName} {Arguments}", launchApplication, launchArguments); - Process.Start(new ProcessStartInfo - { - FileName = launchApplication, - Arguments = launchArguments, - UseShellExecute = true - }); - } - catch (Exception e) - { - _logger.LogError(e, "Unable to launch rom"); - } + _romLauncherService.LaunchRom(romPath, config.EnvironmentSettings.LaunchApplication, + config.EnvironmentSettings.LaunchArguments); } private static string SolutionPath diff --git a/src/Randomizer.PatchBuilder/Program.cs b/src/Randomizer.PatchBuilder/Program.cs index 900a5480e..41a5c3e5a 100644 --- a/src/Randomizer.PatchBuilder/Program.cs +++ b/src/Randomizer.PatchBuilder/Program.cs @@ -11,6 +11,7 @@ using Randomizer.Data.WorldData.Regions; using Randomizer.PatchBuilder; using Randomizer.Shared; +using Randomizer.SMZ3.Infrastructure; using Randomizer.Sprites; var serviceProvider = new ServiceCollection() @@ -25,6 +26,7 @@ .AddSingleton() .AddRandomizerServices() .AddTransient() + .AddTransient() .BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTracker.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTracker.cs index 4b0cc832a..9fb2c16ea 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTracker.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTracker.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Data.WorldData.Regions; using Randomizer.Data.WorldData.Regions.Zelda; using Randomizer.Data.WorldData; @@ -14,943 +15,939 @@ using Randomizer.SMZ3.Tracking.Services; using Randomizer.SMZ3.Tracking.VoiceCommands; using Randomizer.Data.Options; +using Randomizer.Data.Tracking; +using Randomizer.Shared.Enums; -namespace Randomizer.SMZ3.Tracking.AutoTracking +namespace Randomizer.SMZ3.Tracking.AutoTracking; + +/// +/// Manages the automated checking of the emulator memory for purposes of auto tracking +/// and other things using the appropriate connector (USB2SNES or Lura) based on user +/// preferences. +/// +public class AutoTracker : IAutoTracker { + private readonly ILogger _logger; + private readonly List _readActions = new(); + private readonly Dictionary _readActionMap = new(); + private readonly ILoggerFactory _loggerFactory; + private readonly IItemService _itemService; + private readonly IEnumerable _zeldaStateChecks; + private readonly IEnumerable _metroidStateChecks; + private readonly TrackerModuleFactory _trackerModuleFactory; + private readonly IRandomizerConfigService _config; + private readonly IWorldService _worldService; + private int _currentIndex; + private Game _previousGame; + private bool _hasStarted; + private bool _hasValidState; + private IEmulatorConnector? _connector; + private readonly Queue _sendActions = new(); + private CancellationTokenSource? _stopSendingMessages; + private int _numGTItems; + private bool _seenGTTorch; + private bool _foundGTKey; + private bool _beatBothBosses; + private string? _previousRom; + /// - /// Manages the automated checking of the emulator memory for purposes of auto tracking - /// and other things using the appropriate connector (USB2SNES or Lura) based on user - /// preferences. + /// Constructor for Auto Tracker /// - public class AutoTracker + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public AutoTracker(ILogger logger, + ILoggerFactory loggerFactory, + IItemService itemService, + IEnumerable zeldaStateChecks, + IEnumerable metroidStateChecks, + TrackerModuleFactory trackerModuleFactory, + IRandomizerConfigService randomizerConfigService, + IWorldService worldService, + ITracker tracker) { - private readonly ILogger _logger; - private readonly List _readActions = new(); - private readonly Dictionary _readActionMap = new(); - private readonly ILoggerFactory _loggerFactory; - private readonly IItemService _itemService; - private readonly IEnumerable _zeldaStateChecks; - private readonly IEnumerable _metroidStateChecks; - private readonly TrackerModuleFactory _trackerModuleFactory; - private readonly IRandomizerConfigService _config; - private readonly IWorldService _worldService; - private int _currentIndex; - private Game _previousGame; - private bool _hasStarted; - private bool _hasValidState; - private IEmulatorConnector? _connector; - private readonly Queue _sendActions = new(); - private CancellationTokenSource? _stopSendingMessages; - private int _numGTItems; - private bool _seenGTTorch; - private bool _foundGTKey; - private bool _beatBothBosses; - private string? _previousRom; - - /// - /// Constructor for Auto Tracker - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public AutoTracker(ILogger logger, - ILoggerFactory loggerFactory, - IItemService itemService, - IEnumerable zeldaStateChecks, - IEnumerable metroidStateChecks, - TrackerModuleFactory trackerModuleFactory, - IRandomizerConfigService randomizerConfigService, - IWorldService worldService, - Tracker tracker - ) - { - _logger = logger; - _loggerFactory = loggerFactory; - _itemService = itemService; - _trackerModuleFactory = trackerModuleFactory; - _config = randomizerConfigService; - _worldService = worldService; - Tracker = tracker; - - // Check if the game has started or not - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7e0020, - Length = 0x1, - Game = Game.Neither, - Action = CheckStarted - }); - - // Check whether the player is in Zelda or Metroid - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.CartRAM, - Address = 0xA173FE, - Length = 0x2, - Game = Game.Both, - Action = CheckGame - }); - - // Check Zelda rooms (caves, houses, dungeons) - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7ef000, - Length = 0x250, - Game = Game.Zelda, - FrequencySeconds = 1, - Action = CheckZeldaRooms - }); - - // Check Zelda overworld and NPC locations and inventory - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7ef280, - Length = 0x200, - Game = Game.Zelda, - FrequencySeconds = 1, - Action = CheckZeldaMisc - }); - - // Check Zelda state - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7e0000, - Length = 0x250, - Game = Game.Zelda, - Action = CheckZeldaState - }); - - // Check if Ganon is defeated - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.CartRAM, - Address = 0xA17400, - Length = 0x120, - Game = Game.Both, - FrequencySeconds = 1, - Action = CheckBeatFinalBosses - }); - - // Check Metroid locations - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7ed870, - Length = 0x20, - Game = Game.SM, - FrequencySeconds = 1, - Action = CheckMetroidLocations - }); - - // Check Metroid bosses - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7ed828, - Length = 0x08, - Game = Game.SM, - FrequencySeconds = 1, - Action = CheckMetroidBosses - }); - - // Check state of if the player has entered the ship - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7e0FB2, - Length = 0x2, - Game = Game.SM, - Action = CheckShip - }); - - // Check Metroid state - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7e0750, - Length = 0x400, - Game = Game.SM, - Action = CheckMetroidState - }); - - // Check for current Zelda song - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7E010B, - Length = 0x01, - Game = Game.Zelda, - FrequencySeconds = 2, - Action = action => Tracker.UpdateTrackNumber(action.CurrentData!.ReadUInt8(0)) - }); - - // Check for current Metroid song - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7E0332, - Length = 0x01, - Game = Game.SM, - FrequencySeconds = 2, - Action = action => Tracker.UpdateTrackNumber(action.CurrentData!.ReadUInt8(0)) - }); - - // Check for current title screen song - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.WRAM, - Address = 0x7E0331, - Length = 0x02, - Game = Game.Neither, - FrequencySeconds = 2, - Action = action => Tracker.UpdateTrackNumber(action.CurrentData!.ReadUInt8(1)) - }); - - // Get the number of items given to the player via the interactor - AddReadAction(new() - { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.CartRAM, - Address = 0xA26000, - Length = 0x300, - Game = Game.Both, - FrequencySeconds = 30, - Action = action => - { - Tracker.GameService?.SyncItems(action); - } - }); - - // Get the number of items given to the player via the interactor - AddReadAction(new() + _logger = logger; + _loggerFactory = loggerFactory; + _itemService = itemService; + _trackerModuleFactory = trackerModuleFactory; + _config = randomizerConfigService; + _worldService = worldService; + Tracker = tracker; + + // Check if the game has started or not + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7e0020, + Length = 0x1, + Game = Game.Neither, + Action = CheckStarted + }); + + // Check whether the player is in Zelda or Metroid + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.CartRAM, + Address = 0xA173FE, + Length = 0x2, + Game = Game.Both, + Action = CheckGame + }); + + // Check Zelda rooms (caves, houses, dungeons) + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7ef000, + Length = 0x250, + Game = Game.Zelda, + FrequencySeconds = 1, + Action = CheckZeldaRooms + }); + + // Check Zelda overworld and NPC locations and inventory + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7ef280, + Length = 0x200, + Game = Game.Zelda, + FrequencySeconds = 1, + Action = CheckZeldaMisc + }); + + // Check Zelda state + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7e0000, + Length = 0x250, + Game = Game.Zelda, + Action = CheckZeldaState + }); + + // Check if Ganon is defeated + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.CartRAM, + Address = 0xA17400, + Length = 0x120, + Game = Game.Both, + FrequencySeconds = 1, + Action = CheckBeatFinalBosses + }); + + // Check Metroid locations + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7ed870, + Length = 0x20, + Game = Game.SM, + FrequencySeconds = 1, + Action = CheckMetroidLocations + }); + + // Check Metroid bosses + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7ed828, + Length = 0x08, + Game = Game.SM, + FrequencySeconds = 1, + Action = CheckMetroidBosses + }); + + // Check state of if the player has entered the ship + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7e0FB2, + Length = 0x2, + Game = Game.SM, + Action = CheckShip + }); + + // Check Metroid state + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7e0750, + Length = 0x400, + Game = Game.SM, + Action = CheckMetroidState + }); + + // Check for current Zelda song + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7E010B, + Length = 0x01, + Game = Game.Zelda, + FrequencySeconds = 2, + Action = action => Tracker.UpdateTrackNumber(action.CurrentData!.ReadUInt8(0)) + }); + + // Check for current Metroid song + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7E0332, + Length = 0x01, + Game = Game.SM, + FrequencySeconds = 2, + Action = action => Tracker.UpdateTrackNumber(action.CurrentData!.ReadUInt8(0)) + }); + + // Check for current title screen song + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.WRAM, + Address = 0x7E0331, + Length = 0x02, + Game = Game.Neither, + FrequencySeconds = 2, + Action = action => Tracker.UpdateTrackNumber(action.CurrentData!.ReadUInt8(1)) + }); + + // Get the number of items given to the player via the interactor + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.CartRAM, + Address = 0xA26000, + Length = 0x300, + Game = Game.Both, + FrequencySeconds = 30, + Action = action => { - Type = EmulatorActionType.ReadBlock, - Domain = MemoryDomain.CartRAM, - Address = 0xA26300, - Length = 0x300, - Game = Game.Both, - FrequencySeconds = 30, - Action = action => - { - Tracker.GameService?.SyncItems(action); - } - }); + Tracker.GameService?.SyncItems(action); + } + }); - _zeldaStateChecks = zeldaStateChecks; - _metroidStateChecks = metroidStateChecks; - _logger.LogInformation("Zelda state checks: {ZeldaStateCount}", _zeldaStateChecks.Count()); - _logger.LogInformation("Metroid state checks: {MetroidStateCount}", _metroidStateChecks.Count()); - } + // Get the number of items given to the player via the interactor + AddReadAction(new() + { + Type = EmulatorActionType.ReadBlock, + Domain = MemoryDomain.CartRAM, + Address = 0xA26300, + Length = 0x300, + Game = Game.Both, + FrequencySeconds = 30, + Action = action => + { + Tracker.GameService?.SyncItems(action); + } + }); + + _zeldaStateChecks = zeldaStateChecks; + _metroidStateChecks = metroidStateChecks; + _logger.LogInformation("Zelda state checks: {ZeldaStateCount}", _zeldaStateChecks.Count()); + _logger.LogInformation("Metroid state checks: {MetroidStateCount}", _metroidStateChecks.Count()); + } - /// - /// The tracker associated with this auto tracker - /// - public Tracker Tracker { get; set; } + /// + /// The tracker associated with this auto tracker + /// + public ITracker Tracker { get; } - /// - /// The type of connector that the auto tracker is currently using - /// - public EmulatorConnectorType ConnectorType { get; protected set; } + /// + /// The type of connector that the auto tracker is currently using + /// + public EmulatorConnectorType ConnectorType { get; private set; } - /// - /// The game that the player is currently in - /// - public Game CurrentGame { get; protected set; } = Game.Neither; + /// + /// The game that the player is currently in + /// + public Game CurrentGame { get; private set; } = Game.Neither; - /// - /// The latest state that the player in LTTP (location, health, etc.) - /// - public AutoTrackerZeldaState? ZeldaState { get; protected set; } + /// + /// The latest state that the player in LTTP (location, health, etc.) + /// + public AutoTrackerZeldaState? ZeldaState { get; private set; } - /// - /// The latest state that the player in Super Metroid (location, health, etc.) - /// - public AutoTrackerMetroidState? MetroidState { get; protected set; } + /// + /// The latest state that the player in Super Metroid (location, health, etc.) + /// + public AutoTrackerMetroidState? MetroidState { get; private set; } - /// - /// Disables the current connector and creates the requested type - /// - public void SetConnector(EmulatorConnectorType type, string? qusb2SnesIp) + /// + /// Disables the current connector and creates the requested type + /// + public void SetConnector(EmulatorConnectorType type, string? qusb2SnesIp) + { + if (_connector != null) { - if (_connector != null) - { - _connector.Dispose(); - _connector = null; - } + _connector.Dispose(); + _connector = null; + } - if (type != EmulatorConnectorType.None) + if (type != EmulatorConnectorType.None) + { + if (type == EmulatorConnectorType.USB2SNES) { - if (type == EmulatorConnectorType.USB2SNES) - { - _connector = new USB2SNESConnector(_loggerFactory.CreateLogger(), qusb2SnesIp); - } - else - { - _connector = new LuaConnector(_loggerFactory.CreateLogger()); - } - ConnectorType = type; - _connector.OnConnected += Connector_Connected; - _connector.OnDisconnected += Connector_Disconnected; - _connector.MessageReceived += Connector_MessageReceived; - AutoTrackerEnabled?.Invoke(this, EventArgs.Empty); + _connector = new USB2SNESConnector(_loggerFactory.CreateLogger(), qusb2SnesIp); } else { - ConnectorType = EmulatorConnectorType.None; - AutoTrackerDisabled?.Invoke(this, EventArgs.Empty); + _connector = new LuaConnector(_loggerFactory.CreateLogger()); } + ConnectorType = type; + _connector.OnConnected += Connector_Connected; + _connector.OnDisconnected += Connector_Disconnected; + _connector.MessageReceived += Connector_MessageReceived; + AutoTrackerEnabled?.Invoke(this, EventArgs.Empty); + } + else + { + ConnectorType = EmulatorConnectorType.None; + AutoTrackerDisabled?.Invoke(this, EventArgs.Empty); } + } - /// - /// Occurs when the tracker's auto tracker is enabled - /// - public event EventHandler? AutoTrackerEnabled; - - /// - /// Occurs when the tracker's auto tracker is disabled - /// - public event EventHandler? AutoTrackerDisabled; - - /// - /// Occurs when the tracker's auto tracker is connected - /// - public event EventHandler? AutoTrackerConnected; - - /// - /// Occurs when the tracker's auto tracker is disconnected - /// - public event EventHandler? AutoTrackerDisconnected; - - /// - /// Occurs when the MSU track number has changed - /// - public event EventHandler? TrackNumberUpdated; - - /// - /// The action to run when the player asks Tracker to look at the game - /// - public AutoTrackerViewedAction? LatestViewAction; - - /// - /// If a connector is currently enabled - /// - public bool IsEnabled => _connector != null; - - /// - /// If a connector is currently connected to the emulator - /// - public bool IsConnected => _connector != null && _connector.IsConnected(); - - /// - /// If a connector is currently connected to the emulator and a valid game state is detected - /// - public bool HasValidState => IsConnected && _hasValidState; - - /// - /// If the auto tracker is currently sending messages - /// - public bool IsSendingMessages { get; set; } - - /// - /// If the player currently has a fairy - /// - public bool PlayerHasFairy { get; protected set; } - - /// - /// If the user is activately in an SMZ3 rom - /// - public bool IsInSMZ3 => string.IsNullOrEmpty(_previousRom) || _previousRom.StartsWith("SMZ3 Cas"); - - /// - /// Called when the connector successfully established a connection with the emulator - /// - /// - /// - protected async void Connector_Connected(object? sender, EventArgs e) - { - _logger.LogInformation("Connector Connected"); - await Task.Delay(TimeSpan.FromSeconds(0.1f)); - if (!IsSendingMessages) - { - _logger.LogInformation("Start sending messages"); - Tracker.Say(x => x.AutoTracker.WhenConnected); - AutoTrackerConnected?.Invoke(this, EventArgs.Empty); - _stopSendingMessages = new CancellationTokenSource(); - _ = SendMessagesAsync(_stopSendingMessages.Token); - _currentIndex = 0; - } + /// + /// Occurs when the tracker's auto tracker is enabled + /// + public event EventHandler? AutoTrackerEnabled; + + /// + /// Occurs when the tracker's auto tracker is disabled + /// + public event EventHandler? AutoTrackerDisabled; + + /// + /// Occurs when the tracker's auto tracker is connected + /// + public event EventHandler? AutoTrackerConnected; + + /// + /// Occurs when the tracker's auto tracker is disconnected + /// + public event EventHandler? AutoTrackerDisconnected; + + /// + /// The action to run when the player asks Tracker to look at the game + /// + public AutoTrackerViewedAction? LatestViewAction { get; set; } + + /// + /// If a connector is currently enabled + /// + public bool IsEnabled => _connector != null; + + /// + /// If a connector is currently connected to the emulator + /// + public bool IsConnected => _connector != null && _connector.IsConnected(); + + /// + /// If a connector is currently connected to the emulator and a valid game state is detected + /// + public bool HasValidState => IsConnected && _hasValidState; + + /// + /// If the auto tracker is currently sending messages + /// + public bool IsSendingMessages { get; set; } + + /// + /// If the player currently has a fairy + /// + public bool PlayerHasFairy { get; private set; } + + /// + /// If the user is activately in an SMZ3 rom + /// + public bool IsInSMZ3 => string.IsNullOrEmpty(_previousRom) || _previousRom.StartsWith("SMZ3 Cas"); + + /// + /// Called when the connector successfully established a connection with the emulator + /// + /// + /// + private async void Connector_Connected(object? sender, EventArgs e) + { + _logger.LogInformation("Connector Connected"); + await Task.Delay(TimeSpan.FromSeconds(0.1f)); + if (!IsSendingMessages) + { + _logger.LogInformation("Start sending messages"); + Tracker.Say(x => x.AutoTracker.WhenConnected); + AutoTrackerConnected?.Invoke(this, EventArgs.Empty); + _stopSendingMessages = new CancellationTokenSource(); + _ = SendMessagesAsync(_stopSendingMessages.Token); + _currentIndex = 0; } + } + + /// + /// Writes a particular action to the emulator memory + /// + /// The action to write to memory + public void WriteToMemory(EmulatorAction action) + { + _sendActions.Enqueue(action); + } - /// - /// Writes a particular action to the emulator memory - /// - /// The action to write to memory - public void WriteToMemory(EmulatorAction action) + /// + /// Called when a connector has temporarily lost connection with the emulator + /// + /// + /// + private void Connector_Disconnected(object? sender, EventArgs e) + { + Tracker.Say(x => x.AutoTracker.WhenDisconnected); + _logger.LogInformation("Disconnected"); + AutoTrackerDisconnected?.Invoke(this, EventArgs.Empty); + _stopSendingMessages?.Cancel(); + + // Reset everything once + IsSendingMessages = false; + foreach (var action in _readActions) { - _sendActions.Enqueue(action); + action.ClearData(); } + _sendActions.Clear(); + CurrentGame = Game.Neither; + _hasValidState = false; + } - /// - /// Called when a connector has temporarily lost connection with the emulator - /// - /// - /// - protected void Connector_Disconnected(object? sender, EventArgs e) + /// + /// The connector has received memory from the emulator + /// + /// + /// + private void Connector_MessageReceived(object? sender, EmulatorDataReceivedEventArgs e) + { + // If the user is playing SMZ3 (if we don't have the name, assume that they are) + if (string.IsNullOrEmpty(e.RomName) || e.RomName.StartsWith("SMZ3 Cas")) { - Tracker.Say(x => x.AutoTracker.WhenDisconnected); - _logger.LogInformation("Disconnected"); - AutoTrackerDisconnected?.Invoke(this, EventArgs.Empty); - _stopSendingMessages?.Cancel(); + if (!string.IsNullOrEmpty(_previousRom) && e.RomName != _previousRom) + { + _logger.LogInformation("Changed to SMZ3 rom {RomName} ({RomHash})", e.RomName,e.RomHash); + Tracker.Say(x => x.AutoTracker.SwitchedToSMZ3Rom); + } - // Reset everything once - IsSendingMessages = false; - foreach (var action in _readActions) + // Verify that message we received is still valid, then execute + if (_readActionMap[e.Address].ShouldProcess(CurrentGame, _hasStarted)) { - action.ClearData(); + _readActionMap[e.Address].Invoke(e.Data); } - _sendActions.Clear(); - CurrentGame = Game.Neither; - _hasValidState = false; } - - /// - /// The connector has received memory from the emulator - /// - /// - /// - protected void Connector_MessageReceived(object? sender, EmulatorDataReceivedEventArgs e) + // If the user is switching to a non-SMZ3 rom + else if (!string.IsNullOrEmpty(e.RomName) && e.RomName != _previousRom) { - // If the user is playing SMZ3 (if we don't have the name, assume that they are) - if (string.IsNullOrEmpty(e.RomName) || e.RomName.StartsWith("SMZ3 Cas")) - { - if (!string.IsNullOrEmpty(_previousRom) && e.RomName != _previousRom) - { - _logger.LogInformation("Changed to SMZ3 rom {RomName} ({RomHash})", e.RomName,e.RomHash); - Tracker.Say(x => x.AutoTracker.SwitchedToSMZ3Rom); - } + _logger.LogInformation("Ignoring rom {RomName} ({RomHash})", e.RomName,e.RomHash); - // Verify that message we received is still valid, then execute - if (_readActionMap[e.Address].ShouldProcess(CurrentGame, _hasStarted)) - { - _readActionMap[e.Address].Invoke(e.Data); - } - } - // If the user is switching to a non-SMZ3 rom - else if (!string.IsNullOrEmpty(e.RomName) && e.RomName != _previousRom) + var key = "Unknown"; + if (Tracker.Responses.AutoTracker.SwitchedToOtherRom.ContainsKey(e.RomHash!)) { - _logger.LogInformation("Ignoring rom {RomName} ({RomHash})", e.RomName,e.RomHash); - - var key = "Unknown"; - if (Tracker.Responses.AutoTracker.SwitchedToOtherRom.ContainsKey(e.RomHash!)) - { - key = e.RomHash!; - } - - Tracker.Say(x => x.AutoTracker.SwitchedToOtherRom[key]); + key = e.RomHash!; } - _previousRom = e.RomName; + Tracker.Say(x => x.AutoTracker.SwitchedToOtherRom[key]); } - /// - /// Sends requests out to the connected lua script - /// - protected async Task SendMessagesAsync(CancellationToken cancellationToken) + _previousRom = e.RomName; + } + + /// + /// Sends requests out to the connected lua script + /// + private async Task SendMessagesAsync(CancellationToken cancellationToken) + { + Thread.CurrentThread.Name = DateTime.Now.ToString(CultureInfo.InvariantCulture); + _logger.LogInformation("Start sending messages {ThreadName}", Thread.CurrentThread.Name); + IsSendingMessages = true; + while (_connector != null && _connector.IsConnected() && !cancellationToken.IsCancellationRequested) { - Thread.CurrentThread.Name = DateTime.Now.ToString(CultureInfo.InvariantCulture); - _logger.LogInformation("Start sending messages {ThreadName}", Thread.CurrentThread.Name); - IsSendingMessages = true; - while (_connector != null && _connector.IsConnected() && !cancellationToken.IsCancellationRequested) + if (_connector.CanSendMessage()) { - if (_connector.CanSendMessage()) + if (_sendActions.Count > 0) { - if (_sendActions.Count > 0) - { - var nextAction = _sendActions.Dequeue(); + var nextAction = _sendActions.Dequeue(); - if (nextAction.ShouldProcess(CurrentGame, _hasStarted)) - { - _connector.SendMessage(nextAction); - } + if (nextAction.ShouldProcess(CurrentGame, _hasStarted)) + { + _connector.SendMessage(nextAction); } - else + } + else + { + while (!_readActions[_currentIndex].ShouldProcess(CurrentGame, _hasStarted)) { - while (!_readActions[_currentIndex].ShouldProcess(CurrentGame, _hasStarted)) - { - _currentIndex = (_currentIndex + 1) % _readActions.Count; - } - _connector.SendMessage(_readActions[_currentIndex]); _currentIndex = (_currentIndex + 1) % _readActions.Count; } + _connector.SendMessage(_readActions[_currentIndex]); + _currentIndex = (_currentIndex + 1) % _readActions.Count; } - - await Task.Delay(TimeSpan.FromSeconds(0.1f), cancellationToken); } - IsSendingMessages = false; - _logger.LogInformation("Stop sending messages {ThreadName}", Thread.CurrentThread.Name); - } - /// - /// Adds a read action to repeatedly call out to the emulator - /// - /// - protected void AddReadAction(EmulatorAction action) - { - _readActions.Add(action); - _readActionMap.Add(action.Address, action); + await Task.Delay(TimeSpan.FromSeconds(0.1f), cancellationToken); } + IsSendingMessages = false; + _logger.LogInformation("Stop sending messages {ThreadName}", Thread.CurrentThread.Name); + } - /// - /// Check if the player has started playing the game - /// - /// - protected void CheckStarted(EmulatorAction action) - { - if (action.CurrentData == null) return; - var value = action.CurrentData.ReadUInt8(0); - if (value != 0 && !_hasStarted) - { - _logger.LogInformation("Game started"); - _hasStarted = true; - - if (Tracker.World.Config.MultiWorld && _worldService.Worlds.Count > 1) - { - var worldCount = _worldService.Worlds.Count; - var otherPlayerName = _worldService.Worlds.Where(x => x != _worldService.World).Random(new Random())!.Config.PhoneticName; - Tracker.Say(x => x.AutoTracker.GameStartedMultiplayer, worldCount, otherPlayerName); - } - else - { - Tracker.Say(x => x.AutoTracker.GameStarted, Tracker.Rom?.Seed); - } - } - } + /// + /// Adds a read action to repeatedly call out to the emulator + /// + /// + private void AddReadAction(EmulatorAction action) + { + _readActions.Add(action); + _readActionMap.Add(action.Address, action); + } - /// - /// Checks which game the player is currently in - /// - /// - protected void CheckGame(EmulatorAction action) + /// + /// Check if the player has started playing the game + /// + /// + private void CheckStarted(EmulatorAction action) + { + if (action.CurrentData == null) return; + var value = action.CurrentData.ReadUInt8(0); + if (value != 0 && !_hasStarted) { - if (action.CurrentData == null) return; - _previousGame = CurrentGame; - var value = action.CurrentData.ReadUInt8(0); - if (value == 0x00) - { - CurrentGame = Game.Zelda; - _hasValidState = true; - } - else if (value == 0xFF) - { - CurrentGame = Game.SM; - } - else if (value == 0x11) + _logger.LogInformation("Game started"); + _hasStarted = true; + + if (Tracker.World.Config.MultiWorld && _worldService.Worlds.Count > 1) { - CurrentGame = Game.Credits; - Tracker.UpdateTrackNumber(99); + var worldCount = _worldService.Worlds.Count; + var otherPlayerName = _worldService.Worlds.Where(x => x != _worldService.World).Random(new Random())!.Config.PhoneticName; + Tracker.Say(x => x.AutoTracker.GameStartedMultiplayer, worldCount, otherPlayerName); } - if (_previousGame != CurrentGame) + else { - _logger.LogInformation("Game changed to: {CurrentGame}", CurrentGame); + Tracker.Say(x => x.AutoTracker.GameStarted, Tracker.Rom?.Seed); } } + } - /// - /// Checks if the player has cleared Zelda room locations (cave, houses, dungeons) - /// This also checks if the player has gotten the dungeon rewards - /// - /// - protected void CheckZeldaRooms(EmulatorAction action) + /// + /// Checks which game the player is currently in + /// + /// + private void CheckGame(EmulatorAction action) + { + if (action.CurrentData == null) return; + _previousGame = CurrentGame; + var value = action.CurrentData.ReadUInt8(0); + if (value == 0x00) { - if (!_hasValidState) return; - if (action.CurrentData == null || action.PreviousData == null) return; - CheckLocations(action, LocationMemoryType.Default, true, Game.Zelda); - CheckDungeons(action.CurrentData, action.PreviousData); + CurrentGame = Game.Zelda; + _hasValidState = true; } - - /// - /// Checks if the player has cleared misc Zelda locations (overworld, NPCs) - /// Also where you can check inventory (inventory starts at an offset of 0x80) - /// - /// - protected void CheckZeldaMisc(EmulatorAction action) + else if (value == 0xFF) { - if (!_hasValidState) return; - if (action.CurrentData == null || action.PreviousData == null) return; - // Failsafe to prevent incorrect checking - if (action.CurrentData?.ReadUInt8(0x190) == 0xFF && action.CurrentData?.ReadUInt8(0x191) == 0xFF) - { - _logger.LogInformation("Ignoring due to transition"); - return; - } + CurrentGame = Game.SM; + } + else if (value == 0x11) + { + CurrentGame = Game.Credits; + Tracker.UpdateTrackNumber(99); + } + if (_previousGame != CurrentGame) + { + _logger.LogInformation("Game changed to: {CurrentGame}", CurrentGame); + } + } - CheckLocations(action, LocationMemoryType.ZeldaMisc, false, Game.Zelda); + /// + /// Checks if the player has cleared Zelda room locations (cave, houses, dungeons) + /// This also checks if the player has gotten the dungeon rewards + /// + /// + private void CheckZeldaRooms(EmulatorAction action) + { + if (!_hasValidState) return; + if (action.CurrentData == null || action.PreviousData == null) return; + CheckLocations(action, LocationMemoryType.Default, true, Game.Zelda); + CheckDungeons(action.CurrentData, action.PreviousData); + } - PlayerHasFairy = false; - for (var i = 0; i < 4; i++) - { - PlayerHasFairy |= action.CurrentData?.ReadUInt8(0xDC + i) == 6; - } + /// + /// Checks if the player has cleared misc Zelda locations (overworld, NPCs) + /// Also where you can check inventory (inventory starts at an offset of 0x80) + /// + /// + private void CheckZeldaMisc(EmulatorAction action) + { + if (!_hasValidState) return; + if (action.CurrentData == null || action.PreviousData == null) return; + // Failsafe to prevent incorrect checking + if (action.CurrentData?.ReadUInt8(0x190) == 0xFF && action.CurrentData?.ReadUInt8(0x191) == 0xFF) + { + _logger.LogInformation("Ignoring due to transition"); + return; + } - // Activated flute - if (action.CurrentData?.CheckBinary8Bit(0x10C, 0x01) == true && action.PreviousData?.CheckBinary8Bit(0x10C, 0x01) != true) - { - var duckItem = _itemService.FirstOrDefault("Duck"); - if (duckItem?.State.TrackingState == 0) - { - Tracker.TrackItem(duckItem, null, null, false, true); - } - } + CheckLocations(action, LocationMemoryType.ZeldaMisc, false, Game.Zelda); - // Check if the player cleared Aga - if (action.CurrentData?.ReadUInt8(0x145) >= 3) - { - var castleTower = Tracker.World.CastleTower; - if (castleTower.DungeonState.Cleared == false) - { - Tracker.MarkDungeonAsCleared(castleTower, null, autoTracked: true); - _logger.LogInformation("Auto tracked {Name} as cleared", castleTower.Name); - } - } + PlayerHasFairy = false; + for (var i = 0; i < 4; i++) + { + PlayerHasFairy |= action.CurrentData?.ReadUInt8(0xDC + i) == 6; } - /// - /// Checks to see if the player has cleared locations in Super Metroid - /// - /// - protected void CheckMetroidLocations(EmulatorAction action) + // Activated flute + if (action.CurrentData?.CheckBinary8Bit(0x10C, 0x01) == true && action.PreviousData?.CheckBinary8Bit(0x10C, 0x01) != true) { - if (!_hasValidState) return; - if (action.CurrentData != null && action.PreviousData != null) + var duckItem = _itemService.FirstOrDefault("Duck"); + if (duckItem?.State.TrackingState == 0) { - CheckLocations(action, LocationMemoryType.Default, false, Game.SM); + Tracker.TrackItem(duckItem, null, null, false, true); } } - /// - /// Checks if the player has defeated Super Metroid bosses - /// - /// - protected void CheckMetroidBosses(EmulatorAction action) + // Check if the player cleared Aga + if (action.CurrentData?.ReadUInt8(0x145) >= 3) { - if (!_hasValidState) return; - if (action.CurrentData != null && action.PreviousData != null) + var castleTower = Tracker.World.CastleTower; + if (castleTower.DungeonState.Cleared == false) { - CheckSMBosses(action.CurrentData); + Tracker.MarkDungeonAsCleared(castleTower, null, autoTracked: true); + _logger.LogInformation("Auto tracked {Name} as cleared", castleTower.Name); } } + } + + /// + /// Checks to see if the player has cleared locations in Super Metroid + /// + /// + private void CheckMetroidLocations(EmulatorAction action) + { + if (!_hasValidState) return; + if (action.CurrentData != null && action.PreviousData != null) + { + CheckLocations(action, LocationMemoryType.Default, false, Game.SM); + } + } - /// - /// Checks locations to see if they have accessed or not - /// - /// The emulator action with the emulator memory data - /// The type of location to find the correct LocationInfo objects - /// Set to true if this is a 16 bit value or false for 8 bit - /// The game that is being checked - protected void CheckLocations(EmulatorAction action, LocationMemoryType type, bool is16Bit, Game game) + /// + /// Checks if the player has defeated Super Metroid bosses + /// + /// + private void CheckMetroidBosses(EmulatorAction action) + { + if (!_hasValidState) return; + if (action.CurrentData != null && action.PreviousData != null) { - var currentData = action.CurrentData; - var prevData = action.PreviousData; + CheckSMBosses(action.CurrentData); + } + } + + /// + /// Checks locations to see if they have accessed or not + /// + /// The emulator action with the emulator memory data + /// The type of location to find the correct LocationInfo objects + /// Set to true if this is a 16 bit value or false for 8 bit + /// The game that is being checked + private void CheckLocations(EmulatorAction action, LocationMemoryType type, bool is16Bit, Game game) + { + var currentData = action.CurrentData; + var prevData = action.PreviousData; - if (currentData == null || prevData == null) return; + if (currentData == null || prevData == null) return; - // Store the locations for this action so that we don't need to grab them each time every half a second or so - action.Locations ??= _worldService.AllLocations().Where(x => - x.MemoryType == type && ((game == Game.SM && (int)x.Id < 256) || (game == Game.Zelda && (int)x.Id >= 256))).ToList(); + // Store the locations for this action so that we don't need to grab them each time every half a second or so + action.Locations ??= _worldService.AllLocations().Where(x => + x.MemoryType == type && ((game == Game.SM && (int)x.Id < 256) || (game == Game.Zelda && (int)x.Id >= 256))).ToList(); - foreach (var location in action.Locations) + foreach (var location in action.Locations) + { + try { - try + var loc = location.MemoryAddress ?? 0; + var flag = location.MemoryFlag ?? 0; + var currentCleared = (is16Bit && currentData.CheckUInt16(loc * 2, flag)) || (!is16Bit && currentData.CheckBinary8Bit(loc, flag)); + var prevCleared = (is16Bit && prevData.CheckUInt16(loc * 2, flag)) || (!is16Bit && prevData.CheckBinary8Bit(loc, flag)); + if (location.State.Autotracked == false && currentCleared && prevCleared) { - var loc = location.MemoryAddress ?? 0; - var flag = location.MemoryFlag ?? 0; - var currentCleared = (is16Bit && currentData.CheckUInt16(loc * 2, flag)) || (!is16Bit && currentData.CheckBinary8Bit(loc, flag)); - var prevCleared = (is16Bit && prevData.CheckUInt16(loc * 2, flag)) || (!is16Bit && prevData.CheckBinary8Bit(loc, flag)); - if (location.State.Autotracked == false && currentCleared && prevCleared) + // Increment GT guessing game number + if (location.Region is GanonsTower gt && location != gt.BobsTorch) { - // Increment GT guessing game number - if (location.Region is GanonsTower gt && location != gt.BobsTorch) - { - IncrementGTItems(location); - } - - var item = location.Item; - location.State.Autotracked = true; - Tracker.TrackItem(item: item, trackedAs: null, confidence: null, tryClear: true, autoTracked: true, location: location); - _logger.LogInformation("Auto tracked {ItemName} from {LocationName}", location.Item.Name, location.Name); - - // Mark HC as cleared if this was Zelda's Cell - if (location.Id == LocationId.HyruleCastleZeldasCell && Tracker.World.HyruleCastle.DungeonState.Cleared == false) - { - Tracker.MarkDungeonAsCleared(Tracker.World.HyruleCastle, null, autoTracked: true); - } + IncrementGTItems(location); } + var item = location.Item; + location.State.Autotracked = true; + Tracker.TrackItem(item: item, trackedAs: null, confidence: null, tryClear: true, autoTracked: true, location: location); + _logger.LogInformation("Auto tracked {ItemName} from {LocationName}", location.Item.Name, location.Name); + + // Mark HC as cleared if this was Zelda's Cell + if (location.Id == LocationId.HyruleCastleZeldasCell && Tracker.World.HyruleCastle.DungeonState.Cleared == false) + { + Tracker.MarkDungeonAsCleared(Tracker.World.HyruleCastle, null, autoTracked: true); + } } - catch (Exception e) - { - _logger.LogError(e, "Unable to auto track location: {LocationName}", location.Name); - Tracker.Error(); - } + + } + catch (Exception e) + { + _logger.LogError(e, "Unable to auto track location: {LocationName}", location.Name); + Tracker.Error(); } } + } - /// - /// Checks the status of if dungeons have been cleared - /// - /// The latest memory data returned from the emulator - /// The previous memory data returned from the emulator - protected void CheckDungeons(EmulatorMemoryData currentData, EmulatorMemoryData prevData) + /// + /// Checks the status of if dungeons have been cleared + /// + /// The latest memory data returned from the emulator + /// The previous memory data returned from the emulator + private void CheckDungeons(EmulatorMemoryData currentData, EmulatorMemoryData prevData) + { + foreach (var dungeon in Tracker.World.Dungeons) { - foreach (var dungeon in Tracker.World.Dungeons) + var region = (Z3Region)dungeon; + + // Skip if we don't have any memory addresses saved for this dungeon + if (region.MemoryAddress == null || region.MemoryFlag == null) { - var region = (Z3Region)dungeon; + continue; + } - // Skip if we don't have any memory addresses saved for this dungeon - if (region.MemoryAddress == null || region.MemoryFlag == null) + try + { + var prevValue = prevData.CheckUInt16((int)(region.MemoryAddress * 2), region.MemoryFlag ?? 0); + var currentValue = currentData.CheckUInt16((int)(region.MemoryAddress * 2), region.MemoryFlag ?? 0); + if (dungeon.DungeonState.AutoTracked == false && prevValue && currentValue) { - continue; + dungeon.DungeonState.AutoTracked = true; + Tracker.MarkDungeonAsCleared(dungeon, autoTracked: true); + _logger.LogInformation("Auto tracked {DungeonName} as cleared", dungeon.DungeonName); } - try - { - var prevValue = prevData.CheckUInt16((int)(region.MemoryAddress * 2), region.MemoryFlag ?? 0); - var currentValue = currentData.CheckUInt16((int)(region.MemoryAddress * 2), region.MemoryFlag ?? 0); - if (dungeon.DungeonState.AutoTracked == false && prevValue && currentValue) - { - dungeon.DungeonState.AutoTracked = true; - Tracker.MarkDungeonAsCleared(dungeon, autoTracked: true); - _logger.LogInformation("Auto tracked {DungeonName} as cleared", dungeon.DungeonName); - } - - } - catch (Exception e) - { - _logger.LogError(e, "Unable to auto track Dungeon: {DungeonName}", dungeon.DungeonName); - Tracker.Error(); - } + } + catch (Exception e) + { + _logger.LogError(e, "Unable to auto track Dungeon: {DungeonName}", dungeon.DungeonName); + Tracker.Error(); } } + } - /// - /// Checks the status of if the Super Metroid bosses have been defeated - /// - /// The response from the lua script - protected void CheckSMBosses(EmulatorMemoryData data) + /// + /// Checks the status of if the Super Metroid bosses have been defeated + /// + /// The response from the lua script + private void CheckSMBosses(EmulatorMemoryData data) + { + foreach (var boss in Tracker.World.AllBosses.Where(x => x.Metadata.MemoryAddress != null && x.Metadata.MemoryFlag > 0 && !x.State.AutoTracked)) { - foreach (var boss in Tracker.World.AllBosses.Where(x => x.Metadata.MemoryAddress != null && x.Metadata.MemoryFlag > 0 && !x.State.AutoTracked)) + if (data.CheckBinary8Bit(boss.Metadata.MemoryAddress ?? 0, boss.Metadata.MemoryFlag ?? 100)) { - if (data.CheckBinary8Bit(boss.Metadata.MemoryAddress ?? 0, boss.Metadata.MemoryFlag ?? 100)) - { - boss.State.AutoTracked = true; - Tracker.MarkBossAsDefeated(boss, true, null, true); - _logger.LogInformation("Auto tracked {BossName} as defeated", boss.Name); - } + boss.State.AutoTracked = true; + Tracker.MarkBossAsDefeated(boss, true, null, true); + _logger.LogInformation("Auto tracked {BossName} as defeated", boss.Name); } } + } + + /// + /// Tracks the current memory state of LttP for Tracker voice lines + /// + /// The message from the emulator with the memory state + private void CheckZeldaState(EmulatorAction action) + { + if (_previousGame != CurrentGame || action.CurrentData == null) return; + var prevState = ZeldaState; + ZeldaState = new(action.CurrentData); + _logger.LogDebug("{StateDetails}", ZeldaState.ToString()); + if (prevState == null) return; + + if (!_seenGTTorch + && ZeldaState.CurrentRoom == 140 + && !ZeldaState.IsOnBottomHalfOfRoom + && !ZeldaState.IsOnRightHalfOfRoom + && ZeldaState.Substate != 14) + { + _seenGTTorch = true; + IncrementGTItems(Tracker.World.GanonsTower.BobsTorch); + } - /// - /// Tracks the current memory state of LttP for Tracker voice lines - /// - /// The message from the emulator with the memory state - protected void CheckZeldaState(EmulatorAction action) + // Entered the triforce room + if (ZeldaState.State == 0x19) { - if (_previousGame != CurrentGame || action.CurrentData == null) return; - var prevState = ZeldaState; - ZeldaState = new(action.CurrentData); - _logger.LogDebug("{StateDetails}", ZeldaState.ToString()); - if (prevState == null) return; + if (_beatBothBosses) + { + Tracker.GameBeaten(true); + } + } - if (!_seenGTTorch - && ZeldaState.CurrentRoom == 140 - && !ZeldaState.IsOnBottomHalfOfRoom - && !ZeldaState.IsOnRightHalfOfRoom - && ZeldaState.Substate != 14) + foreach (var check in _zeldaStateChecks) + { + if (check != null && check.ExecuteCheck(Tracker, ZeldaState, prevState)) { - _seenGTTorch = true; - IncrementGTItems(Tracker.World.GanonsTower.BobsTorch); + _logger.LogInformation("{StateName} detected", check.GetType().Name); } + } + } + + /// + /// Checks if the final bosses of both games are defeated + /// It appears as if 0x2 represents the first boss defeated and 0x106 is the second, + /// no matter what order the bosses were defeated in + /// + /// The message from the emulator with the memory state + private void CheckBeatFinalBosses(EmulatorAction action) + { + if (_previousGame != CurrentGame || action.CurrentData == null) return; + var didUpdate = false; - // Entered the triforce room - if (ZeldaState.State == 0x19) + if (action.PreviousData?.ReadUInt8(0x2) == 0 && action.CurrentData.ReadUInt8(0x2) > 0) + { + if (CurrentGame == Game.Zelda) { - if (_beatBothBosses) + var gt = Tracker.World.GanonsTower; + if (gt.DungeonState.Cleared == false) { - Tracker.GameBeaten(true); + _logger.LogInformation("Auto tracked Ganon's Tower"); + Tracker.MarkDungeonAsCleared(gt, confidence: null, autoTracked: true); + didUpdate = true; } } - - foreach (var check in _zeldaStateChecks) + else if (CurrentGame == Game.SM) { - if (check != null && check.ExecuteCheck(Tracker, ZeldaState, prevState)) + var motherBrain = Tracker.World.AllBosses.First(x => x.Name == "Mother Brain"); + if (motherBrain.State.Defeated != true) { - _logger.LogInformation("{StateName} detected", check.GetType().Name); + _logger.LogInformation("Auto tracked Mother Brain"); + Tracker.MarkBossAsDefeated(motherBrain, admittedGuilt: true, confidence: null, autoTracked: true); + didUpdate = true; } } } - /// - /// Checks if the final bosses of both games are defeated - /// It appears as if 0x2 represents the first boss defeated and 0x106 is the second, - /// no matter what order the bosses were defeated in - /// - /// The message from the emulator with the memory state - protected void CheckBeatFinalBosses(EmulatorAction action) + if (action.PreviousData?.ReadUInt8(0x106) == 0 && action.CurrentData.ReadUInt8(0x106) > 0) { - if (_previousGame != CurrentGame || action.CurrentData == null) return; - var didUpdate = false; - - if (action.PreviousData?.ReadUInt8(0x2) == 0 && action.CurrentData.ReadUInt8(0x2) > 0) + if (CurrentGame == Game.Zelda) { - if (CurrentGame == Game.Zelda) - { - var gt = Tracker.World.GanonsTower; - if (gt.DungeonState.Cleared == false) - { - _logger.LogInformation("Auto tracked Ganon's Tower"); - Tracker.MarkDungeonAsCleared(gt, confidence: null, autoTracked: true); - didUpdate = true; - } - } - else if (CurrentGame == Game.SM) + var gt = Tracker.World.GanonsTower; + if (gt.DungeonState.Cleared == false) { - var motherBrain = Tracker.World.AllBosses.First(x => x.Name == "Mother Brain"); - if (motherBrain.State.Defeated != true) - { - _logger.LogInformation("Auto tracked Mother Brain"); - Tracker.MarkBossAsDefeated(motherBrain, admittedGuilt: true, confidence: null, autoTracked: true); - didUpdate = true; - } + _logger.LogInformation("Auto tracked Ganon's Tower"); + Tracker.MarkDungeonAsCleared(gt, confidence: null, autoTracked: true); + didUpdate = true; } } - - if (action.PreviousData?.ReadUInt8(0x106) == 0 && action.CurrentData.ReadUInt8(0x106) > 0) + else if (CurrentGame == Game.SM) { - if (CurrentGame == Game.Zelda) + var motherBrain = Tracker.World.AllBosses.First(x => x.Name == "Mother Brain"); + if (motherBrain.State.Defeated != true) { - var gt = Tracker.World.GanonsTower; - if (gt.DungeonState.Cleared == false) - { - _logger.LogInformation("Auto tracked Ganon's Tower"); - Tracker.MarkDungeonAsCleared(gt, confidence: null, autoTracked: true); - didUpdate = true; - } + _logger.LogInformation("Auto tracked Mother Brain"); + Tracker.MarkBossAsDefeated(motherBrain, admittedGuilt: true, confidence: null, autoTracked: true); + didUpdate = true; } - else if (CurrentGame == Game.SM) - { - var motherBrain = Tracker.World.AllBosses.First(x => x.Name == "Mother Brain"); - if (motherBrain.State.Defeated != true) - { - _logger.LogInformation("Auto tracked Mother Brain"); - Tracker.MarkBossAsDefeated(motherBrain, admittedGuilt: true, confidence: null, autoTracked: true); - didUpdate = true; - } - } - } - - if (didUpdate && action.CurrentData.ReadUInt8(0x2) > 0 && action.CurrentData.ReadUInt8(0x106) > 0) - { - _beatBothBosses = true; } - } - /// - /// Tracks the current memory state of SM for Tracker voice lines - /// - /// The message from the emulator with the memory state - protected void CheckMetroidState(EmulatorAction action) + if (didUpdate && action.CurrentData.ReadUInt8(0x2) > 0 && action.CurrentData.ReadUInt8(0x106) > 0) { - if (_previousGame != CurrentGame || action.CurrentData == null) return; - var prevState = MetroidState; - MetroidState = new(action.CurrentData); - _logger.LogDebug("{StateDetails}", MetroidState.ToString()); - if (prevState == null) return; - - // If the game hasn't booted up, wait until we find valid data in the Metroid state before we start - // checking locations - if (_hasValidState != MetroidState.IsValid) - { - _hasValidState = MetroidState.IsValid; - if (_hasValidState) - { - _logger.LogInformation("Valid game state detected"); - } - } + _beatBothBosses = true; + } - if (!_hasValidState) return; + } - foreach (var check in _metroidStateChecks) + /// + /// Tracks the current memory state of SM for Tracker voice lines + /// + /// The message from the emulator with the memory state + private void CheckMetroidState(EmulatorAction action) + { + if (_previousGame != CurrentGame || action.CurrentData == null) return; + var prevState = MetroidState; + MetroidState = new(action.CurrentData); + _logger.LogDebug("{StateDetails}", MetroidState.ToString()); + if (prevState == null) return; + + // If the game hasn't booted up, wait until we find valid data in the Metroid state before we start + // checking locations + if (_hasValidState != MetroidState.IsValid) + { + _hasValidState = MetroidState.IsValid; + if (_hasValidState) { - if (check != null && check.ExecuteCheck(Tracker, MetroidState, prevState)) - { - _logger.LogInformation("{StateName} detected", check.GetType().Name); - } + _logger.LogInformation("Valid game state detected"); } } - /// - /// Checks if the player has entered the ship - /// - /// - protected void CheckShip(EmulatorAction action) + if (!_hasValidState) return; + + foreach (var check in _metroidStateChecks) { - if (_previousGame != CurrentGame || action.CurrentData == null || action.PreviousData == null) return; - var currentInShip = action.CurrentData.ReadUInt16(0) == 0xAA4F; - if (currentInShip && _beatBothBosses) + if (check != null && check.ExecuteCheck(Tracker, MetroidState, prevState)) { - Tracker.GameBeaten(true); + _logger.LogInformation("{StateName} detected", check.GetType().Name); } } + } - private void IncrementGTItems(Location location) + /// + /// Checks if the player has entered the ship + /// + /// + private void CheckShip(EmulatorAction action) + { + if (_previousGame != CurrentGame || action.CurrentData == null || action.PreviousData == null) return; + var currentInShip = action.CurrentData.ReadUInt16(0) == 0xAA4F; + if (currentInShip && _beatBothBosses) { - if (_foundGTKey || _config.Config.ZeldaKeysanity) return; + Tracker.GameBeaten(true); + } + } - var chatIntegrationModule = _trackerModuleFactory.GetModule(); - _numGTItems++; - Tracker.Say(_numGTItems.ToString()); - if (location.Item.Type == ItemType.BigKeyGT) + private void IncrementGTItems(Location location) + { + if (_foundGTKey || _config.Config.ZeldaKeysanity) return; + + var chatIntegrationModule = _trackerModuleFactory.GetModule(); + _numGTItems++; + Tracker.Say(_numGTItems.ToString()); + if (location.Item.Type == ItemType.BigKeyGT) + { + var responseIndex = 1; + for (var i = 1; i <= _numGTItems; i++) { - var responseIndex = 1; - for (var i = 1; i <= _numGTItems; i++) + if (Tracker.Responses.AutoTracker.GTKeyResponses.ContainsKey(i)) { - if (Tracker.Responses.AutoTracker.GTKeyResponses.ContainsKey(i)) - { - responseIndex = i; - } + responseIndex = i; } - Tracker.Say(x => x.AutoTracker.GTKeyResponses[responseIndex], _numGTItems); - chatIntegrationModule?.GTItemTracked(_numGTItems, true); - _foundGTKey = true; - } - else - { - chatIntegrationModule?.GTItemTracked(_numGTItems, false); } + Tracker.Say(x => x.AutoTracker.GTKeyResponses[responseIndex], _numGTItems); + chatIntegrationModule?.GTItemTracked(_numGTItems, true); + _foundGTKey = true; + } + else + { + chatIntegrationModule?.GTItemTracked(_numGTItems, false); } } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerMetroidState.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerMetroidState.cs deleted file mode 100644 index 53dc95672..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerMetroidState.cs +++ /dev/null @@ -1,116 +0,0 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// Used to retrieve certain states based on the memory in Metroid - /// Seee https://jathys.zophar.net/supermetroid/kejardon/RAMMap.txt for details on the memory - /// - public class AutoTrackerMetroidState - { - private readonly EmulatorMemoryData _data; - - /// - /// Constructor - /// - /// - public AutoTrackerMetroidState(EmulatorMemoryData data) - { - _data = data; - } - - /// - /// The overall room number the player is in - /// - public int CurrentRoom => _data.ReadUInt8(0x7E079B - 0x7E0750); - - /// - /// The region room number the player is in - /// - public int CurrentRoomInRegion => _data.ReadUInt8(0x7E079D - 0x7E0750); - - /// - /// The current region the player is in - /// - public int CurrentRegion => _data.ReadUInt8(0x7E079F - 0x7E0750); - - /// - /// The amount of energy/health - /// - public int Energy => _data.ReadUInt16(0x7E09C2 - 0x7E0750); - - /// - /// The amount currently in reserve tanks - /// - public int ReserveTanks => _data.ReadUInt16(0x7E09D6 - 0x7E0750); - - /// - /// The max of health - /// - public int MaxEnergy => _data.ReadUInt16(0x7E09C4 - 0x7E0750); - - /// - /// The max in reserve tanks - /// - public int MaxReserveTanks => _data.ReadUInt16(0x7E09D4 - 0x7E0750); - - /// - /// Samus's X Location - /// - public int SamusX => _data.ReadUInt16(0x7E0AF6 - 0x7E0750); - - /// - /// Samus's Y Location - /// - public int SamusY => _data.ReadUInt16(0x7E0AFA - 0x7E0750); - - /// - /// Samus's current super missile count - /// - public int SuperMissiles => _data.ReadUInt8(0x7E09CA - 0x7E0750); - - /// - /// Samus's max super missile count - /// - public int MaxSuperMissiles => _data.ReadUInt8(0x7E09CC - 0x7E0750); - - /// - /// Samus's current missile count - /// - public int Missiles => _data.ReadUInt8(0x7E09C6 - 0x7E0750); - - /// - /// Samus's max missile count - /// - public int MaxMissiles => _data.ReadUInt8(0x7E09C8 - 0x7E0750); - - /// - /// Samus's current power bomb count - /// - public int PowerBombs => _data.ReadUInt8(0x7E09CE - 0x7E0750); - - /// - /// Samus's max power bomb count - /// - public int MaxPowerBombs => _data.ReadUInt8(0x7E09D0 - 0x7E0750); - - public bool IsSamusInArea(int minX, int maxX, int minY, int maxY) - { - return SamusX >= minX && SamusX <= maxX && SamusY >= minY && SamusY <= maxY; - } - - /// - /// Checks to make sure that the state is valid and fully loaded. There's a period upon first booting up that - /// all of these are 0s, but some of the memory in the location data can be screwy. - /// - public bool IsValid => CurrentRoom != 0 || CurrentRegion != 0 || CurrentRoomInRegion != 0 || Energy != 0 || - SamusX != 0 || SamusY != 0; - - /// - /// Prints debug data for the state - /// - /// - public override string ToString() - { - return $"CurrentRoom: {CurrentRoom} | CurrentRoomInRegion: {CurrentRoomInRegion} | CurrentRegion: {CurrentRegion} | Health: {Energy},{ReserveTanks} | X,Y {SamusX},{SamusY}"; - } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerViewedAction.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerViewedAction.cs deleted file mode 100644 index 65c1d9b44..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerViewedAction.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// Class for storing an action from viewing something - /// for a short amount of time - /// - public class AutoTrackerViewedAction - { - private Action? _action; - - /// - /// Constructor - /// - /// - public AutoTrackerViewedAction(Action action) - { - _action = action; - _ = ExpireAsync(); - } - - /// - /// Expires the action after a period of time - /// - private async Task ExpireAsync() - { - await Task.Delay(TimeSpan.FromSeconds(15)); - _action = null; - } - - /// - /// Invokes the action, if it's still valid - /// - /// - public bool Invoke() - { - if (_action == null) return false; - _action.Invoke(); - return true; - } - - /// - /// If the action is valid - /// - public bool IsValid => _action != null; - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerZeldaState.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerZeldaState.cs deleted file mode 100644 index 99d572d6d..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/AutoTrackerZeldaState.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// Used to retrieve certain states based on the memory in Zelda - /// See http://alttp.run/hacking/index.php?title=RAM:_Bank_0x7E:_Page_0x00 for details on the memory - /// These memory address values are the offset from 0x7E0000 - /// - public class AutoTrackerZeldaState - { - private readonly EmulatorMemoryData _data; - - /// - /// Constructor - /// - /// - public AutoTrackerZeldaState(EmulatorMemoryData data) - { - _data = data; - } - - /// - /// The current room the player is in - /// - public int CurrentRoom => ReadUInt16(0xA0); - - /// - /// The previous room the player was in - /// - public int PreviousRoom => ReadUInt16(0xA2); - - /// - /// The state of the game (Overworld, Dungeon, etc.) - /// - public int State => ReadUInt8(0x10); - - /// - /// The secondary state value - /// - public int Substate => ReadUInt8(0x11); - - /// - /// The player's Y location - /// - public int LinkY => ReadUInt16(0x20); - - /// - /// The player's X Location - /// - public int LinkX => ReadUInt16(0x22); - - /// - /// What the player is currently doing - /// - public int LinkState => ReadUInt8(0x5D); - - /// - /// Value used to determine if the player is in the light or dark world - /// Apparently this is used for other calculations as well, so need to be a bit careful - /// Transitioning from Super Metroid also seems to break this until you go through a portal - /// - public int OverworldValue => ReadUInt8(0x7B); - - /// - /// True if Link is on the bottom half of the current room - /// - public bool IsOnBottomHalfOfRoom => ReadUInt8(0xAA) == 2; - - /// - /// True if Link is on the right half of the current room - /// - public bool IsOnRightHalfOfRoom => ReadUInt8(0xA9) == 1; - - /// - /// The overworld screen that the player is on - /// - public int OverworldScreen => ReadUInt16(0x8A); - - /// - /// Reads a specific block of memory - /// - /// The address offset from 0x7E0000 - /// - public int ReadUInt8(int address) => _data.ReadUInt8(address); - - /// - /// Reads a specific block of memory - /// - /// The address offset from 0x7E0000 - /// - public int ReadUInt16(int address) => _data.ReadUInt16(address); - - /// - /// Get debug string - /// - /// - public override string ToString() - { - var vertical = IsOnBottomHalfOfRoom ? "Bottom" : "Top"; - var horizontal = IsOnRightHalfOfRoom ? "Right" : "Left"; - return $"Room: {PreviousRoom}->{CurrentRoom} ({vertical}{horizontal}) | State: {State}/{Substate} | X,Y: {LinkX},{LinkY} | LinkState: {LinkState} | OW Screen: {OverworldScreen}"; - } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorAction.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorAction.cs deleted file mode 100644 index 6f46791ef..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorAction.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using Randomizer.Data.WorldData; - -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// Class used for communicating between the emulator and tracker - /// - public class EmulatorAction - { - /// - /// The action to be done by the emulator (read/write/etc) - /// - public EmulatorActionType Type { get; set; } - - /// - /// The starting memory address - /// - public int Address { get; set; } - - /// - /// The number of bytes to capture from the emulator - /// - public int Length { get; set; } - - /// - /// Values for writing - /// - public ICollection? WriteValues { get; set; } - - /// - /// The type of memory to read or modify (WRAM, CARTRAM, CARTROM) - /// - public MemoryDomain Domain { get; set; } - - /// - /// The game this message is for - /// - public Game Game { get; set; } = Game.Both; - - /// - /// Action to perform when getting a response for this from the emulator - /// - public Action? Action { get; set; } - - /// - /// The previous data collected for this action - /// - public EmulatorMemoryData? PreviousData { get; protected set; } - - /// - /// The latest data collected for this action - /// - public EmulatorMemoryData? CurrentData { get; protected set; } - - /// - /// The amount of time that should happen between consecutive reads - /// - public double FrequencySeconds = 0; - - /// - /// When this action was last executed - /// - public DateTime? LastRunTime; - - /// - /// Update the stored data and invoke the action - /// - /// The data collected from the emulator - public void Invoke(EmulatorMemoryData data) - { - PreviousData = CurrentData; - CurrentData = data; - LastRunTime = DateTime.Now; - Action?.Invoke(this); - } - - /// - /// If this message should be sent based on the game the player is currently in - /// - /// The game the player is currently in - /// If the player has actually started the game - /// True if the message should be sent. - public bool ShouldProcess(Game currentGame, bool hasStartedGame) - { - return HasExpired && ((!hasStartedGame && Game == Game.Neither) || (hasStartedGame && Game != Game.Neither && (Game == Game.Both || Game == currentGame))); - } - - /// - /// If the previous action's result has expired - /// - public bool HasExpired - { - get - { - return FrequencySeconds <= 0 || LastRunTime == null || - (DateTime.Now - LastRunTime.Value).TotalSeconds >= FrequencySeconds; - } - } - - /// - /// Checks if the data has changed between the previous and current collections - /// - /// True if the data has changed, false otherwise - public bool HasDataChanged() - { - return CurrentData != null && !CurrentData.Equals(PreviousData); - } - - /// - /// Cached set of locations for this action - /// - public ICollection? Locations { get; set; } - - /// - /// Clears both the previous and current data sets - /// - public void ClearData() - { - PreviousData = null; - CurrentData = null; - } - - } - -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorDataReceivedEventArgs.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorDataReceivedEventArgs.cs index c53249631..979026de9 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorDataReceivedEventArgs.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorDataReceivedEventArgs.cs @@ -1,4 +1,7 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking { /// /// Event arguments for when connector has received data from the emulator diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorMemoryData.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorMemoryData.cs deleted file mode 100644 index ead44bd01..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/EmulatorMemoryData.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Linq; - -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// Class used to house byte data retrieved from the emulator at a given point in time - /// Used to retrieve data at locations in memory - /// - public class EmulatorMemoryData - { - private byte[] _bytes; - - /// - /// Constructor - /// - /// - public EmulatorMemoryData(byte[] bytes) - { - _bytes = bytes; - } - - /// - /// The raw byte array of the data - /// - public byte[] Raw - { - get - { - return _bytes; - } - } - - /// - /// Returns the memory value at a location - /// - /// The offset location to check - /// The value from the byte array at that location - public byte ReadUInt8(int location) - { - return _bytes[location]; - } - - /// - /// Gets the memory value for a location and returns if it matches a given flag - /// - /// The offset location to check - /// The flag to check against - /// True if the flag is set for the memory location. - public bool CheckBinary8Bit(int location, int flag) - { - return (ReadUInt8(location) & flag) == flag; - } - - /// - /// Checks if a value in memory matches a flag or has been increased to denote obtaining an item - /// - /// The previous data to compare to - /// The offset location to check - /// The flag to check against - /// True if the value in memory was set or increased - public bool CompareUInt8(EmulatorMemoryData previousData, int location, int? flag) - { - var prevValue = previousData != null && previousData._bytes.Length > location ? previousData.ReadUInt8(location) : -1; - var newValue = ReadUInt8(location); - - if (newValue > prevValue) - { - if (flag != null) - { - if ((newValue & flag) == flag) - { - return true; - } - } - else if (newValue > 0) - { - return true; - } - } - - return false; - } - - /// - /// Returns the memory value of two bytes / sixteen bits. Note that these are flipped - /// from what you may expect if you're thinking of it in binary terms. The second byte - /// is actually multiplied by 0xFF / 256 and added to the first - /// - /// The offset location to check - /// The value from the byte array at that location - public int ReadUInt16(int location) - { - return _bytes[location + 1] * 256 + _bytes[location]; - } - - /// - /// Checks if a binary flag is set for a given set of two bytes / sixteen bits - /// - /// The offset location to check - /// The flag to check against - /// True if the flag is set for the memory location. - public bool CheckUInt16(int location, int flag) - { - var data = ReadUInt16(location); - var adjustedFlag = 1 << flag; - var temp = data & adjustedFlag; - return temp == adjustedFlag; - } - - /// - /// Returns if this EmulatorMemoryData equals another - /// - /// - /// - public override bool Equals(object? other) - { - if (other is not EmulatorMemoryData otherData) return false; - return Enumerable.SequenceEqual(otherData._bytes, _bytes); - } - - /// - /// Returns the hash code of the bytes array - /// - /// - public override int GetHashCode() - { - return _bytes.GetHashCode(); - } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/EmulatorActionType.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/EmulatorActionType.cs deleted file mode 100644 index 4b6f20a5f..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/EmulatorActionType.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// The type of action - /// - public enum EmulatorActionType - { - /// - /// Read a block from memory - /// - ReadBlock, - - /// - /// Write data to memory - /// - WriteBytes - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/Game.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/Game.cs deleted file mode 100644 index 49bdf78d1..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/Game.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// Which game(s) the message should be sent to the emulator in - /// - public enum Game - { - /// - /// Send if the player has not started the game - /// - Neither, - - /// - /// Send if the player is in Super Metroid - /// - SM, - - /// - /// Send if the player is in Zelda - /// - Zelda, - - /// - /// Send if the player is in either game - /// - Both, - - /// - /// Send if the player is viewing the credits - /// - Credits - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/MemoryDomain.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/MemoryDomain.cs deleted file mode 100644 index 98d3b1c7b..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/Enums/MemoryDomain.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking -{ - /// - /// The type of memory - /// - public enum MemoryDomain - { - /// - /// SNES Memory - /// - WRAM, - - /// - /// Cartridge Memory / Save File (AKA SRAM) - /// - CartRAM, - - /// - /// Game data saved on cartridge - /// - CartROM - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs index 43db26e99..5a344b048 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/GameService.cs @@ -3,590 +3,592 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; using Randomizer.Shared; using Randomizer.SMZ3.Tracking.Services; using Randomizer.SMZ3.Tracking.VoiceCommands; using Randomizer.Data.WorldData; +using Randomizer.Shared.Enums; using Randomizer.SMZ3.Contracts; -namespace Randomizer.SMZ3.Tracking.AutoTracking +namespace Randomizer.SMZ3.Tracking.AutoTracking; + +/// +/// Service that handles interacting with the game via +/// auto tracker +/// +public class GameService : TrackerModule, IGameService { + private IAutoTracker? AutoTracker => Tracker.AutoTracker; + private readonly ILogger _logger; + private readonly int _trackerPlayerId; + private int _itemCounter; + private readonly Dictionary _emulatorActions = new(); + /// - /// Service that handles interacting with the game via - /// auto tracker + /// Initializes a new instance of the + /// class. /// - public class GameService : TrackerModule + /// The tracker instance. + /// Service to get item information + /// Service to get world information + /// The logger to associate with this module + /// The accesor to determine the tracker player id + public GameService(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, IWorldAccessor worldAccessor) + : base(tracker, itemService, worldService, logger) { - private AutoTracker? AutoTracker => Tracker.AutoTracker; - private readonly ILogger _logger; - private readonly int _trackerPlayerId; - private int _itemCounter; - private readonly Dictionary _emulatorActions = new(); - - /// - /// Initializes a new instance of the - /// class. - /// - /// The tracker instance. - /// Service to get item information - /// Service to get world information - /// The logger to associate with this module - /// The accesor to determine the tracker player id - public GameService(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, IWorldAccessor worldAccessor) - : base(tracker, itemService, worldService, logger) - { - Tracker.GameService = this; - _logger = logger; - _trackerPlayerId = worldAccessor.Worlds.Count > 0 ? worldAccessor.Worlds.Count : 0; - } + Tracker.GameService = this; + _logger = logger; + _trackerPlayerId = worldAccessor.Worlds.Count > 0 ? worldAccessor.Worlds.Count : 0; + } - /// - /// Updates memory values so both SM and Z3 will cancel any pending MSU resumes and play - /// all tracks from the start until new resume points have been stored. - /// - /// True, even if it didn't do anything - public void TryCancelMsuResume() + /// + /// Updates memory values so both SM and Z3 will cancel any pending MSU resumes and play + /// all tracks from the start until new resume points have been stored. + /// + /// True, even if it didn't do anything + public void TryCancelMsuResume() + { + if (IsInGame(Game.SM)) { - if (IsInGame(Game.SM)) + // Zero out SM's NO_RESUME_AFTER_LO and NO_RESUME_AFTER_HI variables + AutoTracker?.WriteToMemory(new EmulatorAction() { - // Zero out SM's NO_RESUME_AFTER_LO and NO_RESUME_AFTER_HI variables - AutoTracker?.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E033A, // As declared in sm/msu.asm - WriteValues = new List() { 0, 0, 0, 0 } - }); - } - - if (IsInGame(Game.Zelda)) - { - // Zero out Z3's MSUResumeTime variable - AutoTracker?.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E1E6B, // As declared in z3/randomizer/ram.asm - WriteValues = new List() { 0, 0, 0, 0 } - }); - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E033A, // As declared in sm/msu.asm + WriteValues = new List() { 0, 0, 0, 0 } + }); } - /// - /// Gives an item to the player - /// - /// The item to give - /// The id of the player giving the item to the player (null for tracker) - /// False if it is currently unable to give an item to the player - public bool TryGiveItem(Item item, int? fromPlayerId) + if (IsInGame(Game.Zelda)) { - return TryGiveItems(new List() { item }, fromPlayerId ?? _trackerPlayerId); + // Zero out Z3's MSUResumeTime variable + AutoTracker?.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E1E6B, // As declared in z3/randomizer/ram.asm + WriteValues = new List() { 0, 0, 0, 0 } + }); } + } - /// - /// Gives a series of items to the player - /// - /// The list of items to give to the player - /// The id of the player giving the item to the player - /// False if it is currently unable to give an item to the player - public bool TryGiveItems(List items, int fromPlayerId) + /// + /// Gives an item to the player + /// + /// The item to give + /// The id of the player giving the item to the player (null for tracker) + /// False if it is currently unable to give an item to the player + public bool TryGiveItem(Item item, int? fromPlayerId) + { + return TryGiveItems(new List() { item }, fromPlayerId ?? _trackerPlayerId); + } + + /// + /// Gives a series of items to the player + /// + /// The list of items to give to the player + /// The id of the player giving the item to the player + /// False if it is currently unable to give an item to the player + public bool TryGiveItems(List items, int fromPlayerId) + { + if (!IsInGame()) { - if (!IsInGame()) - { - return false; - } + return false; + } - Tracker.TrackItems(items, true, true); + Tracker.TrackItems(items, true, true); - return TryGiveItemTypes(items.Select(x => (x.Type, fromPlayerId)).ToList()); - } + return TryGiveItemTypes(items.Select(x => (x.Type, fromPlayerId)).ToList()); + } - /// - /// Gives a series of item types from particular players - /// - /// The list of item types and the players that are giving the item to the player - /// False if it is currently unable to give the items to the player - public bool TryGiveItemTypes(List<(ItemType type, int fromPlayerId)> items) + /// + /// Gives a series of item types from particular players + /// + /// The list of item types and the players that are giving the item to the player + /// False if it is currently unable to give the items to the player + public bool TryGiveItemTypes(List<(ItemType type, int fromPlayerId)> items) + { + if (!IsInGame()) { - if (!IsInGame()) - { - return false; - } + return false; + } - var tempItemCounter = _itemCounter; - EmulatorAction action; + var tempItemCounter = _itemCounter; + EmulatorAction action; - // First give the player all of the requested items - // Batch them into chunks of 50 due to byte limit for QUSB2SNES - foreach (var batch in items.Chunk(50)) + // First give the player all of the requested items + // Batch them into chunks of 50 due to byte limit for QUSB2SNES + foreach (var batch in items.Chunk(50)) + { + var bytes = new List(); + foreach (var item in batch) { - var bytes = new List(); - foreach (var item in batch) - { - bytes.AddRange(Int16ToBytes(item.fromPlayerId)); - bytes.AddRange(Int16ToBytes((int)item.type)); - } - - action = new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.CartRAM, - Address = 0xA26000 + (tempItemCounter * 4), - WriteValues = bytes - }; - AutoTracker!.WriteToMemory(action); - - tempItemCounter += batch.Length; + bytes.AddRange(Int16ToBytes(item.fromPlayerId)); + bytes.AddRange(Int16ToBytes((int)item.type)); } - // Up the item counter to have them actually pick it up action = new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.CartRAM, - Address = 0xA26602, - WriteValues = Int16ToBytes(tempItemCounter) + Address = 0xA26000 + (tempItemCounter * 4), + WriteValues = bytes }; AutoTracker!.WriteToMemory(action); - _itemCounter = tempItemCounter; - - return true; + tempItemCounter += batch.Length; } - /// - /// Restores the player to max health - /// - /// False if it is currently unable to give an item to the player - public bool TryHealPlayer() + // Up the item counter to have them actually pick it up + action = new EmulatorAction() { - if (!IsInGame()) - { - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.CartRAM, + Address = 0xA26602, + WriteValues = Int16ToBytes(tempItemCounter) + }; + AutoTracker!.WriteToMemory(action); - if (AutoTracker!.CurrentGame == Game.Zelda) - { - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7EF372, - WriteValues = new List() { 0xA0 } - }); - - return true; - } - else if (AutoTracker.CurrentGame == Game.SM && AutoTracker.MetroidState != null) - { - var maxHealth = AutoTracker.MetroidState.MaxEnergy; - var maxReserves = AutoTracker.MetroidState.MaxReserveTanks; - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09C2, - WriteValues = Int16ToBytes(maxHealth) - }); - - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09D6, - WriteValues = Int16ToBytes(maxReserves) - }); - - return true; - } + _itemCounter = tempItemCounter; + return true; + } + + /// + /// Restores the player to max health + /// + /// False if it is currently unable to give an item to the player + public bool TryHealPlayer() + { + if (!IsInGame()) + { return false; } - /// - /// Fully fills the player's magic - /// - /// False if it is currently unable to give magic to the player - public bool TryFillMagic() + if (AutoTracker!.CurrentGame == Game.Zelda) { - if (!IsInGame(Game.Zelda)) - { - return false; - } - - AutoTracker!.WriteToMemory(new EmulatorAction() + AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7EF373, - WriteValues = new List() { 0x80 } + Address = 0x7EF372, + WriteValues = new List() { 0xA0 } }); return true; } - - /// - /// Fully fills the player's bombs to capacity - /// - /// False if it is currently unable to give bombs to the player - public bool TryFillZeldaBombs() + else if (AutoTracker.CurrentGame == Game.SM && AutoTracker.MetroidState != null) { - if (!IsInGame(Game.Zelda)) + var maxHealth = AutoTracker.MetroidState.MaxEnergy; + var maxReserves = AutoTracker.MetroidState.MaxReserveTanks; + AutoTracker.WriteToMemory(new EmulatorAction() { - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09C2, + WriteValues = Int16ToBytes(maxHealth) + }); - AutoTracker!.WriteToMemory(new EmulatorAction() + AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7EF375, - WriteValues = new List() { 0xFF } + Address = 0x7E09D6, + WriteValues = Int16ToBytes(maxReserves) }); return true; } - /// - /// Fully fills the player's arrows - /// - /// False if it is currently unable to give arrows to the player - public bool TryFillArrows() + return false; + } + + /// + /// Fully fills the player's magic + /// + /// False if it is currently unable to give magic to the player + public bool TryFillMagic() + { + if (!IsInGame(Game.Zelda)) { - if (!IsInGame(Game.Zelda)) - { - return false; - } + return false; + } - AutoTracker!.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7EF376, - WriteValues = new List() { 0x80 } - }); + AutoTracker!.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7EF373, + WriteValues = new List() { 0x80 } + }); - return true; + return true; + } + + /// + /// Fully fills the player's bombs to capacity + /// + /// False if it is currently unable to give bombs to the player + public bool TryFillZeldaBombs() + { + if (!IsInGame(Game.Zelda)) + { + return false; } - /// - /// Fully fills the player's rupees (sets to 2000) - /// - /// False if it is currently unable to give rupees to the player - public bool TryFillRupees() + AutoTracker!.WriteToMemory(new EmulatorAction() { - if (!IsInGame(Game.Zelda)) - { - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7EF375, + WriteValues = new List() { 0xFF } + }); - var bytes = Int16ToBytes(2000); + return true; + } - // Writing the target value to $7EF360 makes the rupee count start counting toward it. - // Writing the target value to $7EF362 immediately sets the rupee count, but then it starts counting back toward where it was. - // Writing the target value to both locations immediately sets the rupee count and keeps it there. - AutoTracker!.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7EF360, - WriteValues = bytes.Concat(bytes).ToList() - }); + /// + /// Fully fills the player's arrows + /// + /// False if it is currently unable to give arrows to the player + public bool TryFillArrows() + { + if (!IsInGame(Game.Zelda)) + { + return false; + } - return true; + AutoTracker!.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7EF376, + WriteValues = new List() { 0x80 } + }); + + return true; + } + + /// + /// Fully fills the player's rupees (sets to 2000) + /// + /// False if it is currently unable to give rupees to the player + public bool TryFillRupees() + { + if (!IsInGame(Game.Zelda)) + { + return false; } - /// - /// Fully fills the player's missiles - /// - /// False if it is currently unable to give missiles to the player - public bool TryFillMissiles() + var bytes = Int16ToBytes(2000); + + // Writing the target value to $7EF360 makes the rupee count start counting toward it. + // Writing the target value to $7EF362 immediately sets the rupee count, but then it starts counting back toward where it was. + // Writing the target value to both locations immediately sets the rupee count and keeps it there. + AutoTracker!.WriteToMemory(new EmulatorAction() { - if (!IsInGame(Game.SM)) - { - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7EF360, + WriteValues = bytes.Concat(bytes).ToList() + }); - var maxMissiles = AutoTracker!.MetroidState?.MaxMissiles ?? 0; - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09C6, - WriteValues = Int16ToBytes(maxMissiles) - }); + return true; + } - return true; + /// + /// Fully fills the player's missiles + /// + /// False if it is currently unable to give missiles to the player + public bool TryFillMissiles() + { + if (!IsInGame(Game.SM)) + { + return false; } - /// - /// Fully fills the player's super missiles - /// - /// False if it is currently unable to give super missiles to the player - public bool TryFillSuperMissiles() + var maxMissiles = AutoTracker!.MetroidState?.MaxMissiles ?? 0; + AutoTracker.WriteToMemory(new EmulatorAction() { - if (!IsInGame(Game.SM)) - { - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09C6, + WriteValues = Int16ToBytes(maxMissiles) + }); - var maxSuperMissiles = AutoTracker!.MetroidState?.MaxSuperMissiles ?? 0; - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09CA, - WriteValues = Int16ToBytes(maxSuperMissiles) - }); + return true; + } - return true; + /// + /// Fully fills the player's super missiles + /// + /// False if it is currently unable to give super missiles to the player + public bool TryFillSuperMissiles() + { + if (!IsInGame(Game.SM)) + { + return false; } - /// - /// Fully fills the player's power bombs - /// - /// False if it is currently unable to give power bombs to the player - public bool TryFillPowerBombs() + var maxSuperMissiles = AutoTracker!.MetroidState?.MaxSuperMissiles ?? 0; + AutoTracker.WriteToMemory(new EmulatorAction() { - if (!IsInGame(Game.SM)) - { - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09CA, + WriteValues = Int16ToBytes(maxSuperMissiles) + }); - var maxPowerBombs = AutoTracker!.MetroidState?.MaxPowerBombs ?? 0; - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09CE, - WriteValues = Int16ToBytes(maxPowerBombs) - }); + return true; + } - return true; + /// + /// Fully fills the player's power bombs + /// + /// False if it is currently unable to give power bombs to the player + public bool TryFillPowerBombs() + { + if (!IsInGame(Game.SM)) + { + return false; } - /// - /// Kills the player by removing their health and dealing damage to them - /// - /// True if successful - public bool TryKillPlayer() + var maxPowerBombs = AutoTracker!.MetroidState?.MaxPowerBombs ?? 0; + AutoTracker.WriteToMemory(new EmulatorAction() { - if (!IsInGame()) - { - _logger.LogWarning("Could not kill player as they are not in game"); - return false; - } + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09CE, + WriteValues = Int16ToBytes(maxPowerBombs) + }); - if (AutoTracker!.CurrentGame == Game.Zelda) - { - MarkRecentlyKilled(); - - // Set health to 0 - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7EF36D, - WriteValues = new List() { 0x0 } - }); - - // Deal 1 heart of damage - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E0373, - WriteValues = new List() { 0x8 } - }); - - return true; - } - else if (AutoTracker.CurrentGame == Game.SM) - { - MarkRecentlyKilled(); - - // Empty reserves - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09D6, - WriteValues = new List() { 0x0, 0x0 } - }); - - // Set HP to 1 (to prevent saving with 0 energy) - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E09C2, - WriteValues = new List() { 0x1, 0x0 } - }); - - // Deal 255 damage to player - AutoTracker.WriteToMemory(new EmulatorAction() - { - Type = EmulatorActionType.WriteBytes, - Domain = MemoryDomain.WRAM, - Address = 0x7E0A50, - WriteValues = new List() { 0xFF } - }); - - return true; - } + return true; + } - _logger.LogWarning("Could not kill player as they are not in either Zelda or Metroid currently"); + /// + /// Kills the player by removing their health and dealing damage to them + /// + /// True if successful + public bool TryKillPlayer() + { + if (!IsInGame()) + { + _logger.LogWarning("Could not kill player as they are not in game"); return false; } - /// - /// Sets the player to have the requirements for a crystal flash - /// - /// True if successful - public bool TrySetupCrystalFlash() + if (AutoTracker!.CurrentGame == Game.Zelda) { - if (!IsInGame(Game.SM)) - { - return false; - } + MarkRecentlyKilled(); - // Set HP to 50 health - AutoTracker!.WriteToMemory(new EmulatorAction() + // Set health to 0 + AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7E09C2, - WriteValues = new List() { 0x32, 0x0 } + Address = 0x7EF36D, + WriteValues = new List() { 0x0 } }); - // Empty reserves + // Deal 1 heart of damage AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7E09D6, - WriteValues = new List() { 0x0, 0x0 } + Address = 0x7E0373, + WriteValues = new List() { 0x8 } }); - // Fill missiles - var maxMissiles = AutoTracker.MetroidState?.MaxMissiles ?? 0; + return true; + } + else if (AutoTracker.CurrentGame == Game.SM) + { + MarkRecentlyKilled(); + + // Empty reserves AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7E09C6, - WriteValues = Int16ToBytes(maxMissiles) + Address = 0x7E09D6, + WriteValues = new List() { 0x0, 0x0 } }); - // Fill super missiles - var maxSuperMissiles = AutoTracker.MetroidState?.MaxSuperMissiles ?? 0; + // Set HP to 1 (to prevent saving with 0 energy) AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7E09CA, - WriteValues = Int16ToBytes(maxSuperMissiles) + Address = 0x7E09C2, + WriteValues = new List() { 0x1, 0x0 } }); - // Fill power bombs - var maxPowerBombs = AutoTracker.MetroidState?.MaxPowerBombs ?? 0; + // Deal 255 damage to player AutoTracker.WriteToMemory(new EmulatorAction() { Type = EmulatorActionType.WriteBytes, Domain = MemoryDomain.WRAM, - Address = 0x7E09CE, - WriteValues = Int16ToBytes(maxPowerBombs) + Address = 0x7E0A50, + WriteValues = new List() { 0xFF } }); return true; } - /// - /// Gives the player any items that tracker thinks they should have but are not in memory as having been gifted - /// - /// - public void SyncItems(EmulatorAction action) + _logger.LogWarning("Could not kill player as they are not in either Zelda or Metroid currently"); + return false; + } + + /// + /// Sets the player to have the requirements for a crystal flash + /// + /// True if successful + public bool TrySetupCrystalFlash() + { + if (!IsInGame(Game.SM)) { - if (AutoTracker?.HasValidState != true) - { - return; - } + return false; + } - _emulatorActions[action.Address] = action; + // Set HP to 50 health + AutoTracker!.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09C2, + WriteValues = new List() { 0x32, 0x0 } + }); + + // Empty reserves + AutoTracker.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09D6, + WriteValues = new List() { 0x0, 0x0 } + }); + + // Fill missiles + var maxMissiles = AutoTracker.MetroidState?.MaxMissiles ?? 0; + AutoTracker.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09C6, + WriteValues = Int16ToBytes(maxMissiles) + }); + + // Fill super missiles + var maxSuperMissiles = AutoTracker.MetroidState?.MaxSuperMissiles ?? 0; + AutoTracker.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09CA, + WriteValues = Int16ToBytes(maxSuperMissiles) + }); + + // Fill power bombs + var maxPowerBombs = AutoTracker.MetroidState?.MaxPowerBombs ?? 0; + AutoTracker.WriteToMemory(new EmulatorAction() + { + Type = EmulatorActionType.WriteBytes, + Domain = MemoryDomain.WRAM, + Address = 0x7E09CE, + WriteValues = Int16ToBytes(maxPowerBombs) + }); - if (!_emulatorActions.ContainsKey(0xA26000) || !_emulatorActions.ContainsKey(0xA26300) || !_emulatorActions.Values.All(x => - x.LastRunTime != null && (DateTime.Now - x.LastRunTime.Value).TotalSeconds < 5)) - { - return; - } + return true; + } - var data = _emulatorActions[0xA26000].CurrentData!.Raw.Concat(_emulatorActions[0xA26300].CurrentData!.Raw).ToArray(); + /// + /// Gives the player any items that tracker thinks they should have but are not in memory as having been gifted + /// + /// + public void SyncItems(EmulatorAction action) + { + if (AutoTracker?.HasValidState != true) + { + return; + } - var previouslyGiftedItems = new List<(ItemType type, int fromPlayerId)>(); - for (var i = 0; i < 0x150; i++) - { - var item = (ItemType)BitConverter.ToUInt16(data.AsSpan(i * 4 + 2, 2)); - if (item == ItemType.Nothing) - { - continue; - } - - var playerId = BitConverter.ToUInt16(data.AsSpan(i * 4, 2)); - previouslyGiftedItems.Add((item, playerId)); - } + _emulatorActions[action.Address] = action; - _itemCounter = previouslyGiftedItems.Count; + if (!_emulatorActions.ContainsKey(0xA26000) || !_emulatorActions.ContainsKey(0xA26300) || !_emulatorActions.Values.All(x => + x.LastRunTime != null && (DateTime.Now - x.LastRunTime.Value).TotalSeconds < 5)) + { + return; + } - var otherCollectedItems = WorldService.Worlds.SelectMany(x => x.Locations) - .Where(x => x.State.ItemWorldId == Tracker.World.Id && x.State.WorldId != Tracker.World.Id && - x.State.Autotracked).Select(x => (x.State.Item, x.State.WorldId)).ToList(); + var data = _emulatorActions[0xA26000].CurrentData!.Raw.Concat(_emulatorActions[0xA26300].CurrentData!.Raw).ToArray(); - foreach (var item in previouslyGiftedItems) + var previouslyGiftedItems = new List<(ItemType type, int fromPlayerId)>(); + for (var i = 0; i < 0x150; i++) + { + var item = (ItemType)BitConverter.ToUInt16(data.AsSpan(i * 4 + 2, 2)); + if (item == ItemType.Nothing) { - otherCollectedItems.Remove(item); + continue; } - if (otherCollectedItems.Any()) - { - _logger.LogInformation("Giving player {ItemCount} missing items", otherCollectedItems.Count); - TryGiveItemTypes(otherCollectedItems); - } + var playerId = BitConverter.ToUInt16(data.AsSpan(i * 4, 2)); + previouslyGiftedItems.Add((item, playerId)); } - /// - /// If the player was recently killed by the game service - /// - public bool PlayerRecentlyKilled { get; private set; } + _itemCounter = previouslyGiftedItems.Count; - private async void MarkRecentlyKilled() + var otherCollectedItems = WorldService.Worlds.SelectMany(x => x.Locations) + .Where(x => x.State.ItemWorldId == Tracker.World.Id && x.State.WorldId != Tracker.World.Id && + x.State.Autotracked).Select(x => (x.State.Item, x.State.WorldId)).ToList(); + + foreach (var item in previouslyGiftedItems) { - PlayerRecentlyKilled = true; - await Task.Delay(TimeSpan.FromSeconds(10)); - PlayerRecentlyKilled = false; + otherCollectedItems.Remove(item); } - private static byte[] Int16ToBytes(int value) + if (otherCollectedItems.Any()) { - var bytes = BitConverter.GetBytes((short)value).ToList(); - if (!BitConverter.IsLittleEndian) - { - bytes.Reverse(); - } - return bytes.ToArray(); + _logger.LogInformation("Giving player {ItemCount} missing items", otherCollectedItems.Count); + TryGiveItemTypes(otherCollectedItems); } + } + + /// + /// If the player was recently killed by the game service + /// + public bool PlayerRecentlyKilled { get; private set; } - private bool IsInGame(Game game = Game.Both) + private async void MarkRecentlyKilled() + { + PlayerRecentlyKilled = true; + await Task.Delay(TimeSpan.FromSeconds(10)); + PlayerRecentlyKilled = false; + } + + private static byte[] Int16ToBytes(int value) + { + var bytes = BitConverter.GetBytes((short)value).ToList(); + if (!BitConverter.IsLittleEndian) { - if (AutoTracker is { IsConnected: true, IsInSMZ3: true, HasValidState: true }) - { - return game == Game.Both || AutoTracker.CurrentGame == game; - } - return false; + bytes.Reverse(); } + return bytes.ToArray(); + } - public override void AddCommands() + private bool IsInGame(Game game = Game.Both) + { + if (AutoTracker is { IsConnected: true, IsInSMZ3: true, HasValidState: true }) { - + return game == Game.Both || AutoTracker.CurrentGame == game; } + return false; + } + + public override void AddCommands() + { + } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/IEmulatorConnector.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/IEmulatorConnector.cs index f6b42a678..139970730 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/IEmulatorConnector.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/IEmulatorConnector.cs @@ -1,4 +1,5 @@ using System; +using Randomizer.Data.Tracking; namespace Randomizer.SMZ3.Tracking.AutoTracking { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/LuaConnector.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/LuaConnector.cs index d511d6474..6b08cc152 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/LuaConnector.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/LuaConnector.cs @@ -6,6 +6,9 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Randomizer.Data; +using Randomizer.Data.Tracking; +using Randomizer.Shared.Enums; namespace Randomizer.SMZ3.Tracking.AutoTracking { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/ChangedMetroidRegion.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/ChangedMetroidRegion.cs index 017528d74..98ff21a6e 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/ChangedMetroidRegion.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/ChangedMetroidRegion.cs @@ -1,4 +1,7 @@ using System.Linq; +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData.Regions; namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks @@ -18,7 +21,7 @@ public class ChangedMetroidRegion : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.CurrentRegion != _previousMetroidRegionValue || tracker.CurrentRegion?.GetRegion(tracker.World) is Z3Region) { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Crocomire.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Crocomire.cs index 63bd09715..248b2d50c 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Crocomire.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Crocomire.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { /// /// Metroid state check for nearing Crocomire @@ -13,7 +17,7 @@ public class Crocomire : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.CurrentRegion == 2 && currentState.CurrentRoomInRegion == 9 && currentState.SamusX >= 3000 && currentState.SamusY > 500) { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/CrumbleShaft.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/CrumbleShaft.cs index f34ca993b..d4a389ae7 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/CrumbleShaft.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/CrumbleShaft.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { /// /// Metroid state check for reaching Crumble Shaft @@ -13,7 +17,7 @@ public class CrumbleShaft : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.CurrentRegion == 2 && currentState.CurrentRoomInRegion == 8 && prevState.CurrentRoomInRegion == 4) { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/IMetroidStateCheck.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/IMetroidStateCheck.cs index e9e56b7bb..989d48bd3 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/IMetroidStateCheck.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/IMetroidStateCheck.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { /// /// Abstract class for various Metroid state checks @@ -12,6 +16,6 @@ public interface IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState); + bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState); } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/KraidsAwfulSon.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/KraidsAwfulSon.cs index bd24c9042..78d81b23b 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/KraidsAwfulSon.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/KraidsAwfulSon.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { /// /// Metroid state check for nearing Kraid's awful son @@ -13,7 +17,7 @@ public class KraidsAwfulSon : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.CurrentRegion == 1 && currentState.CurrentRoomInRegion == 45 && prevState.CurrentRoomInRegion == 44) { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/MetroidDeath.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/MetroidDeath.cs index 12a285586..c67e699cb 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/MetroidDeath.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/MetroidDeath.cs @@ -1,4 +1,7 @@ using System.Linq; +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData.Regions; using Randomizer.SMZ3.Tracking.Services; @@ -31,7 +34,7 @@ public MetroidDeath(IItemService itemService) /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.Energy != 0 || currentState.ReserveTanks != 0 || prevState.Energy == 0 || currentState.CurrentRoom == 0 && currentState is { CurrentRegion: 0, SamusY: 0 }) diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Mockball.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Mockball.cs index 522849fdc..4e65f9027 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Mockball.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Mockball.cs @@ -1,5 +1,6 @@ -using Randomizer.Shared; -using Randomizer.SMZ3.Tracking.Services; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; +using Randomizer.Shared; namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { @@ -23,7 +24,7 @@ public Mockball(IItemService itemService) /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (_itemService.IsTracked(ItemType.SpeedBooster)) return false; diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Ridley.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Ridley.cs index 8f150ec3e..9fddf811b 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Ridley.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Ridley.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { /// /// Metroid state check related to greeting the Ridley face @@ -13,7 +17,7 @@ public class Ridley : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.CurrentRegion == 2 && currentState.CurrentRoomInRegion == 37 && currentState.SamusX <= 375 && currentState.SamusX >= 100 && currentState.SamusY <= 200) { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Shaktool.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Shaktool.cs index a2311fe58..b75b10843 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Shaktool.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/Shaktool.cs @@ -1,4 +1,7 @@ using System.Linq; +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; using Randomizer.Shared; namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks @@ -16,7 +19,7 @@ public class Shaktool : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState is { CurrentRegion: 4, CurrentRoomInRegion: 36 } && prevState.CurrentRoomInRegion == 28 && tracker.World.FindLocation(LocationId.InnerMaridiaSpringBall)?.State.Cleared != true && diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/SporeSpawn.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/SporeSpawn.cs index a49906407..a56c425a9 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/SporeSpawn.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/MetroidStateChecks/SporeSpawn.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks { /// /// Metroid state check related to Spore Spawn @@ -13,7 +17,7 @@ public class SporeSpawn : IMetroidStateCheck /// The current state in Super Metroid /// The previous state in Super Metroid /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerMetroidState currentState, AutoTrackerMetroidState prevState) { if (currentState.CurrentRegion == 1 && currentState.CurrentRoomInRegion == 22 && prevState.CurrentRoomInRegion == 9) { diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/USB2SNESConnector.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/USB2SNESConnector.cs index 7b859dd97..97df8282a 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/USB2SNESConnector.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/USB2SNESConnector.cs @@ -6,6 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Randomizer.Data; +using Randomizer.Data.Tracking; +using Randomizer.Shared.Enums; using Websocket.Client; namespace Randomizer.SMZ3.Tracking.AutoTracking diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ChangedOverworld.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ChangedOverworld.cs index 494525c8c..9df5967fb 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ChangedOverworld.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ChangedOverworld.cs @@ -1,34 +1,35 @@ using System.Linq; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData.Regions; -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for changing overworld locations +/// Checks if the game is in the overworld state and was either not in the overworld state previously or has changed overworld screens +/// +public class ChangedOverworld : IZeldaStateCheck { /// - /// Zelda State check for changing overworld locations - /// Checks if the game is in the overworld state and was either not in the overworld state previously or has changed overworld screens + /// Executes the check for the current state /// - public class ChangedOverworld : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + if (currentState.State == 0x09 && (prevState.State != 0x09 || currentState.OverworldScreen != prevState.OverworldScreen)) { - if (currentState.State == 0x09 && (prevState.State != 0x09 || currentState.OverworldScreen != prevState.OverworldScreen)) - { - var region = tracker.World.Regions.Where(x => x is Z3Region) - .Select(x => x as Z3Region) - .FirstOrDefault(x => x != null && x.StartingRooms != null && x.StartingRooms.Contains(currentState.OverworldScreen) && x.IsOverworld); - if (region == null) return false; + var region = tracker.World.Regions.Where(x => x is Z3Region) + .Select(x => x as Z3Region) + .FirstOrDefault(x => x != null && x.StartingRooms != null && x.StartingRooms.Contains(currentState.OverworldScreen) && x.IsOverworld); + if (region == null) return false; - tracker.UpdateRegion(region, tracker.Options.AutoTrackerChangeMap); - return true; - } - return false; + tracker.UpdateRegion(region, tracker.Options.AutoTrackerChangeMap); + return true; } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/DiverDown.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/DiverDown.cs index 9afcb1a36..4638171cb 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/DiverDown.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/DiverDown.cs @@ -1,33 +1,35 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for performing the diver down trick +/// Player is walking the lower water area from an unexpected direction +/// +public class DiverDown : IZeldaStateCheck { /// - /// Zelda State check for performing the diver down trick - /// Player is walking the lower water area from an unexpected direction + /// Executes the check for the current state /// - public class DiverDown : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + // Back diver down + if (currentState.CurrentRoom == 118 && currentState.LinkX < 3474 && (currentState.LinkX < 3424 || currentState.LinkX > 3440) && currentState.LinkY <= 4000 && prevState.LinkY > 4000 && (currentState.LinkState is 0 or 6 or 3) && currentState.IsOnBottomHalfOfRoom && currentState.IsOnRightHalfOfRoom) { - // Back diver down - if (currentState.CurrentRoom == 118 && currentState.LinkX < 3474 && (currentState.LinkX < 3424 || currentState.LinkX > 3440) && currentState.LinkY <= 4000 && prevState.LinkY > 4000 && (currentState.LinkState is 0 or 6 or 3) && currentState.IsOnBottomHalfOfRoom && currentState.IsOnRightHalfOfRoom) - { - tracker.SayOnce(x => x.AutoTracker.DiverDown); - return true; - } - // Left side diver down - else if (currentState.CurrentRoom == 53 && currentState.PreviousRoom == 54 && currentState.LinkX > 2808 && currentState.LinkX < 2850 && currentState.LinkY <= 1940 && prevState.LinkY > 1940 && currentState.LinkX <= prevState.LinkX && (currentState.LinkState is 0 or 6 or 3)) - { - tracker.SayOnce(x => x.AutoTracker.DiverDown); - return true; - } - return false; + tracker.SayOnce(x => x.AutoTracker.DiverDown); + return true; } + // Left side diver down + else if (currentState.CurrentRoom == 53 && currentState.PreviousRoom == 54 && currentState.LinkX > 2808 && currentState.LinkX < 2850 && currentState.LinkY <= 1940 && prevState.LinkY > 1940 && currentState.LinkX <= prevState.LinkX && (currentState.LinkState is 0 or 6 or 3)) + { + tracker.SayOnce(x => x.AutoTracker.DiverDown); + return true; + } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/EnteredDungeon.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/EnteredDungeon.cs index d0f267fec..8e43d7fc8 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/EnteredDungeon.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/EnteredDungeon.cs @@ -1,75 +1,76 @@ using System.Collections.Generic; using System.Linq; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData.Regions; using Randomizer.Data.WorldData.Regions.Zelda; using Randomizer.SMZ3.Contracts; -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for detecting entering a dungeon +/// Player is now in the dungeon state from the overworld in one of the designated starting rooms +/// +public class EnteredDungeon : IZeldaStateCheck { + private readonly HashSet _enteredDungeons = new(); + private readonly IWorldAccessor _worldAccessor; + /// - /// Zelda State check for detecting entering a dungeon - /// Player is now in the dungeon state from the overworld in one of the designated starting rooms + /// Constructor /// - public class EnteredDungeon : IZeldaStateCheck + /// + public EnteredDungeon(IWorldAccessor worldAccessor) { - private readonly HashSet _enteredDungeons = new(); - private readonly IWorldAccessor _worldAccessor; + _worldAccessor = worldAccessor; + } - /// - /// Constructor - /// - /// - public EnteredDungeon(IWorldAccessor worldAccessor) + /// + /// Executes the check for the current state + /// + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + { + if (currentState.State == 0x07 && (prevState.State == 0x06 || prevState.State == 0x09 || prevState.State == 0x0F || prevState.State == 0x10 || prevState.State == 0x11)) { - _worldAccessor = worldAccessor; - } + // Get the region for the room + var region = tracker.World.Regions + .OfType() + .FirstOrDefault(x => x.StartingRooms.Count == 0 && x.StartingRooms.Contains(currentState.CurrentRoom) && !x.IsOverworld); + if (region == null) return false; - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) - { - if (currentState.State == 0x07 && (prevState.State == 0x06 || prevState.State == 0x09 || prevState.State == 0x0F || prevState.State == 0x10 || prevState.State == 0x11)) - { - // Get the region for the room - var region = tracker.World.Regions - .OfType() - .FirstOrDefault(x => x.StartingRooms.Count == 0 && x.StartingRooms.Contains(currentState.CurrentRoom) && !x.IsOverworld); - if (region == null) return false; + // Get the dungeon info for the room + var dungeon = region as IDungeon; + if (dungeon == null) return false; - // Get the dungeon info for the room - var dungeon = region as IDungeon; - if (dungeon == null) return false; + if (!_worldAccessor.World.Config.ZeldaKeysanity && !_enteredDungeons.Contains(region) && dungeon.IsPendantDungeon) + { + tracker.Say(tracker.Responses.AutoTracker.EnterPendantDungeon, dungeon.DungeonMetadata.Name, dungeon.DungeonReward?.Metadata.Name); + } + else if (!_worldAccessor.World.Config.ZeldaKeysanity && region is CastleTower) + { + tracker.Say(x => x.AutoTracker.EnterHyruleCastleTower); + } + else if (region is GanonsTower) + { + var clearedCrystalDungeonCount = tracker.World.Dungeons + .Count(x => x.DungeonState.Cleared && x.IsCrystalDungeon); - if (!_worldAccessor.World.Config.ZeldaKeysanity && !_enteredDungeons.Contains(region) && dungeon.IsPendantDungeon) - { - tracker.Say(tracker.Responses.AutoTracker.EnterPendantDungeon, dungeon.DungeonMetadata.Name, dungeon.DungeonReward?.Metadata.Name); - } - else if (!_worldAccessor.World.Config.ZeldaKeysanity && region is CastleTower) + if (clearedCrystalDungeonCount < 7) { - tracker.Say(x => x.AutoTracker.EnterHyruleCastleTower); + tracker.SayOnce(x => x.AutoTracker.EnteredGTEarly, clearedCrystalDungeonCount); } - else if (region is GanonsTower) - { - var clearedCrystalDungeonCount = tracker.World.Dungeons - .Count(x => x.DungeonState.Cleared && x.IsCrystalDungeon); - - if (clearedCrystalDungeonCount < 7) - { - tracker.SayOnce(x => x.AutoTracker.EnteredGTEarly, clearedCrystalDungeonCount); - } - } - - tracker.UpdateRegion(region, tracker.Options.AutoTrackerChangeMap); - _enteredDungeons.Add(region); - return true; } - return false; + tracker.UpdateRegion(region, tracker.Options.AutoTrackerChangeMap); + _enteredDungeons.Add(region); + return true; } + + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FakeFlippers.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FakeFlippers.cs deleted file mode 100644 index 62d7673cc..000000000 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FakeFlippers.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using Randomizer.Shared; - -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks -{ - /// - /// Zelda State check for performing fake flippers - /// Checks if the player is in the swimming state for two states without the flippers - /// - public class FakeFlippers : IZeldaStateCheck - { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) - { - return false; - /*if (currentState.LinkState == 0x04 && prevState.LinkState == 0x04 && tracker.Items.Any(x => x.InternalItemType == ItemType.Flippers && x.TrackingState == 0)) - { - tracker.SayOnce(x => x.AutoTracker.FakeFlippers); - return true; - } - return false;*/ - } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromGanon.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromGanon.cs index fe10ca36f..f3f25a167 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromGanon.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromGanon.cs @@ -1,26 +1,28 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for falling from Ganon's room +/// Checks if the current room is the one below Ganon and the previous room was the Ganon room +/// +public class FallFromGanon : IZeldaStateCheck { /// - /// Zelda State check for falling from Ganon's room - /// Checks if the current room is the one below Ganon and the previous room was the Ganon room + /// Executes the check for the current state /// - public class FallFromGanon : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + if (currentState.CurrentRoom == 16 && currentState.PreviousRoom == 0 && prevState.CurrentRoom == 0) { - if (currentState.CurrentRoom == 16 && currentState.PreviousRoom == 0 && prevState.CurrentRoom == 0) - { - tracker.SayOnce(x => x.AutoTracker.FallFromGanon); - return true; - } - return false; + tracker.SayOnce(x => x.AutoTracker.FallFromGanon); + return true; } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromMoldorm.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromMoldorm.cs index 4cfa259fe..aed73c3ea 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromMoldorm.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/FallFromMoldorm.cs @@ -1,33 +1,35 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for falling from the moldorm room(s) +/// Checks if the current room is the one below moldorm and the previous room was the moldorm room +/// +public class FallFromMoldorm : IZeldaStateCheck { /// - /// Zelda State check for falling from the moldorm room(s) - /// Checks if the current room is the one below moldorm and the previous room was the moldorm room + /// Executes the check for the current state /// - public class FallFromMoldorm : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + // Tower of Hera + if (currentState.CurrentRoom == 23 && currentState.PreviousRoom == 7 && prevState.CurrentRoom == 7) { - // Tower of Hera - if (currentState.CurrentRoom == 23 && currentState.PreviousRoom == 7 && prevState.CurrentRoom == 7) - { - tracker.SayOnce(x => x.AutoTracker.FallFromMoldorm); - return true; - } - // Ganon's Tower - else if (currentState.CurrentRoom == 166 && currentState.PreviousRoom == 77 && prevState.CurrentRoom == 77) - { - tracker.SayOnce(x => x.AutoTracker.FallFromGTMoldorm); - return true; - } - return false; + tracker.SayOnce(x => x.AutoTracker.FallFromMoldorm); + return true; } + // Ganon's Tower + else if (currentState.CurrentRoom == 166 && currentState.PreviousRoom == 77 && prevState.CurrentRoom == 77) + { + tracker.SayOnce(x => x.AutoTracker.FallFromGTMoldorm); + return true; + } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/HeraPot.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/HeraPot.cs index ed1ba0f20..631bb394d 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/HeraPot.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/HeraPot.cs @@ -1,26 +1,28 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for breaking into the Tower of Hera pot +/// player is in the pot room and did not get there from falling from the two rooms above it +/// +public class HeraPot : IZeldaStateCheck { /// - /// Zelda State check for breaking into the Tower of Hera pot - /// player is in the pot room and did not get there from falling from the two rooms above it + /// Executes the check for the current state /// - public class HeraPot : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + if (currentState.CurrentRoom == 167 && prevState.CurrentRoom == 119 && prevState.PreviousRoom != 49) { - if (currentState.CurrentRoom == 167 && prevState.CurrentRoom == 119 && prevState.PreviousRoom != 49) - { - tracker.SayOnce(x => x.AutoTracker.HeraPot); - return true; - } - return false; + tracker.SayOnce(x => x.AutoTracker.HeraPot); + return true; } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IZeldaStateCheck.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IZeldaStateCheck.cs index 2317342f4..d544c47de 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IZeldaStateCheck.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IZeldaStateCheck.cs @@ -3,6 +3,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks { @@ -18,6 +21,6 @@ public interface IZeldaStateCheck /// The current state in Zelda /// The previous state in Zelda /// True if the state was found - bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState); + bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState); } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IceBreaker.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IceBreaker.cs index e3ebf23b5..ea42876a8 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IceBreaker.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/IceBreaker.cs @@ -1,26 +1,28 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for performing the ice breaker trick +/// player is on the right side of the wall but was previous in the room to the left +/// +public class IceBreaker : IZeldaStateCheck { /// - /// Zelda State check for performing the ice breaker trick - /// player is on the right side of the wall but was previous in the room to the left + /// Executes the check for the current state /// - public class IceBreaker : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + if (currentState.CurrentRoom == 31 && currentState.PreviousRoom == 30 && currentState.LinkX >= 8000 && prevState.LinkX < 8000 && currentState.IsOnRightHalfOfRoom && prevState.IsOnRightHalfOfRoom) { - if (currentState.CurrentRoom == 31 && currentState.PreviousRoom == 30 && currentState.LinkX >= 8000 && prevState.LinkX < 8000 && currentState.IsOnRightHalfOfRoom && prevState.IsOnRightHalfOfRoom) - { - tracker.SayOnce(x => x.AutoTracker.IceBreaker); - return true; - } - return false; + tracker.SayOnce(x => x.AutoTracker.IceBreaker); + return true; } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs index e25d9a599..246e1e426 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/SpeckyClip.cs @@ -1,4 +1,8 @@ -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +using Randomizer.Abstractions; +using Randomizer.Data; +using Randomizer.Data.Tracking; + +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks { /// /// Zelda State check for performing the specky clip trick @@ -13,7 +17,7 @@ public class SpeckyClip : IZeldaStateCheck /// The current state in Zelda /// The previous state in Zelda /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { var inCorrectLocation = currentState is { CurrentRoom: 55, IsOnBottomHalfOfRoom: true, IsOnRightHalfOfRoom: false }; diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs index 3d9044b95..b0e362e34 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMap.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; - +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; using Randomizer.Shared; using Randomizer.SMZ3.Contracts; using Randomizer.Data.WorldData.Regions; @@ -8,168 +9,166 @@ using Randomizer.Data.WorldData.Regions.Zelda.DarkWorld.DeathMountain; using Randomizer.Data.WorldData.Regions.Zelda.LightWorld; using Randomizer.Data.WorldData.Regions.Zelda.LightWorld.DeathMountain; -using Randomizer.SMZ3.Tracking.Services; using Randomizer.Data.WorldData; -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for viewing the map +/// Checks if the game state is viewing the map and the value for viewing the full map is set +/// +public class ViewedMap : IZeldaStateCheck { - /// - /// Zelda State check for viewing the map - /// Checks if the game state is viewing the map and the value for viewing the full map is set - /// - public class ViewedMap : IZeldaStateCheck + private ITracker? _tracker; + private readonly IWorldAccessor _worldAccessor; + private bool _lightWorldUpdated; + private bool _darkWorldUpdated; + + public ViewedMap(IWorldAccessor worldAccessor, IItemService itemService) { - private Tracker? _tracker; - private readonly IWorldAccessor _worldAccessor; - private bool _lightWorldUpdated; - private bool _darkWorldUpdated; + _worldAccessor = worldAccessor; + Items = itemService; + } - public ViewedMap(IWorldAccessor worldAccessor, IItemService itemService) - { - _worldAccessor = worldAccessor; - Items = itemService; - } + protected World World => _worldAccessor.World; - protected World World => _worldAccessor.World; + protected IItemService Items { get; } - protected IItemService Items { get; } + /// + /// Executes the check for the current state + /// + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + { + if (tracker.AutoTracker == null || (_lightWorldUpdated && _darkWorldUpdated)) return false; - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + if (currentState.State == 14 && currentState.Substate == 7 && currentState.ReadUInt8(0xE0) == 0x80) { - if (tracker.AutoTracker == null || (_lightWorldUpdated && _darkWorldUpdated)) return false; - - if (currentState.State == 14 && currentState.Substate == 7 && currentState.ReadUInt8(0xE0) == 0x80) + _tracker = tracker; + var currentRegion = tracker.World.Regions + .OfType() + .FirstOrDefault(x => x.StartingRooms != null && x.StartingRooms.Contains(currentState.OverworldScreen) && x.IsOverworld); + if (currentRegion is LightWorldNorthWest or LightWorldNorthEast or LightWorldSouth or LightWorldDeathMountainEast or LightWorldDeathMountainWest && !_lightWorldUpdated) { - _tracker = tracker; - var currentRegion = tracker.World.Regions - .OfType() - .FirstOrDefault(x => x.StartingRooms != null && x.StartingRooms.Contains(currentState.OverworldScreen) && x.IsOverworld); - if (currentRegion is LightWorldNorthWest or LightWorldNorthEast or LightWorldSouth or LightWorldDeathMountainEast or LightWorldDeathMountainWest && !_lightWorldUpdated) + tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(UpdateLightWorldRewards); + if (tracker.Options.AutoSaveLookAtEvents) { - tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(UpdateLightWorldRewards); - if (tracker.Options.AutoSaveLookAtEvents) - { - tracker.AutoTracker.LatestViewAction.Invoke(); - } + tracker.AutoTracker.LatestViewAction.Invoke(); } - else if (currentRegion is DarkWorldNorthWest or DarkWorldNorthEast or DarkWorldSouth or DarkWorldMire or DarkWorldDeathMountainEast or DarkWorldDeathMountainWest && !_darkWorldUpdated) + } + else if (currentRegion is DarkWorldNorthWest or DarkWorldNorthEast or DarkWorldSouth or DarkWorldMire or DarkWorldDeathMountainEast or DarkWorldDeathMountainWest && !_darkWorldUpdated) + { + tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(UpdateDarkWorldRewards); + if (tracker.Options.AutoSaveLookAtEvents) { - tracker.AutoTracker.LatestViewAction = new AutoTrackerViewedAction(UpdateDarkWorldRewards); - if (tracker.Options.AutoSaveLookAtEvents) - { - tracker.AutoTracker.LatestViewAction.Invoke(); - } + tracker.AutoTracker.LatestViewAction.Invoke(); } - - return true; } - return false; - } - - /// - /// Marks all of the rewards for the light world dungeons - /// - private void UpdateLightWorldRewards() - { - if (_tracker == null || _lightWorldUpdated) return; - var rewards = new List(); - var dungeons = new (Region Region, ItemType Map)[] { - (World.EasternPalace, ItemType.MapEP), - (World.DesertPalace, ItemType.MapDP), - (World.TowerOfHera, ItemType.MapTH) - }; + return true; + } + return false; + } - foreach (var (region, map) in dungeons) - { - if (World.Config.ZeldaKeysanity && !Items.IsTracked(map)) - continue; + /// + /// Marks all of the rewards for the light world dungeons + /// + private void UpdateLightWorldRewards() + { + if (_tracker == null || _lightWorldUpdated) return; - var dungeon = (IDungeon)region; - var rewardRegion = (IHasReward)region; - if (dungeon.DungeonState.MarkedReward != dungeon.DungeonState.Reward) - { - rewards.Add(rewardRegion.RewardType); - _tracker.SetDungeonReward(dungeon, rewardRegion.RewardType); - } - } + var rewards = new List(); + var dungeons = new (Region Region, ItemType Map)[] { + (World.EasternPalace, ItemType.MapEP), + (World.DesertPalace, ItemType.MapDP), + (World.TowerOfHera, ItemType.MapTH) + }; - if (!World.Config.ZeldaKeysanity && rewards.Count(x => x == RewardType.CrystalRed || x == RewardType.CrystalBlue) == 3) - { - _tracker.SayOnce(x => x.AutoTracker.LightWorldAllCrystals); - } - else if (rewards.Count == 0) - { - _tracker.Say(x => x.AutoTracker.LookedAtNothing); - } + foreach (var (region, map) in dungeons) + { + if (World.Config.ZeldaKeysanity && !Items.IsTracked(map)) + continue; - // If all dungeons are marked, save the light world as updated - if (dungeons.Select(x => x.Region as IDungeon).Count(x => x?.DungeonState.MarkedReward != null) >= - dungeons.Length) + var dungeon = (IDungeon)region; + var rewardRegion = (IHasReward)region; + if (dungeon.DungeonState.MarkedReward != dungeon.DungeonState.Reward) { - _lightWorldUpdated = true; + rewards.Add(rewardRegion.RewardType); + _tracker.SetDungeonReward(dungeon, rewardRegion.RewardType); } + } + if (!World.Config.ZeldaKeysanity && rewards.Count(x => x == RewardType.CrystalRed || x == RewardType.CrystalBlue) == 3) + { + _tracker.SayOnce(x => x.AutoTracker.LightWorldAllCrystals); + } + else if (rewards.Count == 0) + { + _tracker.Say(x => x.AutoTracker.LookedAtNothing); } - /// - /// Marks all of the rewards for the dark world dungeons - /// - protected void UpdateDarkWorldRewards() + // If all dungeons are marked, save the light world as updated + if (dungeons.Select(x => x.Region as IDungeon).Count(x => x?.DungeonState.MarkedReward != null) >= + dungeons.Length) { - if (_tracker == null || _darkWorldUpdated) return; - - var rewards = new List(); - var dungeons = new (Region Region, ItemType Map)[] { - (World.PalaceOfDarkness, ItemType.MapPD), - (World.SwampPalace, ItemType.MapSP), - (World.SkullWoods, ItemType.MapSW), - (World.ThievesTown, ItemType.MapTT), - (World.IcePalace, ItemType.MapIP), - (World.MiseryMire, ItemType.MapMM), - (World.TurtleRock, ItemType.MapTR) - }; - - foreach (var (region, map) in dungeons) - { - if (World.Config.ZeldaKeysanity && !Items.IsTracked(map)) - continue; + _lightWorldUpdated = true; + } - var dungeon = (IDungeon)region; - var rewardRegion = (IHasReward)region; - if (dungeon.DungeonState.MarkedReward != dungeon.DungeonState.Reward) - { - rewards.Add(rewardRegion.Reward.Type); - _tracker.SetDungeonReward(dungeon, rewardRegion.Reward.Type); - } - } + } - var isMiseryMirePendant = (World.MiseryMire as IDungeon).IsPendantDungeon; - var isTurtleRockPendant = (World.TurtleRock as IDungeon).IsPendantDungeon; + /// + /// Marks all of the rewards for the dark world dungeons + /// + protected void UpdateDarkWorldRewards() + { + if (_tracker == null || _darkWorldUpdated) return; + + var rewards = new List(); + var dungeons = new (Region Region, ItemType Map)[] { + (World.PalaceOfDarkness, ItemType.MapPD), + (World.SwampPalace, ItemType.MapSP), + (World.SkullWoods, ItemType.MapSW), + (World.ThievesTown, ItemType.MapTT), + (World.IcePalace, ItemType.MapIP), + (World.MiseryMire, ItemType.MapMM), + (World.TurtleRock, ItemType.MapTR) + }; + + foreach (var (region, map) in dungeons) + { + if (World.Config.ZeldaKeysanity && !Items.IsTracked(map)) + continue; - if (!World.Config.ZeldaKeysanity && isMiseryMirePendant && isTurtleRockPendant) - { - _tracker.SayOnce(x => x.AutoTracker.DarkWorldNoMedallions); - } - else if (rewards.Count == 0) + var dungeon = (IDungeon)region; + var rewardRegion = (IHasReward)region; + if (dungeon.DungeonState.MarkedReward != dungeon.DungeonState.Reward) { - _tracker.Say(x => x.AutoTracker.LookedAtNothing); + rewards.Add(rewardRegion.Reward.Type); + _tracker.SetDungeonReward(dungeon, rewardRegion.Reward.Type); } + } - // If all dungeons are marked, save the light world as updated - if (dungeons.Select(x => x.Region as IDungeon).Count(x => x?.DungeonState.MarkedReward != null) >= - dungeons.Length) - { - _darkWorldUpdated = true; - } + var isMiseryMirePendant = (World.MiseryMire as IDungeon).IsPendantDungeon; + var isTurtleRockPendant = (World.TurtleRock as IDungeon).IsPendantDungeon; + if (!World.Config.ZeldaKeysanity && isMiseryMirePendant && isTurtleRockPendant) + { + _tracker.SayOnce(x => x.AutoTracker.DarkWorldNoMedallions); + } + else if (rewards.Count == 0) + { + _tracker.Say(x => x.AutoTracker.LookedAtNothing); } + + // If all dungeons are marked, save the light world as updated + if (dungeons.Select(x => x.Region as IDungeon).Count(x => x?.DungeonState.MarkedReward != null) >= + dungeons.Length) + { + _darkWorldUpdated = true; + } + } } diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs index 139cfd58a..c875bf0c8 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ViewedMedallion.cs @@ -1,6 +1,7 @@ -using Randomizer.Data.WorldData; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; +using Randomizer.Data.WorldData; using Randomizer.SMZ3.Contracts; -using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; @@ -9,7 +10,7 @@ namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; /// public class ViewedMedallion : IZeldaStateCheck { - private Tracker? _tracker; + private ITracker? _tracker; private readonly IWorldAccessor _worldAccessor; private bool _mireUpdated; private bool _turtleRockUpdated; @@ -31,7 +32,7 @@ public ViewedMedallion(IWorldAccessor worldAccessor, IItemService itemService) /// The current state in Zelda /// The previous state in Zelda /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { if (tracker.AutoTracker == null || tracker.AutoTracker.LatestViewAction?.IsValid == true || (_mireUpdated && _turtleRockUpdated)) return false; diff --git a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ZeldaDeath.cs b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ZeldaDeath.cs index 10bd38256..6a598fe00 100644 --- a/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ZeldaDeath.cs +++ b/src/Randomizer.SMZ3.Tracking/AutoTracking/ZeldaStateChecks/ZeldaDeath.cs @@ -1,59 +1,57 @@ -using System; -using System.Linq; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; using Randomizer.Data.WorldData.Regions; -using Randomizer.SMZ3.Tracking.Services; -namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks +namespace Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; + +/// +/// Zelda State check for when dying +/// Checks if the player is in the death spiral animation without a fairy +/// +public class ZeldaDeath : IZeldaStateCheck { + public ZeldaDeath(IItemService itemService) + { + Items = itemService; + } + + public IItemService Items { get; } + /// - /// Zelda State check for when dying - /// Checks if the player is in the death spiral animation without a fairy + /// Executes the check for the current state /// - public class ZeldaDeath : IZeldaStateCheck + /// The tracker instance + /// The current state in Zelda + /// The previous state in Zelda + /// True if the check was identified, false otherwise + public bool ExecuteCheck(ITracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) { - public ZeldaDeath(IItemService itemService) - { - Items = itemService; - } + if (tracker.AutoTracker == null || currentState.State != 0x12 || prevState.State == 0x12 || tracker.AutoTracker.PlayerHasFairy) return false; - public IItemService Items { get; } + var silent = tracker.GameService!.PlayerRecentlyKilled; - /// - /// Executes the check for the current state - /// - /// The tracker instance - /// The current state in Zelda - /// The previous state in Zelda - /// True if the check was identified, false otherwise - public bool ExecuteCheck(Tracker tracker, AutoTrackerZeldaState currentState, AutoTrackerZeldaState prevState) + // Say specific message for dying in the particular screen/room the player is in + if (!silent && tracker.CurrentRegion is { WhenDiedInRoom: not null }) { - if (tracker.AutoTracker == null || currentState.State != 0x12 || prevState.State == 0x12 || tracker.AutoTracker.PlayerHasFairy) return false; - - var silent = tracker.GameService!.PlayerRecentlyKilled; - - // Say specific message for dying in the particular screen/room the player is in - if (!silent && tracker.CurrentRegion is { WhenDiedInRoom: not null }) + var region = tracker.CurrentRegion.GetRegion(tracker.World) as Z3Region; + if (region is { IsOverworld: true } && tracker.CurrentRegion.WhenDiedInRoom.TryGetValue(prevState.OverworldScreen, out var locationResponse)) { - var region = tracker.CurrentRegion.GetRegion(tracker.World) as Z3Region; - if (region is { IsOverworld: true } && tracker.CurrentRegion.WhenDiedInRoom.TryGetValue(prevState.OverworldScreen, out var locationResponse)) - { - tracker.Say(locationResponse); - } - else if (region is { IsOverworld: false } && tracker.CurrentRegion.WhenDiedInRoom.TryGetValue(prevState.CurrentRoom, out locationResponse)) - { - tracker.Say(locationResponse); - } + tracker.Say(locationResponse); } - - tracker.TrackDeath(true); - - var death = Items.FirstOrDefault("Death"); - if (death is not null) + else if (region is { IsOverworld: false } && tracker.CurrentRegion.WhenDiedInRoom.TryGetValue(prevState.CurrentRoom, out locationResponse)) { - tracker.TrackItem(death, autoTracked: true, silent: silent); - return true; + tracker.Say(locationResponse); } - return false; } + + tracker.TrackDeath(true); + + var death = Items.FirstOrDefault("Death"); + if (death is not null) + { + tracker.TrackItem(death, autoTracked: true, silent: silent); + return true; + } + return false; } } diff --git a/src/Randomizer.SMZ3.Tracking/BossTrackedEventArgs.cs b/src/Randomizer.SMZ3.Tracking/BossTrackedEventArgs.cs deleted file mode 100644 index 203863cf9..000000000 --- a/src/Randomizer.SMZ3.Tracking/BossTrackedEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Randomizer.Data.WorldData; - -namespace Randomizer.SMZ3.Tracking -{ - /// - /// Provides data for events that occur when clearing a location. - /// - public class BossTrackedEventArgs : TrackerEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The boss that was tracked. - /// The speech recognition confidence. - /// If the location was automatically tracked - public BossTrackedEventArgs(Boss? boss, float? confidence, bool autoTracked) - : base(confidence, autoTracked) - { - Boss = boss; - } - - /// - /// Gets the boss that was tracked. - /// - public Boss? Boss { get; } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/DungeonTrackedEventArgs.cs b/src/Randomizer.SMZ3.Tracking/DungeonTrackedEventArgs.cs deleted file mode 100644 index 401c68a1f..000000000 --- a/src/Randomizer.SMZ3.Tracking/DungeonTrackedEventArgs.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Randomizer.Data.WorldData; -using Randomizer.Data.WorldData.Regions; - -namespace Randomizer.SMZ3.Tracking -{ - /// - /// Provides data for events that occur when tracking a dungeon. - /// - public class DungeonTrackedEventArgs : TrackerEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The dungeon that was tracked. - /// The speech recognition confidence. - /// If the location was automatically tracked - public DungeonTrackedEventArgs(IDungeon? dungeon, float? confidence, bool autoTracked) - : base(confidence, autoTracked) - { - Dungeon = dungeon; - } - - /// - /// Gets the boss that was tracked. - /// - public IDungeon? Dungeon { get; } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/HistoryService.cs b/src/Randomizer.SMZ3.Tracking/HistoryService.cs deleted file mode 100644 index a37fad324..000000000 --- a/src/Randomizer.SMZ3.Tracking/HistoryService.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using System.Text; -using Microsoft.Extensions.Logging; -using Randomizer.Data.WorldData; -using Randomizer.Shared.Enums; -using Randomizer.Shared.Models; -using Randomizer.SMZ3.Contracts; -using Randomizer.SMZ3.Tracking.Services; -using Randomizer.SMZ3.Tracking.VoiceCommands; - -namespace Randomizer.SMZ3.Tracking -{ - /// - /// Service for managing the history of events through a playthrough - /// - public class HistoryService : IHistoryService - { - private readonly ILogger _logger; - private readonly IWorldAccessor _world; - private readonly ITrackerTimerService _timerService; - private ICollection _historyEvents => _world.World.State?.History ?? new List(); - private bool _isMultiworld; - - /// - /// Constructor - /// - /// - /// - /// - public HistoryService(IWorldAccessor world, ILogger logger, ITrackerTimerService timerService) - { - _world = world; - _logger = logger; - _timerService = timerService; - _isMultiworld = world.World.Config.MultiWorld; - } - - /// - /// Adds an event to the history log - /// - /// The type of event - /// If this is an important event or not - /// The name of the event being logged - /// The optional location of where this event happened - /// The created event - public TrackerHistoryEvent AddEvent(HistoryEventType type, bool isImportant, string objectName, Location? location = null) - { - if (_world.World.State == null) - { - throw new InvalidOperationException("World tracker state not loaded"); - } - - var regionName = location?.Region.Name; - var locationName = location?.Room != null ? $"{location.Room.Name} - {location.Name}" : location?.Name; - var addedEvent = new TrackerHistoryEvent() - { - TrackerState = _world.World.State, - Type = type, - IsImportant = isImportant, - ObjectName = objectName, - LocationName = location != null ? $"{regionName} - {locationName}" : null, - LocationId = location?.Id, - Time = _timerService.SecondsElapsed - }; - AddEvent(addedEvent); - return addedEvent; - } - - /// - /// Adds an event to the history log - /// - /// The event to add - public void AddEvent(TrackerHistoryEvent histEvent) - { - if (_isMultiworld) return; - _historyEvents.Add(histEvent); - } - - /// - /// Removes the event that was added last to the log - /// - public void RemoveLastEvent() - { - if (_isMultiworld) return; - if (_historyEvents.Count > 0) - { - Remove(_historyEvents.OrderByDescending(x => x.Id).First()); - } - } - - /// - /// Removes a specific event from the log - /// - /// The event to log - public void Remove(TrackerHistoryEvent histEvent) - { - if (_isMultiworld) return; - _historyEvents.Remove(histEvent); - } - - /// - /// Retrieves the current history log - /// - /// The collection of events - public IReadOnlyCollection GetHistory() => _isMultiworld ? new List() : _historyEvents.ToList(); - - /// - /// Creates the progression log based off of the history - /// - /// The rom that the history is for - /// All of the events to log - /// If only important events should be logged or not - /// The generated log text - public static string GenerateHistoryText(GeneratedRom rom, IReadOnlyCollection history, bool importantOnly = true) - { - var log = new StringBuilder(); - log.AppendLine(Underline($"SMZ3 Cas’ run progression log", '=')); - log.AppendLine($"Generated on {DateTime.Now:F}"); - log.AppendLine($"Seed: {rom.Seed}"); - log.AppendLine($"Settings String: {rom.Settings}"); - log.AppendLine(); - - var enumType = typeof(HistoryEventType); - - foreach (var historyEvent in history.Where(x => !x.IsUndone && (!importantOnly || x.IsImportant)).OrderBy(x => x.Time)) - { - var time = TimeSpan.FromSeconds(historyEvent.Time).ToString(@"hh\:mm\:ss"); - - var field = enumType.GetField(Enum.GetName(historyEvent.Type) ?? ""); - var verb = field?.GetCustomAttribute()?.Description; - - if (historyEvent.LocationId.HasValue) - { - log.AppendLine($"[{time}] {verb} {historyEvent.ObjectName} from {historyEvent.LocationName}"); - } - else - { - log.AppendLine($"[{time}] {verb} {historyEvent.ObjectName}"); - } - } - - var finalTime = TimeSpan.FromSeconds(rom.TrackerState?.SecondsElapsed ?? 0).ToString(@"hh\:mm\:ss"); - - log.AppendLine(); - log.AppendLine($"Final time: {finalTime}"); - - return log.ToString(); - } - - /// - /// Underlines text in the spoiler log - /// - /// The text to be underlined - /// The character to use for underlining - /// The text to be underlined followed by the underlining text - private static string Underline(string text, char line = '-') - => text + "\n" + new string(line, text.Length); - } -} diff --git a/src/Randomizer.SMZ3.Tracking/IHistoryService.cs b/src/Randomizer.SMZ3.Tracking/IHistoryService.cs deleted file mode 100644 index cccc345f5..000000000 --- a/src/Randomizer.SMZ3.Tracking/IHistoryService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Speech.Recognition; -using Microsoft.Extensions.Logging; -using Randomizer.Data.WorldData; -using Randomizer.Shared.Enums; -using Randomizer.Shared.Models; -using Randomizer.Data.Configuration.ConfigTypes; - -namespace Randomizer.SMZ3.Tracking.VoiceCommands -{ - /// - /// Service for managing the history of events through a playthrough - /// - public interface IHistoryService - { - /// - /// Adds an event to the history log - /// - /// The type of event - /// If this is an important event or not - /// The name of the event being logged - /// The optional location of where this event happened - /// The created event - public TrackerHistoryEvent AddEvent(HistoryEventType type, bool isImportant, string objectName, Location? location = null); - - /// - /// Adds an event to the history log - /// - /// The event to add - public void AddEvent(TrackerHistoryEvent histEvent); - - /// - /// Removes the event that was added last to the log - /// - public void RemoveLastEvent(); - - /// - /// Removes a specific event from the log - /// - /// The event to log - public void Remove(TrackerHistoryEvent histEvent); - - /// - /// Retrieves the current history log - /// - /// The collection of events - public IReadOnlyCollection GetHistory(); - } -} diff --git a/src/Randomizer.SMZ3.Tracking/ItemTrackedEventArgs.cs b/src/Randomizer.SMZ3.Tracking/ItemTrackedEventArgs.cs deleted file mode 100644 index 2a980cf21..000000000 --- a/src/Randomizer.SMZ3.Tracking/ItemTrackedEventArgs.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Randomizer.Data.WorldData; - -namespace Randomizer.SMZ3.Tracking -{ - /// - /// Contains event data for item tracking events. - /// - public class ItemTrackedEventArgs : TrackerEventArgs - { - /// - /// Initializes a new instance of the - /// class. - /// - /// The item that was tracked or untracked - /// - /// The name of the item that was tracked. - /// - /// The speech recognition confidence. - /// If the item was auto tracked - public ItemTrackedEventArgs(Item? item, string? trackedAs, float? confidence, bool autoTracked) - : base(confidence, autoTracked) - { - Item = item; - TrackedAs = trackedAs; - } - - /// - /// Gets the name of the item as it was tracked. - /// - public string? TrackedAs { get; } - - /// - /// The item that was tracked or untracked - /// - public Item? Item { get; } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/LocationClearedEventArgs.cs b/src/Randomizer.SMZ3.Tracking/LocationClearedEventArgs.cs deleted file mode 100644 index 9c65fbe9a..000000000 --- a/src/Randomizer.SMZ3.Tracking/LocationClearedEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Randomizer.Data.WorldData; - -namespace Randomizer.SMZ3.Tracking -{ - /// - /// Provides data for events that occur when clearing a location. - /// - public class LocationClearedEventArgs : TrackerEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The location that was cleared. - /// The speech recognition confidence. - /// If the location was automatically tracked - public LocationClearedEventArgs(Location location, float? confidence, bool autoTracked) - : base(confidence, autoTracked) - { - Location = location; - } - - /// - /// Gets the location that was cleared. - /// - public Location Location { get; } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj b/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj index c31586aaa..72b11237f 100644 --- a/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj +++ b/src/Randomizer.SMZ3.Tracking/Randomizer.SMZ3.Tracking.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Randomizer.SMZ3.Tracking/Services/HistoryService.cs b/src/Randomizer.SMZ3.Tracking/Services/HistoryService.cs new file mode 100644 index 000000000..84099637b --- /dev/null +++ b/src/Randomizer.SMZ3.Tracking/Services/HistoryService.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; +using Randomizer.Data.WorldData; +using Randomizer.Shared.Enums; +using Randomizer.Shared.Models; +using Randomizer.SMZ3.Contracts; + +namespace Randomizer.SMZ3.Tracking.Services; + +/// +/// Service for managing the history of events through a playthrough +/// +public class HistoryService : IHistoryService +{ + private readonly ILogger _logger; + private readonly IWorldAccessor _world; + private readonly ITrackerTimerService _timerService; + private ICollection _historyEvents => _world.World.State?.History ?? new List(); + private bool _isMultiworld; + + /// + /// Constructor + /// + /// + /// + /// + public HistoryService(IWorldAccessor world, ILogger logger, ITrackerTimerService timerService) + { + _world = world; + _logger = logger; + _timerService = timerService; + _isMultiworld = world.World.Config.MultiWorld; + } + + /// + /// Adds an event to the history log + /// + /// The type of event + /// If this is an important event or not + /// The name of the event being logged + /// The optional location of where this event happened + /// The created event + public TrackerHistoryEvent AddEvent(HistoryEventType type, bool isImportant, string objectName, Location? location = null) + { + if (_world.World.State == null) + { + throw new InvalidOperationException("World tracker state not loaded"); + } + + var regionName = location?.Region.Name; + var locationName = location?.Room != null ? $"{location.Room.Name} - {location.Name}" : location?.Name; + var addedEvent = new TrackerHistoryEvent() + { + TrackerState = _world.World.State, + Type = type, + IsImportant = isImportant, + ObjectName = objectName, + LocationName = location != null ? $"{regionName} - {locationName}" : null, + LocationId = location?.Id, + Time = _timerService.SecondsElapsed + }; + AddEvent(addedEvent); + return addedEvent; + } + + /// + /// Adds an event to the history log + /// + /// The event to add + public void AddEvent(TrackerHistoryEvent histEvent) + { + if (_isMultiworld) return; + _historyEvents.Add(histEvent); + } + + /// + /// Removes the event that was added last to the log + /// + public void RemoveLastEvent() + { + if (_isMultiworld) return; + if (_historyEvents.Count > 0) + { + Remove(_historyEvents.OrderByDescending(x => x.Id).First()); + } + } + + /// + /// Removes a specific event from the log + /// + /// The event to log + public void Remove(TrackerHistoryEvent histEvent) + { + if (_isMultiworld) return; + _historyEvents.Remove(histEvent); + } + + /// + /// Retrieves the current history log + /// + /// The collection of events + public IReadOnlyCollection GetHistory() => _isMultiworld ? new List() : _historyEvents.ToList(); + + /// + /// Creates the progression log based off of the history + /// + /// The rom that the history is for + /// All of the events to log + /// If only important events should be logged or not + /// The generated log text + public static string GenerateHistoryText(GeneratedRom rom, IReadOnlyCollection history, bool importantOnly = true) + { + var log = new StringBuilder(); + log.AppendLine(Underline($"SMZ3 Cas’ run progression log", '=')); + log.AppendLine($"Generated on {DateTime.Now:F}"); + log.AppendLine($"Seed: {rom.Seed}"); + log.AppendLine($"Settings String: {rom.Settings}"); + log.AppendLine(); + + var enumType = typeof(HistoryEventType); + + foreach (var historyEvent in history.Where(x => !x.IsUndone && (!importantOnly || x.IsImportant)).OrderBy(x => x.Time)) + { + var time = TimeSpan.FromSeconds(historyEvent.Time).ToString(@"hh\:mm\:ss"); + + var field = enumType.GetField(Enum.GetName(historyEvent.Type) ?? ""); + var verb = field?.GetCustomAttribute()?.Description; + + if (historyEvent.LocationId.HasValue) + { + log.AppendLine($"[{time}] {verb} {historyEvent.ObjectName} from {historyEvent.LocationName}"); + } + else + { + log.AppendLine($"[{time}] {verb} {historyEvent.ObjectName}"); + } + } + + var finalTime = TimeSpan.FromSeconds(rom.TrackerState?.SecondsElapsed ?? 0).ToString(@"hh\:mm\:ss"); + + log.AppendLine(); + log.AppendLine($"Final time: {finalTime}"); + + return log.ToString(); + } + + /// + /// Underlines text in the spoiler log + /// + /// The text to be underlined + /// The character to use for underlining + /// The text to be underlined followed by the underlining text + private static string Underline(string text, char line = '-') + => text + "\n" + new string(line, text.Length); +} diff --git a/src/Randomizer.SMZ3.Tracking/Services/IItemService.cs b/src/Randomizer.SMZ3.Tracking/Services/IItemService.cs deleted file mode 100644 index 373f5b94e..000000000 --- a/src/Randomizer.SMZ3.Tracking/Services/IItemService.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System.Collections.Generic; -using Randomizer.Data.WorldData; -using Randomizer.Shared; -using Randomizer.Data.Configuration.ConfigTypes; -using Randomizer.Data.WorldData.Regions; - -namespace Randomizer.SMZ3.Tracking.Services -{ - /// - /// Defines methods for managing items and their tracking state. - /// - public interface IItemService - { - /// - /// Enumerates all items that can be tracked for all players. - /// - /// A collection of items. - IEnumerable AllItems(); - - /// - /// Enumerates all items that can be tracked for the local player. - /// - /// A collection of items. - IEnumerable LocalPlayersItems(); - - /// - /// Enumarates all currently tracked items for the local player. - /// - /// - /// A collection of items that have been tracked at least once. - /// - IEnumerable TrackedItems(); - - /// - /// Finds the item with the specified name for the local player. - /// - /// - /// The name of the item or item stage to find. - /// - /// - /// An representing the item with the specified - /// name, or if there is no item that has the - /// specified name. - /// - Item? FirstOrDefault(string name); - - /// - /// Finds an item with the specified item type for the local player. - /// - /// The type of item to find. - /// - /// An representing the item. If there are - /// multiple configured items with the same type, this method returns - /// one at random. If there no configured items with the specified type, - /// this method returns . - /// - Item? FirstOrDefault(ItemType itemType); - - /// - /// Returns a random name for the specified item including article, e.g. - /// "an E-Tank" or "the Book of Mudora". - /// - /// The type of item whose name to get. - /// - /// The name of the type of item, including "a", "an" or "the" if - /// applicable. - /// - string GetName(ItemType itemType); - - /// - /// Indicates whether an item of the specified type has been tracked - /// for the local player. - /// - /// The type of item to check. - /// - /// if an item with the specified type has been - /// tracked at least once; otherwise, . - /// - bool IsTracked(ItemType itemType); - - /// - /// Finds an reward with the specified item type. - /// - /// The type of reward to find. - /// - /// An representing the reward. If there are - /// multiple configured rewards with the same type, this method returns - /// one at random. If there no configured rewards with the specified type, - /// this method returns . - /// - Reward? FirstOrDefault(RewardType rewardType); - - /// - /// Returns a random name for the specified item including article, e.g. - /// "a blue crystal" or "the green pendant". - /// - /// The reward of item whose name to get. - /// - /// The name of the reward of item, including "a", "an" or "the" if - /// applicable. - /// - string GetName(RewardType rewardType); - - /// - /// Enumerates all rewards that can be tracked for the local player. - /// - /// A collection of rewards. - - IEnumerable AllRewards(); - - /// - /// Enumerates all rewards that can be tracked for the local player. - /// - /// A collection of rewards. - - IEnumerable LocalPlayersRewards(); - - /// - /// Enumarates all currently tracked rewards for the local player. - /// - /// - /// A collection of reward that have been tracked. - /// - IEnumerable TrackedRewards(); - - /// - /// Enumerates all bosses that can be tracked for all players. - /// - /// A collection of bosses. - - IEnumerable AllBosses(); - - /// - /// Enumerates all bosses that can be tracked for the local player. - /// - /// A collection of bosses. - - IEnumerable LocalPlayersBosses(); - - /// - /// Enumarates all currently tracked bosses for the local player. - /// - /// - /// A collection of bosses that have been tracked. - /// - IEnumerable TrackedBosses(); - - /// - /// Retrieves the progression containing all of the tracked items, rewards, and bosses - /// for determining in logic locations - /// - /// If it should be assumed that the player has all keys and keycards - /// - Progression GetProgression(bool assumeKeys); - - /// - /// Retrieves the progression containing all of the tracked items, rewards, and bosses - /// for determining in logic locations - /// - /// The area being looked at to see if keys/keycards should be assumed or not - /// - Progression GetProgression(IHasLocations area); - - /// - /// Clears cached progression - /// - void ResetProgression(); - } -} diff --git a/src/Randomizer.SMZ3.Tracking/Services/ItemService.cs b/src/Randomizer.SMZ3.Tracking/Services/ItemService.cs index 6ae7b9aab..0f66c90c8 100644 --- a/src/Randomizer.SMZ3.Tracking/Services/ItemService.cs +++ b/src/Randomizer.SMZ3.Tracking/Services/ItemService.cs @@ -1,287 +1,288 @@ using System; using System.Collections.Generic; using System.Linq; +using Randomizer.Abstractions; using Randomizer.Data.WorldData; using Randomizer.Shared; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.Data.Configuration.ConfigTypes; using Randomizer.SMZ3.Contracts; using Randomizer.Data.WorldData.Regions; +using Randomizer.Shared.Enums; -namespace Randomizer.SMZ3.Tracking.Services +namespace Randomizer.SMZ3.Tracking.Services; + +/// +/// Manages items and their tracking state. +/// +public class ItemService : IItemService { + private readonly IWorldAccessor _world; + private readonly Dictionary _progression = new(); + /// - /// Manages items and their tracking state. + /// Initializes a new instance of the class + /// with the specified dependencies. /// - public class ItemService : IItemService + /// + /// Specifies the configuration that contains the item data to be + /// managed. + /// + /// + /// Specifies the configuration that contains the reward data + /// + /// Accessor to get data of the world + public ItemService(ItemConfig items, RewardConfig rewards, IWorldAccessor world) { - private readonly IWorldAccessor _world; - private readonly Dictionary _progression = new(); - - /// - /// Initializes a new instance of the class - /// with the specified dependencies. - /// - /// - /// Specifies the configuration that contains the item data to be - /// managed. - /// - /// - /// Specifies the configuration that contains the reward data - /// - /// Accessor to get data of the world - public ItemService(ItemConfig items, RewardConfig rewards, IWorldAccessor world) - { - Items = items; - Rewards = rewards; - _world = world; - } + Items = items; + Rewards = rewards; + _world = world; + } + + /// + /// Gets a collection of trackable items. + /// + protected IReadOnlyCollection Items { get; } + + /// + /// Gets a collection of rewards + /// + protected IReadOnlyCollection Rewards { get; } + + /// + /// Finds the item with the specified name for the local player. + /// + /// + /// The name of the item or item stage to find. + /// + /// + /// An representing the item with the specified + /// name, or if there is no item that has the + /// specified name. + /// + public Item? FirstOrDefault(string name) + => LocalPlayersItems().FirstOrDefault(x => x.Name == name) + ?? LocalPlayersItems().FirstOrDefault(x => x.Metadata.Name.Contains(name, StringComparison.OrdinalIgnoreCase)) + ?? LocalPlayersItems().FirstOrDefault(x => x.Metadata.GetStage(name) != null); + + /// + /// Finds an item with the specified item type for the local player. + /// + /// The type of item to find. + /// + /// An representing the item. If there are + /// multiple configured items with the same type, this method returns + /// one at random. If there no configured items with the specified type, + /// this method returns . + /// + public Item? FirstOrDefault(ItemType itemType) + => LocalPlayersItems().FirstOrDefault(x => x.Type == itemType); + + /// + /// Indicates whether an item of the specified type has been tracked + /// for the local player. + /// + /// The type of item to check. + /// + /// if an item with the specified type has been + /// tracked at least once; otherwise, . + /// + public virtual bool IsTracked(ItemType itemType) + => LocalPlayersItems().Any(x => x.Type == itemType && x.State.TrackingState > 0); + + /// + /// Enumerates all items that can be tracked for all players. + /// + /// A collection of items. + public IEnumerable AllItems() // I really want to discourage this, but necessary for now + => _world.Worlds.SelectMany(x => x.AllItems); + + /// + /// Enumerates all items that can be tracked for the local player. + /// + /// A collection of items. + public IEnumerable LocalPlayersItems() + => _world.Worlds.SelectMany(x => x.AllItems).Where(x => x.World == _world.World); + + /// + /// Enumarates all currently tracked items for the local player. + /// + /// + /// A collection of items that have been tracked at least once. + /// + public IEnumerable TrackedItems() + => LocalPlayersItems().Where(x => x.State.TrackingState > 0); + + /// + /// Returns a random name for the specified item including article, e.g. + /// "an E-Tank" or "the Book of Mudora". + /// + /// The type of item whose name to get. + /// + /// The name of the type of item, including "a", "an" or "the" if + /// applicable. + /// + public virtual string GetName(ItemType itemType) + { + var item = FirstOrDefault(itemType); + return item?.Metadata.NameWithArticle ?? itemType.GetDescription(); + } + + + /// + /// Finds an reward with the specified item type. + /// + /// The type of reward to find. + /// + /// An representing the reward. If there are + /// multiple configured rewards with the same type, this method returns + /// one at random. If there no configured rewards with the specified type, + /// this method returns . + /// + public virtual Reward? FirstOrDefault(RewardType rewardType) + => LocalPlayersRewards().FirstOrDefault(x => x.Type == rewardType); + + /// + /// Returns a random name for the specified item including article, e.g. + /// "a blue crystal" or "the green pendant". + /// + /// The reward of item whose name to get. + /// + /// The name of the reward of item, including "a", "an" or "the" if + /// applicable. + /// + public virtual string GetName(RewardType rewardType) + { + var reward = FirstOrDefault(rewardType); + return reward?.Metadata.NameWithArticle ?? rewardType.GetDescription(); + } + + /// + /// Enumerates all rewards that can be tracked for all players. + /// + /// A collection of rewards. + + public virtual IEnumerable AllRewards() + => _world.Worlds.SelectMany(x => x.Rewards); + + /// + /// Enumerates all rewards that can be tracked for the local player. + /// + /// A collection of rewards. + + public virtual IEnumerable LocalPlayersRewards() + => _world.World.Rewards; + + /// + /// Enumarates all currently tracked rewards for the local player. + /// This uses what the player marked as the reward for dungeons, + /// not the actual dungeon reward. + /// + /// + /// A collection of reward that have been tracked. + /// + public virtual IEnumerable TrackedRewards() + => _world.World.Dungeons.Where(x => x.HasReward && x.DungeonState.Cleared).Select(x => new Reward(x.MarkedReward, _world.World, (IHasReward)x)); + + /// + /// Enumerates all bosses that can be tracked for all players. + /// + /// A collection of bosses. + + public virtual IEnumerable AllBosses() + => _world.Worlds.SelectMany(x => x.AllBosses); + + /// + /// Enumerates all bosses that can be tracked for the local player. + /// + /// A collection of bosses. + + public virtual IEnumerable LocalPlayersBosses() + => _world.World.AllBosses; - /// - /// Gets a collection of trackable items. - /// - protected IReadOnlyCollection Items { get; } - - /// - /// Gets a collection of rewards - /// - protected IReadOnlyCollection Rewards { get; } - - /// - /// Finds the item with the specified name for the local player. - /// - /// - /// The name of the item or item stage to find. - /// - /// - /// An representing the item with the specified - /// name, or if there is no item that has the - /// specified name. - /// - public Item? FirstOrDefault(string name) - => LocalPlayersItems().FirstOrDefault(x => x.Name == name) - ?? LocalPlayersItems().FirstOrDefault(x => x.Metadata.Name.Contains(name, StringComparison.OrdinalIgnoreCase)) - ?? LocalPlayersItems().FirstOrDefault(x => x.Metadata.GetStage(name) != null); - - /// - /// Finds an item with the specified item type for the local player. - /// - /// The type of item to find. - /// - /// An representing the item. If there are - /// multiple configured items with the same type, this method returns - /// one at random. If there no configured items with the specified type, - /// this method returns . - /// - public Item? FirstOrDefault(ItemType itemType) - => LocalPlayersItems().FirstOrDefault(x => x.Type == itemType); - - /// - /// Indicates whether an item of the specified type has been tracked - /// for the local player. - /// - /// The type of item to check. - /// - /// if an item with the specified type has been - /// tracked at least once; otherwise, . - /// - public virtual bool IsTracked(ItemType itemType) - => LocalPlayersItems().Any(x => x.Type == itemType && x.State.TrackingState > 0); - - /// - /// Enumerates all items that can be tracked for all players. - /// - /// A collection of items. - public IEnumerable AllItems() // I really want to discourage this, but necessary for now - => _world.Worlds.SelectMany(x => x.AllItems); - - /// - /// Enumerates all items that can be tracked for the local player. - /// - /// A collection of items. - public IEnumerable LocalPlayersItems() - => _world.Worlds.SelectMany(x => x.AllItems).Where(x => x.World == _world.World); - - /// - /// Enumarates all currently tracked items for the local player. - /// - /// - /// A collection of items that have been tracked at least once. - /// - public IEnumerable TrackedItems() - => LocalPlayersItems().Where(x => x.State.TrackingState > 0); - - /// - /// Returns a random name for the specified item including article, e.g. - /// "an E-Tank" or "the Book of Mudora". - /// - /// The type of item whose name to get. - /// - /// The name of the type of item, including "a", "an" or "the" if - /// applicable. - /// - public virtual string GetName(ItemType itemType) + /// + /// Enumarates all currently tracked bosses for the local player. + /// + /// + /// A collection of bosses that have been tracked. + /// + public virtual IEnumerable TrackedBosses() + => LocalPlayersBosses().Where(x => x.State.Defeated); + + /// + /// Gets the current progression based on the items the user has collected, + /// bosses that the user has beaten, and rewards that the user has received + /// + /// If it should be assumed that the player has all keys + /// The progression object + public Progression GetProgression(bool assumeKeys) + { + var key = $"{assumeKeys}"; + + if (_progression.ContainsKey(key)) { - var item = FirstOrDefault(itemType); - return item?.Metadata.NameWithArticle ?? itemType.GetDescription(); + return _progression[key]; } + var progression = new Progression(); - /// - /// Finds an reward with the specified item type. - /// - /// The type of reward to find. - /// - /// An representing the reward. If there are - /// multiple configured rewards with the same type, this method returns - /// one at random. If there no configured rewards with the specified type, - /// this method returns . - /// - public virtual Reward? FirstOrDefault(RewardType rewardType) - => LocalPlayersRewards().FirstOrDefault(x => x.Type == rewardType); - - /// - /// Returns a random name for the specified item including article, e.g. - /// "a blue crystal" or "the green pendant". - /// - /// The reward of item whose name to get. - /// - /// The name of the reward of item, including "a", "an" or "the" if - /// applicable. - /// - public virtual string GetName(RewardType rewardType) + if (!_world.World.Config.MetroidKeysanity || assumeKeys) { - var reward = FirstOrDefault(rewardType); - return reward?.Metadata.NameWithArticle ?? rewardType.GetDescription(); + progression.AddRange(_world.World.ItemPools.Keycards); + if (assumeKeys) + progression.AddRange(_world.World.ItemPools.Dungeon); } - /// - /// Enumerates all rewards that can be tracked for all players. - /// - /// A collection of rewards. - - public virtual IEnumerable AllRewards() - => _world.Worlds.SelectMany(x => x.Rewards); - - /// - /// Enumerates all rewards that can be tracked for the local player. - /// - /// A collection of rewards. - - public virtual IEnumerable LocalPlayersRewards() - => _world.World.Rewards; - - /// - /// Enumarates all currently tracked rewards for the local player. - /// This uses what the player marked as the reward for dungeons, - /// not the actual dungeon reward. - /// - /// - /// A collection of reward that have been tracked. - /// - public virtual IEnumerable TrackedRewards() - => _world.World.Dungeons.Where(x => x.HasReward && x.DungeonState.Cleared).Select(x => new Reward(x.MarkedReward, _world.World, (IHasReward)x)); - - /// - /// Enumerates all bosses that can be tracked for all players. - /// - /// A collection of bosses. - - public virtual IEnumerable AllBosses() - => _world.Worlds.SelectMany(x => x.AllBosses); - - /// - /// Enumerates all bosses that can be tracked for the local player. - /// - /// A collection of bosses. - - public virtual IEnumerable LocalPlayersBosses() - => _world.World.AllBosses; - - /// - /// Enumarates all currently tracked bosses for the local player. - /// - /// - /// A collection of bosses that have been tracked. - /// - public virtual IEnumerable TrackedBosses() - => LocalPlayersBosses().Where(x => x.State.Defeated); - - /// - /// Gets the current progression based on the items the user has collected, - /// bosses that the user has beaten, and rewards that the user has received - /// - /// If it should be assumed that the player has all keys - /// The progression object - public Progression GetProgression(bool assumeKeys) + foreach (var item in TrackedItems().Select(x => x.State).Distinct()) { - var key = $"{assumeKeys}"; - - if (_progression.ContainsKey(key)) - { - return _progression[key]; - } - - var progression = new Progression(); - - if (!_world.World.Config.MetroidKeysanity || assumeKeys) - { - progression.AddRange(_world.World.ItemPools.Keycards); - if (assumeKeys) - progression.AddRange(_world.World.ItemPools.Dungeon); - } - - foreach (var item in TrackedItems().Select(x => x.State).Distinct()) - { - if (item.Type == null || item.Type == ItemType.Nothing) continue; - progression.AddRange(Enumerable.Repeat(item.Type.Value, item.TrackingState)); - } - - foreach (var reward in TrackedRewards()) - { - progression.Add(reward); - } - - foreach (var boss in TrackedBosses()) - { - progression.Add(boss); - } - - _progression[key] = progression; - return progression; + if (item.Type == null || item.Type == ItemType.Nothing) continue; + progression.AddRange(Enumerable.Repeat(item.Type.Value, item.TrackingState)); } - /// - /// Gets the current progression based on the items the user has collected, - /// bosses that the user has beaten, and rewards that the user has received - /// - /// The area to check to see if keys should be assumed - /// or not - /// The progression object - public Progression GetProgression(IHasLocations area) + foreach (var reward in TrackedRewards()) { - switch (area) - { - case Z3Region: - case Room { Region: Z3Region }: - return GetProgression(assumeKeys: !_world.World.Config.ZeldaKeysanity); - case SMRegion: - case Room { Region: SMRegion }: - return GetProgression(assumeKeys: !_world.World.Config.MetroidKeysanity); - default: - return GetProgression(assumeKeys: _world.World.Config.KeysanityMode == KeysanityMode.None); - } + progression.Add(reward); } - /// - /// Clears the progression cache after collecting new items, rewards, or bosses - /// - public void ResetProgression() + foreach (var boss in TrackedBosses()) { - _progression.Clear(); + progression.Add(boss); } + _progression[key] = progression; + return progression; + } + + /// + /// Gets the current progression based on the items the user has collected, + /// bosses that the user has beaten, and rewards that the user has received + /// + /// The area to check to see if keys should be assumed + /// or not + /// The progression object + public Progression GetProgression(IHasLocations area) + { + switch (area) + { + case Z3Region: + case Room { Region: Z3Region }: + return GetProgression(assumeKeys: !_world.World.Config.ZeldaKeysanity); + case SMRegion: + case Room { Region: SMRegion }: + return GetProgression(assumeKeys: !_world.World.Config.MetroidKeysanity); + default: + return GetProgression(assumeKeys: _world.World.Config.KeysanityMode == KeysanityMode.None); + } + } - // TODO: Tracking methods + /// + /// Clears the progression cache after collecting new items, rewards, or bosses + /// + public void ResetProgression() + { + _progression.Clear(); } + + + // TODO: Tracking methods } diff --git a/src/Randomizer.SMZ3.Tracking/Services/WorldService.cs b/src/Randomizer.SMZ3.Tracking/Services/WorldService.cs index 96f00caf8..a4888c52c 100644 --- a/src/Randomizer.SMZ3.Tracking/Services/WorldService.cs +++ b/src/Randomizer.SMZ3.Tracking/Services/WorldService.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Linq; +using Randomizer.Abstractions; using Randomizer.Data.WorldData; using Randomizer.Data.WorldData.Regions; using Randomizer.Shared; diff --git a/src/Randomizer.SMZ3.Tracking/Tracker.cs b/src/Randomizer.SMZ3.Tracking/Tracker.cs index 63daffd4a..9f5bf533c 100644 --- a/src/Randomizer.SMZ3.Tracking/Tracker.cs +++ b/src/Randomizer.SMZ3.Tracking/Tracker.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Speech.Recognition; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,12 +11,12 @@ using BunLabs; using Microsoft.Extensions.Logging; using MSURandomizerLibrary.Configs; +using Randomizer.Abstractions; using Randomizer.Shared; using Randomizer.Shared.Enums; using Randomizer.Shared.Models; using Randomizer.SMZ3.ChatIntegration; using Randomizer.SMZ3.Contracts; -using Randomizer.SMZ3.Tracking.AutoTracking; using Randomizer.Data.Configuration; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.Data.Configuration.ConfigTypes; @@ -30,1183 +29,1128 @@ using Randomizer.Data; using Randomizer.Data.Options; using Randomizer.Data.Services; +using Randomizer.Data.Tracking; -namespace Randomizer.SMZ3.Tracking +namespace Randomizer.SMZ3.Tracking; + +/// +/// Tracks items and locations in a playthrough by listening for voice +/// commands and responding with text-to-speech. +/// +public class Tracker : ITracker, IDisposable { + private const int RepeatRateModifier = 2; + private static readonly Random s_random = new(); + + + private readonly IWorldAccessor _worldAccessor; + private readonly TrackerModuleFactory _moduleFactory; + private readonly IChatClient _chatClient; + private readonly ILogger _logger; + private readonly TrackerOptionsAccessor _trackerOptions; + private readonly Dictionary _idleTimers; + private readonly Stack<(Action Action, DateTime UndoTime)> _undoHistory = new(); + private readonly RandomizerContext _dbContext; + private readonly ICommunicator _communicator; + 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; + private Dictionary _progression = new(); + private readonly bool _alternateTracker; + private readonly HashSet _saidLines = new(); + private bool _beatenGame; + private IEnumerable? _previousMissingItems; + /// - /// Tracks items and locations in a playthrough by listening for voice - /// commands and responding with text-to-speech. - /// - public class Tracker : IDisposable - { - private const int RepeatRateModifier = 2; - private static readonly Random s_random = new(); - - - private readonly IWorldAccessor _worldAccessor; - private readonly TrackerModuleFactory _moduleFactory; - private readonly IChatClient _chatClient; - private readonly ILogger _logger; - private readonly TrackerOptionsAccessor _trackerOptions; - private readonly Dictionary _idleTimers; - private readonly Stack<(Action Action, DateTime UndoTime)> _undoHistory = new(); - private readonly RandomizerContext _dbContext; - private readonly ICommunicator _communicator; - 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; - private Dictionary _progression = new(); - private readonly bool _alternateTracker; - private readonly HashSet _saidLines = new(); - private bool _beatenGame; - private IEnumerable? _previousMissingItems; - - /// - /// Initializes a new instance of the class. - /// - /// Used to access external configuration. - /// - /// Used to get the world to track in. - /// - /// - /// Used to provide the tracking speech recognition syntax. - /// - /// - /// Used to write logging information. - /// Provides Tracker preferences. - /// The database context - /// - /// - /// Service for - /// - /// - /// - /// - /// - public Tracker(ConfigProvider configProvider, - IWorldAccessor worldAccessor, - TrackerModuleFactory moduleFactory, - IChatClient chatClient, - ILogger logger, - TrackerOptionsAccessor trackerOptions, - RandomizerContext dbContext, - IItemService itemService, - ICommunicator communicator, - IHistoryService historyService, - Configs configs, - IMetadataService metadataService, - ITrackerStateService stateService, - IWorldService worldService, - ITrackerTimerService timerService, - ISpeechRecognitionService speechRecognitionService) - { - if (trackerOptions.Options == null) - throw new InvalidOperationException("Tracker options have not yet been activated."); - - _worldAccessor = worldAccessor; - _moduleFactory = moduleFactory; - _chatClient = chatClient; - _logger = logger; - _trackerOptions = trackerOptions; - _dbContext = dbContext; - ItemService = itemService; - _communicator = communicator; - _stateService = stateService; - _worldService = worldService; - _timerService = timerService; - - // Initialize the tracker configuration - Responses = configs.Responses; - Requests = configs.Requests; - Metadata = metadataService; - ItemService.ResetProgression(); + /// Initializes a new instance of the class. + /// + /// Used to access external configuration. + /// + /// Used to get the world to track in. + /// + /// + /// Used to provide the tracking speech recognition syntax. + /// + /// + /// Used to write logging information. + /// Provides Tracker preferences. + /// The database context + /// + /// + /// Service for + /// + /// + /// + /// + /// + /// + /// + public Tracker(ConfigProvider configProvider, + IWorldAccessor worldAccessor, + TrackerModuleFactory moduleFactory, + IChatClient chatClient, + ILogger logger, + TrackerOptionsAccessor trackerOptions, + RandomizerContext dbContext, + IItemService itemService, + ICommunicator communicator, + IHistoryService historyService, + Configs configs, + IMetadataService metadataService, + ITrackerStateService stateService, + IWorldService worldService, + ITrackerTimerService timerService, + ISpeechRecognitionService speechRecognitionService) + { + if (trackerOptions.Options == null) + throw new InvalidOperationException("Tracker options have not yet been activated."); + + _worldAccessor = worldAccessor; + _moduleFactory = moduleFactory; + _chatClient = chatClient; + _logger = logger; + _trackerOptions = trackerOptions; + _dbContext = dbContext; + ItemService = itemService; + _communicator = communicator; + _stateService = stateService; + _worldService = worldService; + _timerService = timerService; + + // Initialize the tracker configuration + Responses = configs.Responses; + Requests = configs.Requests; + Metadata = metadataService; + ItemService.ResetProgression(); + + History = historyService; + + // Initalize the timers used to trigger idle responses + _idleTimers = Responses.Idle.ToDictionary( + x => x.Key, + x => new Timer(IdleTimerElapsed, x.Key, Timeout.Infinite, Timeout.Infinite)); + + // Initialize the text-to-speech + if (s_random.NextDouble() <= 0.01) + { + _alternateTracker = true; + _communicator.UseAlternateVoice(); + } + + // Initialize the speech recognition engine + _recognizer = speechRecognitionService; + _recognizer.SpeechRecognized += Recognizer_SpeechRecognized; + InitializeMicrophone(); + } - History = historyService; + /// + /// Occurs when any speech was recognized, regardless of configured + /// thresholds. + /// + public event EventHandler? SpeechRecognized; - // Initalize the timers used to trigger idle responses - _idleTimers = Responses.Idle.ToDictionary( - x => x.Key, - x => new Timer(IdleTimerElapsed, x.Key, Timeout.Infinite, Timeout.Infinite)); + /// + /// Occurs when one more more items have been tracked. + /// + public event EventHandler? ItemTracked; - // Initialize the text-to-speech - if (s_random.NextDouble() <= 0.01) - { - _alternateTracker = true; - _communicator.UseAlternateVoice(); - } + /// + /// Occurs when a location has been cleared. + /// + public event EventHandler? LocationCleared; - // Initialize the speech recognition engine - _recognizer = speechRecognitionService; - _recognizer.SpeechRecognized += Recognizer_SpeechRecognized; - InitializeMicrophone(); - } - - /// - /// Occurs when any speech was recognized, regardless of configured - /// thresholds. - /// - public event EventHandler? SpeechRecognized; - - /// - /// Occurs when one more more items have been tracked. - /// - public event EventHandler? ItemTracked; - - /// - /// Occurs when a location has been cleared. - /// - public event EventHandler? LocationCleared; - - /// - /// Occurs when Peg World mode has been toggled on. - /// - public event EventHandler? ToggledPegWorldModeOn; - - /// - /// Occurs when going to Shaktool - /// - public event EventHandler? ToggledShaktoolMode; - - /// - /// Occurs when a Peg World peg has been pegged. - /// - public event EventHandler? PegPegged; - - /// - /// Occurs when the properties of a dungeon have changed. - /// - public event EventHandler? DungeonUpdated; - - /// - /// Occurs when the properties of a boss have changed. - /// - public event EventHandler? BossUpdated; - - /// - /// Occurs when the marked locations have changed - /// - public event EventHandler? MarkedLocationsUpdated; - - /// - /// Occurs when Go mode has been turned on. - /// - public event EventHandler? GoModeToggledOn; - - /// - /// Occurs when the last action was undone. - /// - public event EventHandler? ActionUndone; - - /// - /// Occurs when the tracker state has been loaded. - /// - public event EventHandler? StateLoaded; - - /// - /// Occurs when the map has been updated - /// - public event EventHandler? MapUpdated; - - /// - /// Occurs when the map has been updated - /// - public event EventHandler? BeatGame; - - /// - /// Occurs when the map has died - /// - public event EventHandler? PlayerDied; - - /// - /// Occurs when the current played track number is updated - /// - public event EventHandler? TrackNumberUpdated; - - /// - /// Occurs when the current track has changed - /// - public event EventHandler? TrackChanged; - - /// - /// Set when the progression needs to be updated for the current tracker - /// instance - /// - public bool UpdateTrackerProgression { get; set; } - - /// - /// Gets extra information about locations. - /// - public IMetadataService Metadata { get; } - - /// - /// Gets a reference to the . - /// - public IItemService ItemService { get; } - - /// - /// The number of pegs that have been pegged for Peg World mode - /// - public int PegsPegged { get; set; } - - /// - /// Gets the world for the currently tracked playthrough. - /// - public World World => _worldAccessor.World; - - /// - /// Indicates whether Tracker is in Go Mode. - /// - public bool GoMode { get; private set; } - - /// - /// Indicates whether Tracker is in Peg World mode. - /// - public bool PegWorldMode { get; set; } - - /// - /// Indicates whether Tracker is in Shaktool mode. - /// - public bool ShaktoolMode { get; set; } - - /// - /// If the speech recognition engine was fully initialized - /// - public bool MicrophoneInitialized { get; private set; } - - /// - /// If voice recognition has been enabled or not - /// - public bool VoiceRecognitionEnabled { get; private set; } - - /// - /// Gets the configured responses. - /// - public ResponseConfig Responses { get; } - - /// - /// Gets a collection of basic requests and responses. - /// - public IReadOnlyCollection Requests { get; } - - /// - /// Gets a dictionary containing the rules and the various speech - /// recognition syntaxes. - /// - public IReadOnlyDictionary> Syntax { get; private set; } - = new Dictionary>(); - - /// - /// Gets the tracking preferences. - /// - public TrackerOptions Options => _trackerOptions.Options!; - - /// - /// The generated rom - /// - public GeneratedRom? Rom { get; private set; } - - /// - /// The path to the generated rom - /// - public string? RomPath { get; private set; } - - /// - /// The region the player is currently in according to the Auto Tracker - /// - public RegionInfo? CurrentRegion { get; private set; } - - /// - /// The map to display for the player - /// - public string CurrentMap { get; private set; } = ""; - - /// - /// The current track number being played - /// - public int CurrentTrackNumber { get; private set; } - - /// - /// Gets a string describing tracker's mood. - /// - public string Mood - { - get => _mood ??= Responses.Moods.Keys.Random(Rng.Current) ?? Responses.Moods.Keys.First(); - } - - /// - /// Get if the Tracker has been updated since it was last saved - /// - public bool IsDirty { get; set; } - - /// - /// The Auto Tracker for the Tracker - /// - public AutoTracker? AutoTracker { get; set; } - - /// - /// Service that handles modifying the game via auto tracker - /// - public GameService? GameService { get; set; } - - /// - /// Module that houses the history - /// - public IHistoryService History { get; set; } - - /// - /// Gets or sets a value indicating whether Tracker may give hints when - /// asked about items or locations. - /// - public bool HintsEnabled { get; set; } - - /// - /// Gets or sets a value indicating whether Tracker may give spoilers - /// when asked about items or locations. - /// - public bool SpoilersEnabled { get; set; } - - /// - /// Gets if the local player has beaten the game or not - /// - public bool HasBeatenGame => _beatenGame; - - /// - /// Formats a string so that it will be pronounced correctly by the - /// text-to-speech engine. - /// - /// The text to correct. - /// A string with the pronunciations replaced. - public static string CorrectPronunciation(string name) - => name.Replace("Samus", "Sammus"); - - /// - /// Attempts to replace a user name with a pronunciation-corrected - /// version of it. - /// - /// The user name to correct. - /// - /// The corrected user name, or . - /// - public string CorrectUserNamePronunciation(string userName) - { - var correctedUserName = Responses.Chat.UserNamePronunciation - .SingleOrDefault(x => x.Key.Equals(userName, StringComparison.OrdinalIgnoreCase)); - - return string.IsNullOrEmpty(correctedUserName.Value) ? userName.Replace('_', ' ') : correctedUserName.Value; - } - - /// - /// Initializes the microphone from the default audio device - /// - /// - /// True if the microphone is initialized, false otherwise - /// - public bool InitializeMicrophone() - { - if (MicrophoneInitialized) return true; - MicrophoneInitialized = _recognizer.InitializeMicrophone(); - return MicrophoneInitialized; - } - - /// - /// Loads the state from the database for a given rom - /// - /// The rom to load - /// The full path to the rom to load - /// True or false if the load was successful - public bool Load(GeneratedRom rom, string romPath) - { - IsDirty = false; - Rom = rom; - RomPath = romPath; - var trackerState = _stateService.LoadState(_worldAccessor.Worlds, rom); - - if (trackerState != null) - { - _timerService.SetSavedTime(TimeSpan.FromSeconds(trackerState.SecondsElapsed)); - OnStateLoaded(); - return true; - } - return false; - } + /// + /// Occurs when Peg World mode has been toggled on. + /// + public event EventHandler? ToggledPegWorldModeOn; - /// - /// Saves the state of the tracker to the database - /// - /// - 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); - } + /// + /// Occurs when going to Shaktool + /// + public event EventHandler? ToggledShaktoolMode; - /// - /// Undoes the last operation. - /// - /// The speech recognition confidence. - public void Undo(float confidence) - { - if (_undoHistory.TryPop(out var undoLast)) - { - if ((DateTime.Now - undoLast.UndoTime).TotalMinutes <= (_trackerOptions.Options?.UndoExpirationTime ?? 3)) - { - Say(Responses.ActionUndone); - undoLast.Action(); - OnActionUndone(new TrackerEventArgs(confidence)); - } - else - { - Say(Responses.UndoExpired); - _undoHistory.Push(undoLast); - } - } - else - { - Say(Responses.NothingToUndo); - } - } + /// + /// Occurs when a Peg World peg has been pegged. + /// + public event EventHandler? PegPegged; - /// - /// Toggles Go Mode on. - /// - /// The speech recognition confidence. - public void ToggleGoMode(float? confidence = null) - { - ShutUp(); - Say("Toggled Go Mode ", wait: true); - GoMode = true; - OnGoModeToggledOn(new TrackerEventArgs(confidence)); - Say("on."); + /// + /// Occurs when the properties of a dungeon have changed. + /// + public event EventHandler? DungeonUpdated; - AddUndo(() => - { - GoMode = false; - if (Responses.GoModeToggledOff != null) - Say(Responses.GoModeToggledOff); - }); - } + /// + /// Occurs when the properties of a boss have changed. + /// + public event EventHandler? BossUpdated; - /// - /// Removes one or more items from the available treasure in the - /// specified dungeon. - /// - /// The dungeon. - /// The number of treasures to track. - /// The speech recognition confidence. - /// If this was called by the auto tracker - /// If tracker should state the treasure ammount - /// - /// true if treasure was tracked; false if there is no - /// treasure left to track. - /// - /// - /// This method adds to the undo history if the return value is - /// true. - /// - /// - /// is less than 1. - /// - public bool TrackDungeonTreasure(IDungeon dungeon, float? confidence = null, int amount = 1, bool autoTracked = false, bool stateResponse = true) - { - if (amount < 1) - throw new ArgumentOutOfRangeException(nameof(amount), "The amount of items must be greater than zero."); - - if (amount > dungeon.DungeonState.RemainingTreasure && !dungeon.DungeonState.HasManuallyClearedTreasure) - { - _logger.LogWarning("Trying to track {Amount} treasures in a dungeon with only {Left} treasures left.", amount, dungeon.DungeonState.RemainingTreasure); - Say(Responses.DungeonTooManyTreasuresTracked?.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonState.RemainingTreasure, amount)); - return false; - } + /// + /// Occurs when the marked locations have changed + /// + public event EventHandler? MarkedLocationsUpdated; - if (dungeon.DungeonState.RemainingTreasure > 0) - { - dungeon.DungeonState.RemainingTreasure -= amount; + /// + /// Occurs when Go mode has been turned on. + /// + public event EventHandler? GoModeToggledOn; - // If there are no more treasures and the boss is defeated, clear all locations in the dungeon - var clearedLocations = new List(); - if (dungeon.DungeonState.RemainingTreasure == 0 && dungeon.DungeonState.Cleared) - { - foreach (var location in ((Region)dungeon).Locations.Where(x => !x.State.Cleared)) - { - location.State.Cleared = true; - if (autoTracked) - { - location.State.Autotracked = true; - } - clearedLocations.Add(location); - } - } + /// + /// Occurs when the last action was undone. + /// + public event EventHandler? ActionUndone; - // Always add a response if there's treasure left, even when - // clearing a dungeon (because that means it was out of logic - // and could be relevant) - if (stateResponse && (confidence != null || dungeon.DungeonState.RemainingTreasure >= 1 || autoTracked)) - { - // Try to get the response based on the amount of items left - if (Responses.DungeonTreasureTracked.TryGetValue(dungeon.DungeonState.RemainingTreasure, out var response)) - Say(response.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonState.RemainingTreasure)); - // If we don't have a response for the exact amount and we - // have multiple left, get the one for 2 (considered - // generic) - else if (dungeon.DungeonState.RemainingTreasure >= 2 && Responses.DungeonTreasureTracked.TryGetValue(2, out response)) - Say(response.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonState.RemainingTreasure)); - } + /// + /// Occurs when the tracker state has been loaded. + /// + public event EventHandler? StateLoaded; - OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); - AddUndo(() => - { - dungeon.DungeonState.RemainingTreasure += amount; - foreach (var location in clearedLocations) - { - location.State.Cleared = false; - } - }); + /// + /// Occurs when the map has been updated + /// + public event EventHandler? MapUpdated; - return true; - } - else if (stateResponse && confidence != null && Responses.DungeonTreasureTracked.TryGetValue(-1, out var response)) - { - // Attempted to track treasure when all treasure items were - // already cleared out - Say(response.Format(dungeon.DungeonMetadata.Name)); - } + /// + /// Occurs when the map has been updated + /// + public event EventHandler? BeatGame; - return false; - } + /// + /// Occurs when the map has died + /// + public event EventHandler? PlayerDied; - /// - /// Sets the dungeon's reward to the specific pendant or crystal. - /// - /// The dungeon to mark. - /// - /// The type of pendant or crystal, or null to cycle through the - /// possible rewards. - /// - /// The speech recognition confidence. - /// If this was called by the auto tracker - public void SetDungeonReward(IDungeon dungeon, RewardType? reward = null, float? confidence = null, bool autoTracked = false) - { - var originalReward = dungeon.DungeonState.MarkedReward; - if (reward == null) - { - var currentValue = dungeon.DungeonState.MarkedReward ?? RewardType.None; - dungeon.DungeonState.MarkedReward = Enum.IsDefined(currentValue + 1) ? currentValue + 1 : RewardType.None; - // Cycling through rewards is done via UI, so speaking the - // reward out loud for multiple clicks is kind of annoying - } - else - { - dungeon.DungeonState.MarkedReward = reward.Value; - var rewardObj = ItemService.FirstOrDefault(reward.Value); - Say(Responses.DungeonRewardMarked.Format(dungeon.DungeonMetadata.Name, rewardObj?.Metadata.Name ?? reward.GetDescription())); - } + /// + /// Occurs when the current played track number is updated + /// + public event EventHandler? TrackNumberUpdated; - OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); + /// + /// Occurs when the current track has changed + /// + public event EventHandler? TrackChanged; + + /// + /// Set when the progression needs to be updated for the current tracker + /// instance + /// + public bool UpdateTrackerProgression { get; set; } - if (!autoTracked) AddUndo(() => dungeon.DungeonState.MarkedReward = originalReward); + /// + /// Gets extra information about locations. + /// + public IMetadataService Metadata { get; } + + /// + /// Gets a reference to the . + /// + public IItemService ItemService { get; } + + /// + /// The number of pegs that have been pegged for Peg World mode + /// + public int PegsPegged { get; set; } + + /// + /// Gets the world for the currently tracked playthrough. + /// + public World World => _worldAccessor.World; + + /// + /// Indicates whether Tracker is in Go Mode. + /// + public bool GoMode { get; private set; } + + /// + /// Indicates whether Tracker is in Peg World mode. + /// + public bool PegWorldMode { get; set; } + + /// + /// Indicates whether Tracker is in Shaktool mode. + /// + public bool ShaktoolMode { get; set; } + + /// + /// If the speech recognition engine was fully initialized + /// + public bool MicrophoneInitialized { get; private set; } + + /// + /// If voice recognition has been enabled or not + /// + public bool VoiceRecognitionEnabled { get; private set; } + + /// + /// Gets the configured responses. + /// + public ResponseConfig Responses { get; } + + /// + /// Gets a collection of basic requests and responses. + /// + public IReadOnlyCollection Requests { get; } + + /// + /// Gets a dictionary containing the rules and the various speech + /// recognition syntaxes. + /// + public IReadOnlyDictionary> Syntax { get; private set; } + = new Dictionary>(); + + /// + /// Gets the tracking preferences. + /// + public TrackerOptions Options => _trackerOptions.Options!; + + /// + /// The generated rom + /// + public GeneratedRom? Rom { get; private set; } + + /// + /// The path to the generated rom + /// + public string? RomPath { get; private set; } + + /// + /// The region the player is currently in according to the Auto Tracker + /// + public RegionInfo? CurrentRegion { get; private set; } + + /// + /// The map to display for the player + /// + public string CurrentMap { get; private set; } = ""; + + /// + /// The current track number being played + /// + public int CurrentTrackNumber { get; private set; } + + /// + /// Gets a string describing tracker's mood. + /// + public string Mood + { + get => _mood ??= Responses.Moods.Keys.Random(Rng.Current) ?? Responses.Moods.Keys.First(); + } + + /// + /// Get if the Tracker has been updated since it was last saved + /// + public bool IsDirty { get; set; } + + /// + /// The Auto Tracker for the Tracker + /// + public IAutoTracker? AutoTracker { get; set; } + + /// + /// Service that handles modifying the game via auto tracker + /// + public IGameService? GameService { get; set; } + + /// + /// Module that houses the history + /// + public IHistoryService History { get; set; } + + /// + /// Gets or sets a value indicating whether Tracker may give hints when + /// asked about items or locations. + /// + public bool HintsEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether Tracker may give spoilers + /// when asked about items or locations. + /// + public bool SpoilersEnabled { get; set; } + + /// + /// Gets if the local player has beaten the game or not + /// + public bool HasBeatenGame => _beatenGame; + + /// + /// Attempts to replace a user name with a pronunciation-corrected + /// version of it. + /// + /// The user name to correct. + /// + /// The corrected user name, or . + /// + public string CorrectUserNamePronunciation(string userName) + { + var correctedUserName = Responses.Chat.UserNamePronunciation + .SingleOrDefault(x => x.Key.Equals(userName, StringComparison.OrdinalIgnoreCase)); + + return string.IsNullOrEmpty(correctedUserName.Value) ? userName.Replace('_', ' ') : correctedUserName.Value; + } + + /// + /// Initializes the microphone from the default audio device + /// + /// + /// True if the microphone is initialized, false otherwise + /// + public bool InitializeMicrophone() + { + if (MicrophoneInitialized) return true; + MicrophoneInitialized = _recognizer.InitializeMicrophone(); + return MicrophoneInitialized; + } + + /// + /// Loads the state from the database for a given rom + /// + /// The rom to load + /// The full path to the rom to load + /// True or false if the load was successful + public bool Load(GeneratedRom rom, string romPath) + { + IsDirty = false; + Rom = rom; + RomPath = romPath; + var trackerState = _stateService.LoadState(_worldAccessor.Worlds, rom); + + if (trackerState != null) + { + _timerService.SetSavedTime(TimeSpan.FromSeconds(trackerState.SecondsElapsed)); + OnStateLoaded(); + return true; } + return false; + } - /// - /// Sets the reward of all unmarked dungeons. - /// - /// The reward to set. - /// The speech recognition confidence. - public void SetUnmarkedDungeonReward(RewardType reward, float? confidence = null) + /// + /// Saves the state of the tracker to the database + /// + /// + public async Task SaveAsync() + { + if (Rom == null) { - var unmarkedDungeons = World.Dungeons - .Where(x => x.DungeonState is { HasReward: true, HasMarkedReward: false }) - .ToImmutableList(); + throw new NullReferenceException("No rom loaded into tracker"); + } + IsDirty = false; + await _stateService.SaveStateAsync(_worldAccessor.Worlds, Rom, _timerService.SecondsElapsed); + } - if (unmarkedDungeons.Count > 0) + /// + /// Undoes the last operation. + /// + /// The speech recognition confidence. + public void Undo(float confidence) + { + if (_undoHistory.TryPop(out var undoLast)) + { + if ((DateTime.Now - undoLast.UndoTime).TotalMinutes <= (_trackerOptions.Options?.UndoExpirationTime ?? 3)) { - Say(Responses.RemainingDungeonsMarked.Format(ItemService.GetName(reward))); - unmarkedDungeons.ForEach(dungeon => dungeon.DungeonState.MarkedReward = reward); - AddUndo(() => unmarkedDungeons.ForEach(dungeon => dungeon.DungeonState!.MarkedReward = RewardType.None)); - OnDungeonUpdated(new DungeonTrackedEventArgs(null, confidence, false)); + Say(Responses.ActionUndone); + undoLast.Action(); + OnActionUndone(new TrackerEventArgs(confidence)); } else { - Say(Responses.NoRemainingDungeons); + Say(Responses.UndoExpired); + _undoHistory.Push(undoLast); } } - - /// - /// Sets the dungeon's medallion requirement to the specified item. - /// - /// The dungeon to mark. - /// The medallion that is required. - /// The speech recognition confidence. - public void SetDungeonRequirement(IDungeon dungeon, ItemType? medallion = null, float? confidence = null) + else { - var region = World.Regions.SingleOrDefault(x => dungeon.DungeonMetadata.Name.Contains(x.Name, StringComparison.OrdinalIgnoreCase)); - if (region == null) - { - Say("Strange, I can't find that dungeon in this seed."); - } - else if (region is not INeedsMedallion medallionRegion) - { - Say(Responses.DungeonRequirementInvalid.Format(dungeon.DungeonMetadata.Name)); - return; - } + Say(Responses.NothingToUndo); + } + } - var originalRequirement = dungeon.DungeonState.MarkedMedallion ?? ItemType.Nothing; - if (medallion == null) - { - var medallionItems = new List(Enum.GetValues()); - medallionItems.Insert(0, ItemType.Nothing); - var index = (medallionItems.IndexOf(originalRequirement) + 1) % medallionItems.Count; - dungeon.DungeonState.MarkedMedallion = medallionItems[index]; - OnDungeonUpdated(new DungeonTrackedEventArgs(null, confidence, false)); - } - else - { - if (region is INeedsMedallion medallionRegion - && medallionRegion.Medallion != ItemType.Nothing - && medallionRegion.Medallion != medallion.Value - && confidence >= Options.MinimumSassConfidence) - { - Say(Responses.DungeonRequirementMismatch?.Format( - HintsEnabled ? "a different medallion" : medallionRegion.Medallion.ToString(), - dungeon.DungeonMetadata.Name, - medallion.Value.ToString())); - } + /// + /// Toggles Go Mode on. + /// + /// The speech recognition confidence. + public void ToggleGoMode(float? confidence = null) + { + ShutUp(); + Say("Toggled Go Mode ", wait: true); + GoMode = true; + OnGoModeToggledOn(new TrackerEventArgs(confidence)); + Say("on."); + + AddUndo(() => + { + GoMode = false; + if (Responses.GoModeToggledOff != null) + Say(Responses.GoModeToggledOff); + }); + } - dungeon.DungeonState.MarkedMedallion = medallion.Value; - Say(Responses.DungeonRequirementMarked.Format(medallion.ToString(), dungeon.DungeonMetadata.Name)); - OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, false)); - } + /// + /// Removes one or more items from the available treasure in the + /// specified dungeon. + /// + /// The dungeon. + /// The number of treasures to track. + /// The speech recognition confidence. + /// If this was called by the auto tracker + /// If tracker should state the treasure ammount + /// + /// true if treasure was tracked; false if there is no + /// treasure left to track. + /// + /// + /// This method adds to the undo history if the return value is + /// true. + /// + /// + /// is less than 1. + /// + public bool TrackDungeonTreasure(IDungeon dungeon, float? confidence = null, int amount = 1, bool autoTracked = false, bool stateResponse = true) + { + if (amount < 1) + throw new ArgumentOutOfRangeException(nameof(amount), "The amount of items must be greater than zero."); - AddUndo(() => dungeon.DungeonState.MarkedMedallion = originalRequirement); + if (amount > dungeon.DungeonState.RemainingTreasure && !dungeon.DungeonState.HasManuallyClearedTreasure) + { + _logger.LogWarning("Trying to track {Amount} treasures in a dungeon with only {Left} treasures left.", amount, dungeon.DungeonState.RemainingTreasure); + Say(Responses.DungeonTooManyTreasuresTracked?.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonState.RemainingTreasure, amount)); + return false; } - /// - /// Starts voice recognition. - /// - public virtual bool TryStartTracking() + if (dungeon.DungeonState.RemainingTreasure > 0) { - // Load the modules for voice recognition - StartTimer(true); + dungeon.DungeonState.RemainingTreasure -= amount; - var loadError = false; - try - { - Syntax = _moduleFactory.LoadAll(this, _recognizer.RecognitionEngine, out loadError); - } - catch (Exception e) + // If there are no more treasures and the boss is defeated, clear all locations in the dungeon + var clearedLocations = new List(); + if (dungeon.DungeonState.RemainingTreasure == 0 && dungeon.DungeonState.Cleared) { - _logger.LogError(e, "Unable to load modules"); - loadError = true; + foreach (var location in ((Region)dungeon).Locations.Where(x => !x.State.Cleared)) + { + location.State.Cleared = true; + if (autoTracked) + { + location.State.Autotracked = true; + } + clearedLocations.Add(location); + } } - try - { - EnableVoiceRecognition(); - } - catch (InvalidOperationException e) + // Always add a response if there's treasure left, even when + // clearing a dungeon (because that means it was out of logic + // and could be relevant) + if (stateResponse && (confidence != null || dungeon.DungeonState.RemainingTreasure >= 1 || autoTracked)) { - _logger.LogError(e, "Error enabling voice recognition"); - loadError = true; + // Try to get the response based on the amount of items left + if (Responses.DungeonTreasureTracked.TryGetValue(dungeon.DungeonState.RemainingTreasure, out var response)) + Say(response.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonState.RemainingTreasure)); + // If we don't have a response for the exact amount and we + // have multiple left, get the one for 2 (considered + // generic) + else if (dungeon.DungeonState.RemainingTreasure >= 2 && Responses.DungeonTreasureTracked.TryGetValue(2, out response)) + Say(response.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonState.RemainingTreasure)); } - Say(_alternateTracker ? Responses.StartingTrackingAlternate : Responses.StartedTracking); - RestartIdleTimers(); - return !loadError; - } - - /// - /// Connects Tracker to chat. - /// - /// The user name to connect as. - /// - /// The OAuth token for . - /// - /// - /// The channel to monitor for incoming messages. - /// - /// - /// The is for . - /// - public void ConnectToChat(string? userName, string? oauthToken, string? channel, string? id) - { - if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(oauthToken)) + OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); + AddUndo(() => { - try - { - _chatClient.Connect(userName, oauthToken, channel ?? userName, id ?? ""); - } - catch (AggregateException e) + dungeon.DungeonState.RemainingTreasure += amount; + foreach (var location in clearedLocations) { - _logger.LogError(e, "Error in connection to Twitch chat"); - Say(x => x.Chat.WhenDisconnected); + location.State.Cleared = false; } - } + }); + + return true; + } + else if (stateResponse && confidence != null && Responses.DungeonTreasureTracked.TryGetValue(-1, out var response)) + { + // Attempted to track treasure when all treasure items were + // already cleared out + Say(response.Format(dungeon.DungeonMetadata.Name)); } - /// - /// Sets the start time of the timer - /// - public virtual void StartTimer(bool isInitial = false) + return false; + } + + /// + /// Sets the dungeon's reward to the specific pendant or crystal. + /// + /// The dungeon to mark. + /// + /// The type of pendant or crystal, or null to cycle through the + /// possible rewards. + /// + /// The speech recognition confidence. + /// If this was called by the auto tracker + public void SetDungeonReward(IDungeon dungeon, RewardType? reward = null, float? confidence = null, bool autoTracked = false) + { + var originalReward = dungeon.DungeonState.MarkedReward; + if (reward == null) + { + var currentValue = dungeon.DungeonState.MarkedReward ?? RewardType.None; + dungeon.DungeonState.MarkedReward = Enum.IsDefined(currentValue + 1) ? currentValue + 1 : RewardType.None; + // Cycling through rewards is done via UI, so speaking the + // reward out loud for multiple clicks is kind of annoying + } + else { - _timerService.StartTimer(); + dungeon.DungeonState.MarkedReward = reward.Value; + var rewardObj = ItemService.FirstOrDefault(reward.Value); + Say(Responses.DungeonRewardMarked.Format(dungeon.DungeonMetadata.Name, rewardObj?.Metadata.Name ?? reward.GetDescription())); + } - if (!isInitial) - { - Say(Responses.TimerResumed); - AddUndo(() => _timerService.Undo()); - } + OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); + + if (!autoTracked) AddUndo(() => dungeon.DungeonState.MarkedReward = originalReward); + } + + /// + /// Sets the reward of all unmarked dungeons. + /// + /// The reward to set. + /// The speech recognition confidence. + public void SetUnmarkedDungeonReward(RewardType reward, float? confidence = null) + { + var unmarkedDungeons = World.Dungeons + .Where(x => x.DungeonState is { HasReward: true, HasMarkedReward: false }) + .ToImmutableList(); + + if (unmarkedDungeons.Count > 0) + { + Say(Responses.RemainingDungeonsMarked.Format(ItemService.GetName(reward))); + unmarkedDungeons.ForEach(dungeon => dungeon.DungeonState.MarkedReward = reward); + AddUndo(() => unmarkedDungeons.ForEach(dungeon => dungeon.DungeonState!.MarkedReward = RewardType.None)); + OnDungeonUpdated(new DungeonTrackedEventArgs(null, confidence, false)); } + else + { + Say(Responses.NoRemainingDungeons); + } + } - /// - /// Resets the timer to 0 - /// - public virtual void ResetTimer(bool isInitial = false) + /// + /// Sets the dungeon's medallion requirement to the specified item. + /// + /// The dungeon to mark. + /// The medallion that is required. + /// The speech recognition confidence. + public void SetDungeonRequirement(IDungeon dungeon, ItemType? medallion = null, float? confidence = null) + { + var region = World.Regions.SingleOrDefault(x => dungeon.DungeonMetadata.Name.Contains(x.Name, StringComparison.OrdinalIgnoreCase)); + if (region == null) { - _timerService.ResetTimer(); + Say("Strange, I can't find that dungeon in this seed."); + } + else if (region is not INeedsMedallion medallionRegion) + { + Say(Responses.DungeonRequirementInvalid.Format(dungeon.DungeonMetadata.Name)); + return; + } - if (!isInitial) + var originalRequirement = dungeon.DungeonState.MarkedMedallion ?? ItemType.Nothing; + if (medallion == null) + { + var medallionItems = new List(Enum.GetValues()); + medallionItems.Insert(0, ItemType.Nothing); + var index = (medallionItems.IndexOf(originalRequirement) + 1) % medallionItems.Count; + dungeon.DungeonState.MarkedMedallion = medallionItems[index]; + OnDungeonUpdated(new DungeonTrackedEventArgs(null, confidence, false)); + } + else + { + if (region is INeedsMedallion medallionRegion + && medallionRegion.Medallion != ItemType.Nothing + && medallionRegion.Medallion != medallion.Value + && confidence >= Options.MinimumSassConfidence) { - Say(Responses.TimerReset); - AddUndo(() => _timerService.Undo()); + Say(Responses.DungeonRequirementMismatch?.Format( + HintsEnabled ? "a different medallion" : medallionRegion.Medallion.ToString(), + dungeon.DungeonMetadata.Name, + medallion.Value.ToString())); } + + dungeon.DungeonState.MarkedMedallion = medallion.Value; + Say(Responses.DungeonRequirementMarked.Format(medallion.ToString(), dungeon.DungeonMetadata.Name)); + OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, false)); } - /// - /// Pauses the timer, saving the elapsed time - /// - public virtual Action? PauseTimer(bool addUndo = true) - { - _timerService.StopTimer(); + AddUndo(() => dungeon.DungeonState.MarkedMedallion = originalRequirement); + } - Say(Responses.TimerPaused); + /// + /// Starts voice recognition. + /// + public virtual bool TryStartTracking() + { + // Load the modules for voice recognition + StartTimer(true); - if (addUndo) - { - AddUndo(() => _timerService.Undo()); - return null; - } - else - { - return () => _timerService.Undo(); - } + 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 + { + EnableVoiceRecognition(); + } + catch (InvalidOperationException e) + { + _logger.LogError(e, "Error enabling voice recognition"); + loadError = true; + } + + Say(_alternateTracker ? Responses.StartingTrackingAlternate : Responses.StartedTracking); + RestartIdleTimers(); + return !loadError; + } - /// - /// Pauses or resumes the timer based on if it is - /// currently paused or not - /// - public virtual void ToggleTimer() + /// + /// Connects Tracker to chat. + /// + /// The user name to connect as. + /// + /// The OAuth token for . + /// + /// + /// The channel to monitor for incoming messages. + /// + /// + /// The is for . + /// + public void ConnectToChat(string? userName, string? oauthToken, string? channel, string? id) + { + if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(oauthToken)) { - if (_timerService.IsTimerPaused) + try { - StartTimer(); + _chatClient.Connect(userName, oauthToken, channel ?? userName, id ?? ""); } - else + catch (AggregateException e) { - PauseTimer(); + _logger.LogError(e, "Error in connection to Twitch chat"); + Say(x => x.Chat.WhenDisconnected); } } + } - /// - /// Stops voice recognition. - /// - public virtual void StopTracking() - { - DisableVoiceRecognition(); - _communicator.Abort(); - _chatClient.Disconnect(); - Say(GoMode ? Responses.StoppedTrackingPostGoMode : Responses.StoppedTracking, wait: true); + /// + /// Sets the start time of the timer + /// + public virtual void StartTimer(bool isInitial = false) + { + _timerService.StartTimer(); - foreach (var timer in _idleTimers.Values) - timer.Change(Timeout.Infinite, Timeout.Infinite); + if (!isInitial) + { + Say(Responses.TimerResumed); + AddUndo(() => _timerService.Undo()); } + } + + /// + /// Resets the timer to 0 + /// + public virtual void ResetTimer(bool isInitial = false) + { + _timerService.ResetTimer(); - /// - /// Enables the voice recognizer if the microphone is enabled - /// - public void EnableVoiceRecognition() + if (!isInitial) { - if (MicrophoneInitialized && !VoiceRecognitionEnabled) - { - _logger.LogInformation("Starting speech recognition"); - _recognizer.SetInputToDefaultAudioDevice(); - _recognizer.RecognizeAsyncStop(); - _recognizer.RecognizeAsync(RecognizeMode.Multiple); - VoiceRecognitionEnabled = true; - } + Say(Responses.TimerReset); + AddUndo(() => _timerService.Undo()); } + } + + /// + /// Pauses the timer, saving the elapsed time + /// + public virtual Action? PauseTimer(bool addUndo = true) + { + _timerService.StopTimer(); + + Say(Responses.TimerPaused); - /// - /// Disables voice recognition if it was previously enabled - /// - public void DisableVoiceRecognition() + if (addUndo) { - if (VoiceRecognitionEnabled) - { - VoiceRecognitionEnabled = false; - _recognizer.RecognizeAsyncStop(); - _logger.LogInformation("Stopped speech recognition"); - } + AddUndo(() => _timerService.Undo()); + return null; } - - /// - /// Speak a sentence using text-to-speech. - /// - /// The possible sentences to speak. - /// - /// true if a sentence was spoken, false if was null. - /// - public virtual bool Say(SchrodingersString? text) + else { - if (text == null) - return false; - - return Say(text.ToString()); + return () => _timerService.Undo(); } + } - /// - /// Speak a sentence using text-to-speech. - /// - /// Selects the response to use. - /// - /// true if a sentence was spoken, false if the selected - /// response was null. - /// - public virtual bool Say(Func selectResponse) + /// + /// Pauses or resumes the timer based on if it is + /// currently paused or not + /// + public virtual void ToggleTimer() + { + if (_timerService.IsTimerPaused) { - return Say(selectResponse(Responses)); + StartTimer(); } - - /// - /// Speak a sentence using text-to-speech. - /// - /// The possible sentences to speak. - /// The arguments used to format the text. - /// - /// true if a sentence was spoken, false if was null. - /// - public virtual bool Say(SchrodingersString? text, params object?[] args) + else { - if (text == null) - return false; - - return Say(text.Format(args), wait: false); + PauseTimer(); } + } + + /// + /// Stops voice recognition. + /// + public virtual void StopTracking() + { + DisableVoiceRecognition(); + _communicator.Abort(); + _chatClient.Disconnect(); + Say(GoMode ? Responses.StoppedTrackingPostGoMode : Responses.StoppedTracking, wait: true); - /// - /// Speak a sentence using text-to-speech. - /// - /// Selects the response to use. - /// The arguments used to format the text. - /// - /// true if a sentence was spoken, false if the selected - /// response was null. - /// - public virtual bool Say(Func selectResponse, params object?[] args) + foreach (var timer in _idleTimers.Values) + timer.Change(Timeout.Infinite, Timeout.Infinite); + } + + /// + /// Enables the voice recognizer if the microphone is enabled + /// + public void EnableVoiceRecognition() + { + if (MicrophoneInitialized && !VoiceRecognitionEnabled) { - return Say(selectResponse(Responses), args); + _logger.LogInformation("Starting speech recognition"); + _recognizer.SetInputToDefaultAudioDevice(); + _recognizer.RecognizeAsyncStop(); + _recognizer.RecognizeAsync(RecognizeMode.Multiple); + VoiceRecognitionEnabled = true; } + } - /// - /// Speak a sentence using text-to-speech only one time. - /// - /// The possible sentences to speak. - /// - /// true if a sentence was spoken, false if was null. - /// - public virtual bool SayOnce(SchrodingersString? text) + /// + /// Disables voice recognition if it was previously enabled + /// + public void DisableVoiceRecognition() + { + if (VoiceRecognitionEnabled) { - if (text == null) - return false; + VoiceRecognitionEnabled = false; + _recognizer.RecognizeAsyncStop(); + _logger.LogInformation("Stopped speech recognition"); + } + } - if (!_saidLines.Contains(text)) - { - _saidLines.Add(text); - return Say(text.ToString()); - } + /// + /// Speak a sentence using text-to-speech. + /// + /// The possible sentences to speak. + /// + /// true if a sentence was spoken, false if was null. + /// + public virtual bool Say(SchrodingersString? text) + { + if (text == null) + return false; - return true; - } + return Say(text.ToString()); + } - /// - /// Speak a sentence using text-to-speech only one time. - /// - /// Selects the response to use. - /// - /// true if a sentence was spoken, false if the selected - /// response was null. - /// - public virtual bool SayOnce(Func selectResponse) + /// + /// Speak a sentence using text-to-speech. + /// + /// Selects the response to use. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public virtual bool Say(Func selectResponse) + { + return Say(selectResponse(Responses)); + } + + /// + /// Speak a sentence using text-to-speech. + /// + /// The possible sentences to speak. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if was null. + /// + public virtual bool Say(SchrodingersString? text, params object?[] args) + { + if (text == null) + return false; + + return Say(text.Format(args), wait: false); + } + + /// + /// Speak a sentence using text-to-speech. + /// + /// Selects the response to use. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public virtual bool Say(Func selectResponse, params object?[] args) + { + return Say(selectResponse(Responses), args); + } + + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// The possible sentences to speak. + /// + /// true if a sentence was spoken, false if was null. + /// + public virtual bool SayOnce(SchrodingersString? text) + { + if (text == null) + return false; + + if (!_saidLines.Contains(text)) { - return SayOnce(selectResponse(Responses)); + _saidLines.Add(text); + return Say(text.ToString()); } - /// - /// Speak a sentence using text-to-speech only one time. - /// - /// The possible sentences to speak. - /// The arguments used to format the text. - /// - /// true if a sentence was spoken, false if was null. - /// - protected virtual bool SayOnce(SchrodingersString? text, params object?[] args) - { - if (text == null) - return false; + return true; + } - if (!_saidLines.Contains(text)) - { - _saidLines.Add(text); - return Say(text.Format(args), wait: false); - } + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// Selects the response to use. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public virtual bool SayOnce(Func selectResponse) + { + return SayOnce(selectResponse(Responses)); + } - return true; - } + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// The possible sentences to speak. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if was null. + /// + protected virtual bool SayOnce(SchrodingersString? text, params object?[] args) + { + if (text == null) + return false; - /// - /// Speak a sentence using text-to-speech only one time. - /// - /// Selects the response to use. - /// The arguments used to format the text. - /// - /// true if a sentence was spoken, false if the selected - /// response was null. - /// - public virtual bool SayOnce(Func selectResponse, params object?[] args) - { - return SayOnce(selectResponse(Responses), args); - } - - /// - /// Speak a sentence using text-to-speech. - /// - /// The phrase to speak. - /// - /// true to wait until the text has been spoken completely or - /// false to immediately return. The default is false. - /// - /// - /// true if a sentence was spoken, false if the selected - /// response was null. - /// - public virtual bool Say(string? text, bool wait = false) - { - if (text == null) - return false; - - var formattedText = FormatPlaceholders(text); - if (wait) - _communicator.SayWait(formattedText); - else - _communicator.Say(formattedText); - _lastSpokenText = text; - return true; + if (!_saidLines.Contains(text)) + { + _saidLines.Add(text); + return Say(text.Format(args), wait: false); } - /// - /// Replaces global placeholders in a given string. - /// - /// The text with placeholders to format. - /// The formatted text with placeholders replaced. - [return: NotNullIfNotNull("text")] - protected virtual string? FormatPlaceholders(string? text) - { - if (string.IsNullOrEmpty(text)) - return text; + return true; + } - var builder = new StringBuilder(text); - builder.Replace("{Link}", CorrectPronunciation(World.Config.LinkName)); - builder.Replace("{Samus}", CorrectPronunciation(World.Config.SamusName)); - builder.Replace("{User}", CorrectUserNamePronunciation(Options.UserName ?? "someone")); + /// + /// Speak a sentence using text-to-speech only one time. + /// + /// Selects the response to use. + /// The arguments used to format the text. + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public virtual bool SayOnce(Func selectResponse, params object?[] args) + { + return SayOnce(selectResponse(Responses), args); + } + + /// + /// Speak a sentence using text-to-speech. + /// + /// The phrase to speak. + /// + /// true to wait until the text has been spoken completely or + /// false to immediately return. The default is false. + /// + /// + /// true if a sentence was spoken, false if the selected + /// response was null. + /// + public virtual bool Say(string? text, bool wait = false) + { + if (text == null) + return false; + + var formattedText = FormatPlaceholders(text); + if (wait) + _communicator.SayWait(formattedText); + else + _communicator.Say(formattedText); + _lastSpokenText = text; + return true; + } - // Just in case some text doesn't pass a string.Format - builder.Replace("{{Link}}", CorrectPronunciation(World.Config.LinkName)); - builder.Replace("{{Samus}}", CorrectPronunciation(World.Config.SamusName)); - builder.Replace("{{User}}", CorrectUserNamePronunciation(Options.UserName ?? "someone")); - return builder.ToString(); + /// + /// Replaces global placeholders in a given string. + /// + /// The text with placeholders to format. + /// The formatted text with placeholders replaced. + [return: NotNullIfNotNull("text")] + protected virtual string? FormatPlaceholders(string? text) + { + if (string.IsNullOrEmpty(text)) + return text; + + var builder = new StringBuilder(text); + builder.Replace("{Link}", ITracker.CorrectPronunciation(World.Config.LinkName)); + builder.Replace("{Samus}", ITracker.CorrectPronunciation(World.Config.SamusName)); + builder.Replace("{User}", CorrectUserNamePronunciation(Options.UserName ?? "someone")); + + // Just in case some text doesn't pass a string.Format + builder.Replace("{{Link}}", ITracker.CorrectPronunciation(World.Config.LinkName)); + builder.Replace("{{Samus}}", ITracker.CorrectPronunciation(World.Config.SamusName)); + builder.Replace("{{User}}", CorrectUserNamePronunciation(Options.UserName ?? "someone")); + return builder.ToString(); + } + + /// + /// Repeats the most recently spoken sentence using text-to-speech at a + /// slower rate. + /// + public virtual void Repeat() + { + if (Options.VoiceFrequency == TrackerVoiceFrequency.Disabled) + { + return; } - /// - /// Repeats the most recently spoken sentence using text-to-speech at a - /// slower rate. - /// - public virtual void Repeat() + if (_lastSpokenText == null) { - if (Options.VoiceFrequency == TrackerVoiceFrequency.Disabled) - { - return; - } + Say("I haven't said anything yet."); + return; + } - if (_lastSpokenText == null) - { - Say("I haven't said anything yet."); - return; - } + _communicator.SayWait("I said"); + _communicator.SlowDown(); + Say(_lastSpokenText, wait: true); + _communicator.SpeedUp(); + } - _communicator.SayWait("I said"); - _communicator.SlowDown(); - Say(_lastSpokenText, wait: true); - _communicator.SpeedUp(); - } - - /// - /// Makes Tracker stop talking. - /// - public virtual void ShutUp() - { - _communicator.Abort(); - } - - /// - /// Notifies the user an error occurred. - /// - public virtual void Error() - { - Say(Responses.Error); - } - - /// - /// Cleans up resources used by this class. - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - /// - /// Tracks the specifies item. - /// - /// The item data to track. - /// - /// The text that was tracked, when triggered by voice command. - /// - /// The speech recognition confidence. - /// - /// to attempt to clear a location for the - /// tracked item; if that is done by the caller. - /// - /// If this was tracked by the auto tracker - /// The location an item was tracked from - /// If the item was gifted to the player by tracker or another player - /// If tracker should not say anything - /// - /// if the item was actually tracked; if the item could not be tracked, e.g. when - /// tracking Bow twice. - /// - public bool TrackItem(Item item, string? trackedAs = null, float? confidence = null, bool tryClear = true, bool autoTracked = false, Location? location = null, bool giftedItem = false, bool silent = false) - { - var didTrack = false; - var accessibleBefore = _worldService.AccessibleLocations(false); - var itemName = item.Name; - var originalTrackingState = item.State.TrackingState; - ItemService.ResetProgression(); + /// + /// Makes Tracker stop talking. + /// + public virtual void ShutUp() + { + _communicator.Abort(); + } + + /// + /// Notifies the user an error occurred. + /// + public virtual void Error() + { + Say(Responses.Error); + } - var isGTPreBigKey = !World.Config.ZeldaKeysanity - && autoTracked - && location?.Region.GetType() == typeof(GanonsTower) - && !ItemService.GetProgression(false).BigKeyGT; - var stateResponse = !isGTPreBigKey && !silent && (!autoTracked - || !item.Metadata.IsDungeonItem() - || World.Config.ZeldaKeysanity); + /// + /// Cleans up resources used by this class. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } - // Actually track the item if it's for the local player's world - if (item.World == World) + /// + /// Tracks the specifies item. + /// + /// The item data to track. + /// + /// The text that was tracked, when triggered by voice command. + /// + /// The speech recognition confidence. + /// + /// to attempt to clear a location for the + /// tracked item; if that is done by the caller. + /// + /// If this was tracked by the auto tracker + /// The location an item was tracked from + /// If the item was gifted to the player by tracker or another player + /// If tracker should not say anything + /// + /// if the item was actually tracked; if the item could not be tracked, e.g. when + /// tracking Bow twice. + /// + public bool TrackItem(Item item, string? trackedAs = null, float? confidence = null, bool tryClear = true, bool autoTracked = false, Location? location = null, bool giftedItem = false, bool silent = false) + { + var didTrack = false; + var accessibleBefore = _worldService.AccessibleLocations(false); + var itemName = item.Name; + var originalTrackingState = item.State.TrackingState; + ItemService.ResetProgression(); + + var isGTPreBigKey = !World.Config.ZeldaKeysanity + && autoTracked + && location?.Region.GetType() == typeof(GanonsTower) + && !ItemService.GetProgression(false).BigKeyGT; + var stateResponse = !isGTPreBigKey && !silent && (!autoTracked + || !item.Metadata.IsDungeonItem() + || World.Config.ZeldaKeysanity); + + // Actually track the item if it's for the local player's world + if (item.World == World) + { + if (item.Metadata.HasStages) { - if (item.Metadata.HasStages) + if (trackedAs != null && item.Metadata.GetStage(trackedAs) != null) { - if (trackedAs != null && item.Metadata.GetStage(trackedAs) != null) - { - var stage = item.Metadata.GetStage(trackedAs)!; + var stage = item.Metadata.GetStage(trackedAs)!; - // Tracked by specific stage name (e.g. Tempered Sword), set - // to that stage specifically - var stageName = item.Metadata.Stages[stage.Value].ToString(); + // Tracked by specific stage name (e.g. Tempered Sword), set + // to that stage specifically + var stageName = item.Metadata.Stages[stage.Value].ToString(); - didTrack = item.Track(stage.Value); - if (stateResponse) + didTrack = item.Track(stage.Value); + if (stateResponse) + { + if (didTrack) { - if (didTrack) + if (item.TryGetTrackingResponse(out var response)) { - if (item.TryGetTrackingResponse(out var response)) - { - Say(response.Format(item.Counter)); - } - else - { - Say(Responses.TrackedItemByStage.Format(itemName, stageName)); - } + Say(response.Format(item.Counter)); } else { - Say(Responses.TrackedOlderProgressiveItem?.Format(itemName, item.Metadata.Stages[item.State.TrackingState].ToString())); + Say(Responses.TrackedItemByStage.Format(itemName, stageName)); } } - } - else - { - // Tracked by regular name, upgrade by one step - didTrack = item.Track(); - if (stateResponse) + else { - if (didTrack) - { - if (item.TryGetTrackingResponse(out var response)) - { - Say(response.Format(item.Counter)); - } - else - { - var stageName = item.Metadata.Stages[item.State.TrackingState].ToString(); - Say(Responses.TrackedProgressiveItem.Format(itemName, stageName)); - } - } - else - { - Say(Responses.TrackedTooManyOfAnItem?.Format(itemName)); - } + Say(Responses.TrackedOlderProgressiveItem?.Format(itemName, item.Metadata.Stages[item.State.TrackingState].ToString())); } } } - else if (item.Metadata.Multiple) - { - didTrack = item.Track(); - if (item.TryGetTrackingResponse(out var response)) - { - if (stateResponse) - Say(response.Format(item.Counter)); - } - else if (item.Counter == 1) - { - if (stateResponse) - Say(Responses.TrackedItem.Format(itemName, item.Metadata.NameWithArticle)); - } - else if (item.Counter > 1) - { - if (stateResponse) - Say(Responses.TrackedItemMultiple.Format(item.Metadata.Plural ?? $"{itemName}s", item.Counter, item.Name)); - } - else - { - _logger.LogWarning("Encountered multiple item with counter 0: {Item} has counter {Counter}", item, item.Counter); - if (stateResponse) - Say(Responses.TrackedItem.Format(itemName, item.Metadata.NameWithArticle)); - } - } else { + // Tracked by regular name, upgrade by one step didTrack = item.Track(); if (stateResponse) { @@ -1218,1472 +1162,1520 @@ public bool TrackItem(Item item, string? trackedAs = null, float? confidence = n } else { - Say(Responses.TrackedItem.Format(itemName, item.Metadata.NameWithArticle)); + var stageName = item.Metadata.Stages[item.State.TrackingState].ToString(); + Say(Responses.TrackedProgressiveItem.Format(itemName, stageName)); } } else { - Say(Responses.TrackedAlreadyTrackedItem?.Format(itemName)); + Say(Responses.TrackedTooManyOfAnItem?.Format(itemName)); + } + } + } + } + else if (item.Metadata.Multiple) + { + didTrack = item.Track(); + if (item.TryGetTrackingResponse(out var response)) + { + if (stateResponse) + Say(response.Format(item.Counter)); + } + else if (item.Counter == 1) + { + if (stateResponse) + Say(Responses.TrackedItem.Format(itemName, item.Metadata.NameWithArticle)); + } + else if (item.Counter > 1) + { + if (stateResponse) + Say(Responses.TrackedItemMultiple.Format(item.Metadata.Plural ?? $"{itemName}s", item.Counter, item.Name)); + } + else + { + _logger.LogWarning("Encountered multiple item with counter 0: {Item} has counter {Counter}", item, item.Counter); + if (stateResponse) + Say(Responses.TrackedItem.Format(itemName, item.Metadata.NameWithArticle)); + } + } + else + { + didTrack = item.Track(); + if (stateResponse) + { + if (didTrack) + { + if (item.TryGetTrackingResponse(out var response)) + { + Say(response.Format(item.Counter)); + } + else + { + Say(Responses.TrackedItem.Format(itemName, item.Metadata.NameWithArticle)); } } + else + { + Say(Responses.TrackedAlreadyTrackedItem?.Format(itemName)); + } } } + } - var undoTrack = () => { item.State.TrackingState = originalTrackingState; ItemService.ResetProgression(); }; - OnItemTracked(new ItemTrackedEventArgs(item, trackedAs, confidence, autoTracked)); + var undoTrack = () => { item.State.TrackingState = originalTrackingState; ItemService.ResetProgression(); }; + OnItemTracked(new ItemTrackedEventArgs(item, trackedAs, confidence, autoTracked)); - // Check if we can clear a location - Action? undoClear = null; - Action? undoTrackDungeonTreasure = null; + // Check if we can clear a location + Action? undoClear = null; + Action? undoTrackDungeonTreasure = null; + + // If this was not gifted to the player, try to clear the location + if (!giftedItem && item.Type != ItemType.Nothing) + { + if (location == null && !World.Config.MultiWorld) + { + location = _worldService.Locations(outOfLogic: true, itemFilter: item.Type).TrySingle(); + } - // If this was not gifted to the player, try to clear the location - if (!giftedItem && item.Type != ItemType.Nothing) + // Clear the location if it's for the local player's world + if (location != null && location.World == World && location.State.Cleared == false) { - if (location == null && !World.Config.MultiWorld) + if (stateResponse) { - location = _worldService.Locations(outOfLogic: true, itemFilter: item.Type).TrySingle(); + GiveLocationComment(item, location, isTracking: true, confidence); } - // Clear the location if it's for the local player's world - if (location != null && location.World == World && location.State.Cleared == false) + if (tryClear) { - if (stateResponse) - { - GiveLocationComment(item, location, isTracking: true, confidence); - } + // If this item was in a dungeon, track treasure count + undoTrackDungeonTreasure = TryTrackDungeonTreasure(location, confidence, autoTracked, stateResponse); + + // Important: clear only after tracking dungeon treasure, as + // the "guess dungeon from location" algorithm excludes + // cleared items + location.State.Cleared = true; + World.LastClearedLocation = location; + OnLocationCleared(new(location, confidence, autoTracked)); - if (tryClear) + undoClear = () => location.State.Cleared = false; + if ((location.State.MarkedItem ?? ItemType.Nothing) != ItemType.Nothing) { - // If this item was in a dungeon, track treasure count - undoTrackDungeonTreasure = TryTrackDungeonTreasure(location, confidence, autoTracked, stateResponse); + location.State.MarkedItem = null; + OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); + } + } - // Important: clear only after tracking dungeon treasure, as - // the "guess dungeon from location" algorithm excludes - // cleared items - location.State.Cleared = true; - World.LastClearedLocation = location; - OnLocationCleared(new(location, confidence, autoTracked)); + var isKeysanityForLocation = (location.Region is Z3Region && World.Config.ZeldaKeysanity) || (location.Region is SMRegion && World.Config.MetroidKeysanity); + var items = ItemService.GetProgression(!isKeysanityForLocation); + if (stateResponse && !location.IsAvailable(items) && (confidence >= Options.MinimumSassConfidence || autoTracked)) + { + var locationInfo = location.Metadata; + var roomInfo = location.Room?.Metadata; + var regionInfo = location.Region.Metadata; - undoClear = () => location.State.Cleared = false; - if ((location.State.MarkedItem ?? ItemType.Nothing) != ItemType.Nothing) - { - location.State.MarkedItem = null; - OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); - } + if (locationInfo.OutOfLogic != null) + { + Say(locationInfo.OutOfLogic); } - - var isKeysanityForLocation = (location.Region is Z3Region && World.Config.ZeldaKeysanity) || (location.Region is SMRegion && World.Config.MetroidKeysanity); - var items = ItemService.GetProgression(!isKeysanityForLocation); - if (stateResponse && !location.IsAvailable(items) && (confidence >= Options.MinimumSassConfidence || autoTracked)) + else if (roomInfo?.OutOfLogic != null) + { + Say(roomInfo.OutOfLogic); + } + else if (regionInfo.OutOfLogic != null) { - var locationInfo = location.Metadata; - var roomInfo = location.Room?.Metadata; - var regionInfo = location.Region.Metadata; + Say(regionInfo.OutOfLogic); + } + else + { + var allMissingCombinations = Logic.GetMissingRequiredItems(location, items, out var allMissingItems); + allMissingItems = allMissingItems.OrderBy(x => x); - if (locationInfo.OutOfLogic != null) - { - Say(locationInfo.OutOfLogic); - } - else if (roomInfo?.OutOfLogic != null) - { - Say(roomInfo.OutOfLogic); - } - else if (regionInfo.OutOfLogic != null) + var missingItems = allMissingCombinations.MinBy(x => x.Length); + if (missingItems == null) { - Say(regionInfo.OutOfLogic); + Say(x => x.TrackedOutOfLogicItemTooManyMissing, item.Metadata.Name, locationInfo.Name); } + // Do not say anything if the only thing missing are keys else { - var allMissingCombinations = Logic.GetMissingRequiredItems(location, items, out var allMissingItems); - allMissingItems = allMissingItems.OrderBy(x => x); + var itemsChanged = _previousMissingItems == null || !allMissingItems.SequenceEqual(_previousMissingItems); + var onlyKeys = allMissingItems.All(x => x.IsInAnyCategory(ItemCategory.BigKey, ItemCategory.SmallKey, ItemCategory.Keycard)); + _previousMissingItems = allMissingItems; - var missingItems = allMissingCombinations.MinBy(x => x.Length); - if (missingItems == null) + if (itemsChanged && !onlyKeys) { - Say(x => x.TrackedOutOfLogicItemTooManyMissing, item.Metadata.Name, locationInfo.Name); + var missingItemNames = NaturalLanguage.Join(missingItems.Select(ItemService.GetName)); + Say(x => x.TrackedOutOfLogicItem, item.Metadata.Name, locationInfo.Name, missingItemNames); } - // Do not say anything if the only thing missing are keys - else - { - var itemsChanged = _previousMissingItems == null || !allMissingItems.SequenceEqual(_previousMissingItems); - var onlyKeys = allMissingItems.All(x => x.IsInAnyCategory(ItemCategory.BigKey, ItemCategory.SmallKey, ItemCategory.Keycard)); - _previousMissingItems = allMissingItems; - - if (itemsChanged && !onlyKeys) - { - var missingItemNames = NaturalLanguage.Join(missingItems.Select(ItemService.GetName)); - Say(x => x.TrackedOutOfLogicItem, item.Metadata.Name, locationInfo.Name, missingItemNames); - } - } - - _previousMissingItems = allMissingItems; } + _previousMissingItems = allMissingItems; } + } } + } - var addedEvent = History.AddEvent( - HistoryEventType.TrackedItem, - item.Metadata.IsProgression(World.Config), - item.Metadata.NameWithArticle, - location - ); + var addedEvent = History.AddEvent( + HistoryEventType.TrackedItem, + item.Metadata.IsProgression(World.Config), + item.Metadata.NameWithArticle, + location + ); - IsDirty = true; + IsDirty = true; - if (!autoTracked) + if (!autoTracked) + { + AddUndo(() => { - AddUndo(() => - { - undoTrack(); - undoClear?.Invoke(); - undoTrackDungeonTreasure?.Invoke(); - ItemService.ResetProgression(); - addedEvent.IsUndone = true; - }); - } + undoTrack(); + undoClear?.Invoke(); + undoTrackDungeonTreasure?.Invoke(); + ItemService.ResetProgression(); + addedEvent.IsUndone = true; + }); + } - GiveLocationHint(accessibleBefore); - RestartIdleTimers(); + GiveLocationHint(accessibleBefore); + RestartIdleTimers(); - return didTrack; - } + return didTrack; + } - /// - /// Tracks multiple items at the same time - /// - /// The items to track - /// If the items were tracked via auto tracker - /// If the items were gifted to the player - public void TrackItems(List items, bool autoTracked, bool giftedItem) + /// + /// Tracks multiple items at the same time + /// + /// The items to track + /// If the items were tracked via auto tracker + /// If the items were gifted to the player + public void TrackItems(List items, bool autoTracked, bool giftedItem) + { + if (items.Count == 1) { - if (items.Count == 1) - { - TrackItem(items.First(), null, null, false, autoTracked, null, giftedItem); - return; - } + TrackItem(items.First(), null, null, false, autoTracked, null, giftedItem); + return; + } - ItemService.ResetProgression(); + ItemService.ResetProgression(); - foreach (var item in items) - { - item.Track(); - } + foreach (var item in items) + { + item.Track(); + } - if (items.Count == 2) - { - Say(x => x.TrackedTwoItems, items[0].Metadata.Name, items[1].Metadata.Name); - } - else if (items.Count == 3) - { - Say(x => x.TrackedThreeItems, items[0].Metadata.Name, items[1].Metadata.Name, items[2].Metadata.Name); - } - else + if (items.Count == 2) + { + Say(x => x.TrackedTwoItems, items[0].Metadata.Name, items[1].Metadata.Name); + } + else if (items.Count == 3) + { + Say(x => x.TrackedThreeItems, items[0].Metadata.Name, items[1].Metadata.Name, items[2].Metadata.Name); + } + else + { + var itemsToSay = items.Where(x => x.Type.IsPossibleProgression(World.Config.ZeldaKeysanity, World.Config.MetroidKeysanity)).Take(2).ToList(); + if (itemsToSay.Count() < 2) { - var itemsToSay = items.Where(x => x.Type.IsPossibleProgression(World.Config.ZeldaKeysanity, World.Config.MetroidKeysanity)).Take(2).ToList(); - if (itemsToSay.Count() < 2) - { - var numToTake = 2 - itemsToSay.Count(); - itemsToSay.AddRange(items.Where(x => !x.Type.IsPossibleProgression(World.Config.ZeldaKeysanity, World.Config.MetroidKeysanity)).Take(numToTake)); - } - - Say(x => x.TrackedManyItems, itemsToSay[0].Metadata.Name, itemsToSay[1].Metadata.Name, items.Count - 2); + var numToTake = 2 - itemsToSay.Count(); + itemsToSay.AddRange(items.Where(x => !x.Type.IsPossibleProgression(World.Config.ZeldaKeysanity, World.Config.MetroidKeysanity)).Take(numToTake)); } - OnItemTracked(new ItemTrackedEventArgs(null, null, null, true)); - IsDirty = true; - RestartIdleTimers(); + Say(x => x.TrackedManyItems, itemsToSay[0].Metadata.Name, itemsToSay[1].Metadata.Name, items.Count - 2); } - /// - /// Removes an item from the tracker. - /// - /// The item to untrack. - /// The speech recognition confidence. - public void UntrackItem(Item item, float? confidence = null) - { - var originalTrackingState = item.State.TrackingState; - ItemService.ResetProgression(); + OnItemTracked(new ItemTrackedEventArgs(null, null, null, true)); + IsDirty = true; + RestartIdleTimers(); + } - if (!item.Untrack()) - { - Say(Responses.UntrackedNothing.Format(item.Name, item.Metadata.NameWithArticle)); - return; - } + /// + /// Removes an item from the tracker. + /// + /// The item to untrack. + /// The speech recognition confidence. + public void UntrackItem(Item item, float? confidence = null) + { + var originalTrackingState = item.State.TrackingState; + ItemService.ResetProgression(); - if (item.Metadata.HasStages) - { - Say(Responses.UntrackedProgressiveItem.Format(item.Name, item.Metadata.NameWithArticle)); - } - else if (item.Metadata.Multiple) + if (!item.Untrack()) + { + Say(Responses.UntrackedNothing.Format(item.Name, item.Metadata.NameWithArticle)); + return; + } + + if (item.Metadata.HasStages) + { + Say(Responses.UntrackedProgressiveItem.Format(item.Name, item.Metadata.NameWithArticle)); + } + else if (item.Metadata.Multiple) + { + if (item.State.TrackingState > 0) { - if (item.State.TrackingState > 0) - { - if (item.Metadata.CounterMultiplier > 1) - Say(Responses.UntrackedItemMultiple.Format($"{item.Metadata.CounterMultiplier} {item.Metadata.Plural}", $"{item.Metadata.CounterMultiplier} {item.Metadata.Plural}")); - else - Say(Responses.UntrackedItemMultiple.Format(item.Name, item.Metadata.NameWithArticle)); - } + if (item.Metadata.CounterMultiplier > 1) + Say(Responses.UntrackedItemMultiple.Format($"{item.Metadata.CounterMultiplier} {item.Metadata.Plural}", $"{item.Metadata.CounterMultiplier} {item.Metadata.Plural}")); else - Say(Responses.UntrackedItemMultipleLast.Format(item.Name, item.Metadata.NameWithArticle)); + Say(Responses.UntrackedItemMultiple.Format(item.Name, item.Metadata.NameWithArticle)); } else - { - Say(Responses.UntrackedItem.Format(item.Name, item.Metadata.NameWithArticle)); - } + Say(Responses.UntrackedItemMultipleLast.Format(item.Name, item.Metadata.NameWithArticle)); + } + else + { + Say(Responses.UntrackedItem.Format(item.Name, item.Metadata.NameWithArticle)); + } - IsDirty = true; - OnItemTracked(new(item, null, confidence, false)); - AddUndo(() => { item.State.TrackingState = originalTrackingState; ItemService.ResetProgression(); }); + IsDirty = true; + OnItemTracked(new(item, null, confidence, false)); + AddUndo(() => { item.State.TrackingState = originalTrackingState; ItemService.ResetProgression(); }); + } + + /// + /// Tracks the specifies item and clears it from the specified dungeon. + /// + /// The item data to track. + /// + /// The text that was tracked, when triggered by voice command. + /// + /// The dungeon the item was tracked in. + /// The speech recognition confidence. + public void TrackItem(Item item, IDungeon dungeon, string? trackedAs = null, float? confidence = null) + { + var tracked = TrackItem(item, trackedAs, confidence, tryClear: false); + var undoTrack = _undoHistory.Pop(); + ItemService.ResetProgression(); + + // Check if we can remove something from the remaining treasures in + // a dungeon + Action? undoTrackTreasure = null; + if (tracked) // Only track treasure if we actually tracked anything + { + dungeon = GetDungeonFromItem(item, dungeon)!; + if (TrackDungeonTreasure(dungeon, confidence)) + undoTrackTreasure = _undoHistory.Pop().Action; } - /// - /// Tracks the specifies item and clears it from the specified dungeon. - /// - /// The item data to track. - /// - /// The text that was tracked, when triggered by voice command. - /// - /// The dungeon the item was tracked in. - /// The speech recognition confidence. - public void TrackItem(Item item, IDungeon dungeon, string? trackedAs = null, float? confidence = null) + IsDirty = true; + + // Check if we can remove something from the marked location + var location = _worldService.Locations(itemFilter: item.Type, inRegion: dungeon as Region).TrySingle(); + if (location != null) { - var tracked = TrackItem(item, trackedAs, confidence, tryClear: false); - var undoTrack = _undoHistory.Pop(); - ItemService.ResetProgression(); + location.State.Cleared = true; + World.LastClearedLocation = location; + OnLocationCleared(new(location, confidence, false)); - // Check if we can remove something from the remaining treasures in - // a dungeon - Action? undoTrackTreasure = null; - if (tracked) // Only track treasure if we actually tracked anything + if (location.State.HasMarkedItem) { - dungeon = GetDungeonFromItem(item, dungeon)!; - if (TrackDungeonTreasure(dungeon, confidence)) - undoTrackTreasure = _undoHistory.Pop().Action; + location.State.MarkedItem = null; + OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); } - IsDirty = true; - - // Check if we can remove something from the marked location - var location = _worldService.Locations(itemFilter: item.Type, inRegion: dungeon as Region).TrySingle(); - if (location != null) + AddUndo(() => { - location.State.Cleared = true; - World.LastClearedLocation = location; - OnLocationCleared(new(location, confidence, false)); + undoTrack.Action(); + undoTrackTreasure?.Invoke(); + location.State.Cleared = false; + ItemService.ResetProgression(); + }); + } + else + { + AddUndo(() => + { + undoTrack.Action(); + undoTrackTreasure?.Invoke(); + ItemService.ResetProgression(); + }); + } + } - if (location.State.HasMarkedItem) - { - location.State.MarkedItem = null; - OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); - } + /// + /// Tracks the specified item and clears it from the specified room. + /// + /// The item data to track. + /// + /// The text that was tracked, when triggered by voice command. + /// + /// The area the item was found in. + /// The speech recognition confidence. + public void TrackItem(Item item, IHasLocations area, string? trackedAs = null, float? confidence = null) + { + var locations = area.Locations + .Where(x => x.Item.Type == item.Type) + .ToImmutableList(); + ItemService.ResetProgression(); - AddUndo(() => - { - undoTrack.Action(); - undoTrackTreasure?.Invoke(); - location.State.Cleared = false; - ItemService.ResetProgression(); - }); - } - else - { - AddUndo(() => - { - undoTrack.Action(); - undoTrackTreasure?.Invoke(); - ItemService.ResetProgression(); - }); - } + if (locations.Count == 0) + { + Say(Responses.AreaDoesNotHaveItem?.Format(item.Name, area.Name, item.Metadata.NameWithArticle)); + } + else if (locations.Count > 1) + { + // Consider tracking/clearing everything? + Say(Responses.AreaHasMoreThanOneItem?.Format(item.Name, area.Name, item.Metadata.NameWithArticle)); } - /// - /// Tracks the specified item and clears it from the specified room. - /// - /// The item data to track. - /// - /// The text that was tracked, when triggered by voice command. - /// - /// The area the item was found in. - /// The speech recognition confidence. - public void TrackItem(Item item, IHasLocations area, string? trackedAs = null, float? confidence = null) - { - var locations = area.Locations - .Where(x => x.Item.Type == item.Type) - .ToImmutableList(); - ItemService.ResetProgression(); + IsDirty = true; - if (locations.Count == 0) - { - Say(Responses.AreaDoesNotHaveItem?.Format(item.Name, area.Name, item.Metadata.NameWithArticle)); - } - else if (locations.Count > 1) + TrackItem(item, trackedAs, confidence, tryClear: false); + if (locations.Count == 1) + { + Clear(locations.Single()); + var undoClear = _undoHistory.Pop(); + var undoTrack = _undoHistory.Pop(); + AddUndo(() => { - // Consider tracking/clearing everything? - Say(Responses.AreaHasMoreThanOneItem?.Format(item.Name, area.Name, item.Metadata.NameWithArticle)); - } + undoClear.Action(); + undoTrack.Action(); + ItemService.ResetProgression(); + }); + } + } - IsDirty = true; + /// + /// Sets the item count for the specified item. + /// + /// The item to track. + /// + /// The amount of the item that is in the player's inventory now. + /// + /// The speech recognition confidence. + public void TrackItemAmount(Item item, int count, float confidence) + { + ItemService.ResetProgression(); - TrackItem(item, trackedAs, confidence, tryClear: false); - if (locations.Count == 1) - { - Clear(locations.Single()); - var undoClear = _undoHistory.Pop(); - var undoTrack = _undoHistory.Pop(); - AddUndo(() => - { - undoClear.Action(); - undoTrack.Action(); - ItemService.ResetProgression(); - }); - } + var newItemCount = count; + if (item.Metadata.CounterMultiplier > 1 + && count % item.Metadata.CounterMultiplier == 0) + { + newItemCount = count / item.Metadata.CounterMultiplier.Value; } - /// - /// Sets the item count for the specified item. - /// - /// The item to track. - /// - /// The amount of the item that is in the player's inventory now. - /// - /// The speech recognition confidence. - public void TrackItemAmount(Item item, int count, float confidence) + var oldItemCount = item.State.TrackingState; + if (newItemCount == oldItemCount) { - ItemService.ResetProgression(); + Say(Responses.TrackedExactAmountDuplicate.Format(item.Metadata.Plural, count)); + return; + } - var newItemCount = count; - if (item.Metadata.CounterMultiplier > 1 - && count % item.Metadata.CounterMultiplier == 0) - { - newItemCount = count / item.Metadata.CounterMultiplier.Value; - } + item.State.TrackingState = newItemCount; + if (item.TryGetTrackingResponse(out var response)) + { + Say(response.Format(item.Counter)); + } + else if (newItemCount > oldItemCount) + { + Say(Responses.TrackedItemMultiple.Format(item.Metadata.Plural ?? $"{item.Name}s", item.Counter, item.Name)); + } + else + { + Say(Responses.UntrackedItemMultiple.Format(item.Metadata.Plural ?? $"{item.Name}s", item.Metadata.Plural ?? $"{item.Name}s")); + } - var oldItemCount = item.State.TrackingState; - if (newItemCount == oldItemCount) - { - Say(Responses.TrackedExactAmountDuplicate.Format(item.Metadata.Plural, count)); - return; - } + IsDirty = true; - item.State.TrackingState = newItemCount; - if (item.TryGetTrackingResponse(out var response)) - { - Say(response.Format(item.Counter)); - } - else if (newItemCount > oldItemCount) - { - Say(Responses.TrackedItemMultiple.Format(item.Metadata.Plural ?? $"{item.Name}s", item.Counter, item.Name)); - } - else - { - Say(Responses.UntrackedItemMultiple.Format(item.Metadata.Plural ?? $"{item.Name}s", item.Metadata.Plural ?? $"{item.Name}s")); - } + AddUndo(() => { item.State.TrackingState = oldItemCount; ItemService.ResetProgression(); }); + OnItemTracked(new(item, null, confidence, false)); + } - IsDirty = true; - - AddUndo(() => { item.State.TrackingState = oldItemCount; ItemService.ResetProgression(); }); - OnItemTracked(new(item, null, confidence, false)); - } - - /// - /// Clears every item in the specified area, optionally tracking the - /// cleared items. - /// - /// The area whose items to clear. - /// - /// true to track any items found; false to only clear the - /// affected locations. - /// - /// - /// true to include every item in , even - /// those that are not in logic. false to only include chests - /// available with current items. - /// - /// The speech recognition confidence. - /// - /// Set to true to ignore keys when clearing the location. - /// - public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailable = false, float? confidence = null, bool assumeKeys = false) - { - var locations = area.Locations - .Where(x => x.State.Cleared == false) - .WhereUnless(includeUnavailable, x => x.IsAvailable(ItemService.GetProgression(area))) - .ToImmutableList(); + /// + /// Clears every item in the specified area, optionally tracking the + /// cleared items. + /// + /// The area whose items to clear. + /// + /// true to track any items found; false to only clear the + /// affected locations. + /// + /// + /// true to include every item in , even + /// those that are not in logic. false to only include chests + /// available with current items. + /// + /// The speech recognition confidence. + /// + /// Set to true to ignore keys when clearing the location. + /// + public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailable = false, float? confidence = null, bool assumeKeys = false) + { + var locations = area.Locations + .Where(x => x.State.Cleared == false) + .WhereUnless(includeUnavailable, x => x.IsAvailable(ItemService.GetProgression(area))) + .ToImmutableList(); - ItemService.ResetProgression(); + ItemService.ResetProgression(); - if (locations.Count == 0) - { - var outOfLogicLocations = area.Locations - .Count(x => x.State.Cleared == false); + if (locations.Count == 0) + { + var outOfLogicLocations = area.Locations + .Count(x => x.State.Cleared == false); - if (outOfLogicLocations > 1) - Say(Responses.TrackedNothingOutOfLogic[2].Format(area.Name, outOfLogicLocations)); - else if (outOfLogicLocations > 0) - Say(Responses.TrackedNothingOutOfLogic[1].Format(area.Name, outOfLogicLocations)); + if (outOfLogicLocations > 1) + Say(Responses.TrackedNothingOutOfLogic[2].Format(area.Name, outOfLogicLocations)); + else if (outOfLogicLocations > 0) + Say(Responses.TrackedNothingOutOfLogic[1].Format(area.Name, outOfLogicLocations)); + else + Say(Responses.TrackedNothing.Format(area.Name)); + } + else + { + // If there is only one (available) item here, just call the + // regular TrackItem instead + var onlyLocation = locations.TrySingle(); + if (onlyLocation != null) + { + if (!trackItems) + { + Clear(onlyLocation, confidence); + } else - Say(Responses.TrackedNothing.Format(area.Name)); + { + var item = onlyLocation.Item; + TrackItem(item: item, trackedAs: null, confidence: confidence, tryClear: true, autoTracked: false, location: onlyLocation); + } } else { - // If there is only one (available) item here, just call the - // regular TrackItem instead - var onlyLocation = locations.TrySingle(); - if (onlyLocation != null) + // Otherwise, start counting + var itemsCleared = 0; + var itemsTracked = new List(); + var treasureTracked = 0; + foreach (var location in locations) { + itemsCleared++; if (!trackItems) { - Clear(onlyLocation, confidence); + if (IsTreasure(location.Item) || World.Config.ZeldaKeysanity) + treasureTracked++; + location.State.Cleared = true; + World.LastClearedLocation = location; + OnLocationCleared(new(location, confidence, false)); + continue; } + + var item = location.Item; + if (!item.Track()) + _logger.LogWarning("Failed to track {ItemType} in {Area}.", item.Name, area.Name); // Probably the compass or something, who cares else - { - var item = onlyLocation.Item; - TrackItem(item: item, trackedAs: null, confidence: confidence, tryClear: true, autoTracked: false, location: onlyLocation); - } + itemsTracked.Add(item); + if (IsTreasure(location.Item) || World.Config.ZeldaKeysanity) + treasureTracked++; + + location.State.Cleared = true; } - else + + if (trackItems) { - // Otherwise, start counting - var itemsCleared = 0; - var itemsTracked = new List(); - var treasureTracked = 0; - foreach (var location in locations) - { - itemsCleared++; - if (!trackItems) - { - if (IsTreasure(location.Item) || World.Config.ZeldaKeysanity) - treasureTracked++; - location.State.Cleared = true; - World.LastClearedLocation = location; - OnLocationCleared(new(location, confidence, false)); - continue; - } + var itemNames = confidence >= Options.MinimumSassConfidence + ? NaturalLanguage.Join(itemsTracked, World.Config) + : $"{itemsCleared} items"; + Say(x => x.TrackedMultipleItems, itemsCleared, area.Name, itemNames); - var item = location.Item; - if (!item.Track()) - _logger.LogWarning("Failed to track {ItemType} in {Area}.", item.Name, area.Name); // Probably the compass or something, who cares - else - itemsTracked.Add(item); - if (IsTreasure(location.Item) || World.Config.ZeldaKeysanity) - treasureTracked++; + var roomInfo = area is Room room ? room.Metadata : null; + var regionInfo = area is Region region ? region.Metadata : null; - location.State.Cleared = true; + if (roomInfo?.OutOfLogic != null) + { + Say(roomInfo.OutOfLogic); } - - if (trackItems) + else if (regionInfo?.OutOfLogic != null) { - var itemNames = confidence >= Options.MinimumSassConfidence - ? NaturalLanguage.Join(itemsTracked, World.Config) - : $"{itemsCleared} items"; - Say(x => x.TrackedMultipleItems, itemsCleared, area.Name, itemNames); - - var roomInfo = area is Room room ? room.Metadata : null; - var regionInfo = area is Region region ? region.Metadata : null; - - if (roomInfo?.OutOfLogic != null) - { - Say(roomInfo.OutOfLogic); - } - else if (regionInfo?.OutOfLogic != null) - { - Say(regionInfo.OutOfLogic); - } - else + Say(regionInfo.OutOfLogic); + } + else + { + var progression = ItemService.GetProgression(area); + var someOutOfLogicLocation = locations.Where(x => !x.IsAvailable(progression)).Random(s_random); + if (someOutOfLogicLocation != null && confidence >= Options.MinimumSassConfidence) { - var progression = ItemService.GetProgression(area); - var someOutOfLogicLocation = locations.Where(x => !x.IsAvailable(progression)).Random(s_random); - if (someOutOfLogicLocation != null && confidence >= Options.MinimumSassConfidence) + var someOutOfLogicItem = someOutOfLogicLocation.Item; + var missingItems = Logic.GetMissingRequiredItems(someOutOfLogicLocation, progression, out _).MinBy(x => x.Length); + if (missingItems != null) + { + var missingItemNames = NaturalLanguage.Join(missingItems.Select(ItemService.GetName)); + Say(x => x.TrackedOutOfLogicItem, someOutOfLogicItem.Metadata.Name, someOutOfLogicLocation.Metadata.Name, missingItemNames); + } + else { - var someOutOfLogicItem = someOutOfLogicLocation.Item; - var missingItems = Logic.GetMissingRequiredItems(someOutOfLogicLocation, progression, out _).MinBy(x => x.Length); - if (missingItems != null) - { - var missingItemNames = NaturalLanguage.Join(missingItems.Select(ItemService.GetName)); - Say(x => x.TrackedOutOfLogicItem, someOutOfLogicItem.Metadata.Name, someOutOfLogicLocation.Metadata.Name, missingItemNames); - } - else - { - Say(x => x.TrackedOutOfLogicItemTooManyMissing, someOutOfLogicItem.Metadata.Name, someOutOfLogicLocation.Metadata.Name); - } + Say(x => x.TrackedOutOfLogicItemTooManyMissing, someOutOfLogicItem.Metadata.Name, someOutOfLogicLocation.Metadata.Name); } } } - else - { - Say(x => x.ClearedMultipleItems, itemsCleared, area.Name); - } + } + else + { + Say(x => x.ClearedMultipleItems, itemsCleared, area.Name); + } - if (treasureTracked > 0) + if (treasureTracked > 0) + { + var dungeon = GetDungeonFromArea(area); + if (dungeon != null) { - var dungeon = GetDungeonFromArea(area); - if (dungeon != null) - { - TrackDungeonTreasure(dungeon, amount: treasureTracked); - } + TrackDungeonTreasure(dungeon, amount: treasureTracked); } } - - OnItemTracked(new ItemTrackedEventArgs(null, null, confidence, false)); } - IsDirty = true; + OnItemTracked(new ItemTrackedEventArgs(null, null, confidence, false)); + } + + IsDirty = true; - AddUndo(() => + AddUndo(() => + { + foreach (var location in locations) { - foreach (var location in locations) + if (trackItems) { - if (trackItems) - { - var item = location.Item; - if (item.Type != ItemType.Nothing && item.State.TrackingState > 0) - item.State.TrackingState--; - } - - location.State.Cleared = false; + var item = location.Item; + if (item.Type != ItemType.Nothing && item.State.TrackingState > 0) + item.State.TrackingState--; } - ItemService.ResetProgression(); - }); + + location.State.Cleared = false; + } + ItemService.ResetProgression(); + }); + } + + /// + /// Marks all locations and treasure within a dungeon as cleared. + /// + /// The dungeon to clear. + /// The speech recognition confidence. + public void ClearDungeon(IDungeon dungeon, float? confidence = null) + { + var remaining = dungeon.DungeonState.RemainingTreasure; + if (remaining > 0) + { + dungeon.DungeonState.RemainingTreasure = 0; } - /// - /// Marks all locations and treasure within a dungeon as cleared. - /// - /// The dungeon to clear. - /// The speech recognition confidence. - public void ClearDungeon(IDungeon dungeon, float? confidence = null) + // Clear the dungeon only if there's no bosses to defeat + if (!dungeon.DungeonState.HasReward) + dungeon.DungeonState.Cleared = true; + + var region = (Region)dungeon; + var progress = ItemService.GetProgression(assumeKeys: !World.Config.ZeldaKeysanity); + var locations = region.Locations.Where(x => x.State.Cleared == false).ToList(); + var inaccessibleLocations = locations.Where(x => !x.IsAvailable(progress)).ToList(); + if (locations.Count > 0) { - var remaining = dungeon.DungeonState.RemainingTreasure; - if (remaining > 0) + foreach (var state in locations.Select(x => x.State).NonNull()) { - dungeon.DungeonState.RemainingTreasure = 0; + state.Cleared = true; } + } - // Clear the dungeon only if there's no bosses to defeat - if (!dungeon.DungeonState.HasReward) - dungeon.DungeonState.Cleared = true; + if (remaining <= 0 && locations.Count <= 0) + { + // We didn't do anything + Say(x => x.DungeonAlreadyCleared, dungeon.DungeonMetadata.Name); + return; + } - var region = (Region)dungeon; - var progress = ItemService.GetProgression(assumeKeys: !World.Config.ZeldaKeysanity); - var locations = region.Locations.Where(x => x.State.Cleared == false).ToList(); - var inaccessibleLocations = locations.Where(x => !x.IsAvailable(progress)).ToList(); - if (locations.Count > 0) + Say(x => x.DungeonCleared, dungeon.DungeonMetadata.Name); + if (inaccessibleLocations.Count > 0 && confidence >= Options.MinimumSassConfidence) + { + var anyMissedLocation = inaccessibleLocations.Random(s_random) ?? inaccessibleLocations.First(); + var locationInfo = anyMissedLocation.Metadata; + var missingItemCombinations = Logic.GetMissingRequiredItems(anyMissedLocation, progress, out _); + if (missingItemCombinations.Any()) { - foreach (var state in locations.Select(x => x.State).NonNull()) - { - state.Cleared = true; - } + var missingItems = (missingItemCombinations.Random(s_random) ?? missingItemCombinations.First()) + .Select(ItemService.FirstOrDefault) + .NonNull(); + var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); + Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.DungeonMetadata.Name, locationInfo.Name, missingItemsText); } - - if (remaining <= 0 && locations.Count <= 0) + else { - // We didn't do anything - Say(x => x.DungeonAlreadyCleared, dungeon.DungeonMetadata.Name); - return; + Say(x => x.DungeonClearedWithTooManyInaccessibleItems, dungeon.DungeonMetadata.Name, locationInfo.Name); } + } + ItemService.ResetProgression(); - Say(x => x.DungeonCleared, dungeon.DungeonMetadata.Name); - if (inaccessibleLocations.Count > 0 && confidence >= Options.MinimumSassConfidence) + OnDungeonUpdated(new(dungeon, confidence, false)); + AddUndo(() => + { + dungeon.DungeonState.RemainingTreasure = remaining; + if (remaining > 0 && !dungeon.DungeonState.HasReward) + dungeon.DungeonState.Cleared = false; + foreach (var state in locations.Select(x => x.State).NonNull()) { - var anyMissedLocation = inaccessibleLocations.Random(s_random) ?? inaccessibleLocations.First(); - var locationInfo = anyMissedLocation.Metadata; - var missingItemCombinations = Logic.GetMissingRequiredItems(anyMissedLocation, progress, out _); - if (missingItemCombinations.Any()) - { - var missingItems = (missingItemCombinations.Random(s_random) ?? missingItemCombinations.First()) - .Select(ItemService.FirstOrDefault) - .NonNull(); - var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); - Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.DungeonMetadata.Name, locationInfo.Name, missingItemsText); - } - else - { - Say(x => x.DungeonClearedWithTooManyInaccessibleItems, dungeon.DungeonMetadata.Name, locationInfo.Name); - } + state.Cleared = false; } ItemService.ResetProgression(); + }); + } - OnDungeonUpdated(new(dungeon, confidence, false)); - AddUndo(() => - { - dungeon.DungeonState.RemainingTreasure = remaining; - if (remaining > 0 && !dungeon.DungeonState.HasReward) - dungeon.DungeonState.Cleared = false; - foreach (var state in locations.Select(x => x.State).NonNull()) - { - state.Cleared = false; - } - ItemService.ResetProgression(); - }); - } + /// + /// Clears an item from the specified location. + /// + /// The location to clear. + /// The speech recognition confidence. + /// If this was tracked by the auto tracker + public void Clear(Location location, float? confidence = null, bool autoTracked = false) + { + ItemService.ResetProgression(); + location.State.Cleared = true; - /// - /// Clears an item from the specified location. - /// - /// The location to clear. - /// The speech recognition confidence. - /// If this was tracked by the auto tracker - public void Clear(Location location, float? confidence = null, bool autoTracked = false) + if (confidence != null) { - ItemService.ResetProgression(); - location.State.Cleared = true; + // Only use TTS if called from a voice command + var locationName = location.Metadata.Name; + Say(Responses.LocationCleared.Format(locationName)); + } - if (confidence != null) - { - // Only use TTS if called from a voice command - var locationName = location.Metadata.Name; - Say(Responses.LocationCleared.Format(locationName)); - } + if (location.State.HasMarkedItem) + { + location.State.MarkedItem = null; + OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); + } - if (location.State.HasMarkedItem) - { - location.State.MarkedItem = null; - OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); - } + var undoTrackTreasure = TryTrackDungeonTreasure(location, confidence); - var undoTrackTreasure = TryTrackDungeonTreasure(location, confidence); + Action? undoStopPegWorldMode = null; + if (location == World.DarkWorldNorthWest.PegWorld) + { + StopPegWorldMode(); - Action? undoStopPegWorldMode = null; - if (location == World.DarkWorldNorthWest.PegWorld) + if (!autoTracked) { - StopPegWorldMode(); - - if (!autoTracked) - { - undoStopPegWorldMode = _undoHistory.Pop().Action; - } + undoStopPegWorldMode = _undoHistory.Pop().Action; } + } - IsDirty = true; + IsDirty = true; - if (!autoTracked) + if (!autoTracked) + { + AddUndo(() => { - AddUndo(() => - { - location.State.Cleared = false; - undoTrackTreasure?.Invoke(); - undoStopPegWorldMode?.Invoke(); - ItemService.ResetProgression(); - }); - } - - World.LastClearedLocation = location; - OnLocationCleared(new(location, confidence, autoTracked)); + location.State.Cleared = false; + undoTrackTreasure?.Invoke(); + undoStopPegWorldMode?.Invoke(); + ItemService.ResetProgression(); + }); } - /// - /// Marks a dungeon as cleared and, if possible, tracks the boss reward. - /// - /// The dungeon that was cleared. - /// The speech recognition confidence. - /// If this was cleared by the auto tracker - public void MarkDungeonAsCleared(IDungeon dungeon, float? confidence = null, bool autoTracked = false) + World.LastClearedLocation = location; + OnLocationCleared(new(location, confidence, autoTracked)); + } + + /// + /// Marks a dungeon as cleared and, if possible, tracks the boss reward. + /// + /// The dungeon that was cleared. + /// The speech recognition confidence. + /// If this was cleared by the auto tracker + public void MarkDungeonAsCleared(IDungeon dungeon, float? confidence = null, bool autoTracked = false) + { + if (dungeon.DungeonState.Cleared) { - if (dungeon.DungeonState.Cleared) - { - if (!autoTracked) - Say(Responses.DungeonBossAlreadyCleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); - else - OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); + if (!autoTracked) + Say(Responses.DungeonBossAlreadyCleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); + else + OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); - return; - } + return; + } - ItemService.ResetProgression(); + ItemService.ResetProgression(); - var addedEvent = History.AddEvent( - HistoryEventType.BeatBoss, - true, - dungeon.DungeonMetadata.Boss.ToString() ?? $"boss of {dungeon.DungeonMetadata.Name}" - ); + var addedEvent = History.AddEvent( + HistoryEventType.BeatBoss, + true, + dungeon.DungeonMetadata.Boss.ToString() ?? $"boss of {dungeon.DungeonMetadata.Name}" + ); - // If all treasures have been retrieved and the boss is defeated, clear all locations in the dungeon - var clearedLocations = new List(); - if (dungeon.DungeonState.RemainingTreasure == 0) + // If all treasures have been retrieved and the boss is defeated, clear all locations in the dungeon + var clearedLocations = new List(); + if (dungeon.DungeonState.RemainingTreasure == 0) + { + foreach (var location in ((Region)dungeon).Locations.Where(x => !x.State.Cleared)) { - foreach (var location in ((Region)dungeon).Locations.Where(x => !x.State.Cleared)) + location.State.Cleared = true; + if (autoTracked) { - location.State.Cleared = true; - if (autoTracked) - { - location.State.Autotracked = true; - } - clearedLocations.Add(location); + location.State.Autotracked = true; } + clearedLocations.Add(location); } + } - // Auto track the dungeon reward if not already marked - if (autoTracked && dungeon.DungeonState.MarkedReward != dungeon.DungeonState.Reward) - { - dungeon.DungeonState.MarkedReward = dungeon.DungeonState.Reward; - SetDungeonReward(dungeon, dungeon.DungeonState.Reward); - } + // Auto track the dungeon reward if not already marked + if (autoTracked && dungeon.DungeonState.MarkedReward != dungeon.DungeonState.Reward) + { + dungeon.DungeonState.MarkedReward = dungeon.DungeonState.Reward; + SetDungeonReward(dungeon, dungeon.DungeonState.Reward); + } - dungeon.DungeonState.Cleared = true; - Say(Responses.DungeonBossCleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); - IsDirty = true; - RestartIdleTimers(); - OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); + dungeon.DungeonState.Cleared = true; + Say(Responses.DungeonBossCleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); + IsDirty = true; + RestartIdleTimers(); + OnDungeonUpdated(new DungeonTrackedEventArgs(dungeon, confidence, autoTracked)); - if (!autoTracked) + if (!autoTracked) + { + AddUndo(() => { - AddUndo(() => + ItemService.ResetProgression(); + dungeon.DungeonState.Cleared = false; + addedEvent.IsUndone = true; + foreach (var location in clearedLocations) { - ItemService.ResetProgression(); - dungeon.DungeonState.Cleared = false; - addedEvent.IsUndone = true; - foreach (var location in clearedLocations) - { - location.State.Cleared = false; - } - }); - } + location.State.Cleared = false; + } + }); } + } - /// - /// Marks a boss as defeated. - /// - /// The boss that was defeated. - /// - /// if the command implies the boss was killed; - /// if the boss was simply "tracked". - /// - /// The speech recognition confidence. - /// If this was tracked by the auto tracker - public void MarkBossAsDefeated(Boss boss, bool admittedGuilt = true, float? confidence = null, bool autoTracked = false) + /// + /// Marks a boss as defeated. + /// + /// The boss that was defeated. + /// + /// if the command implies the boss was killed; + /// if the boss was simply "tracked". + /// + /// The speech recognition confidence. + /// If this was tracked by the auto tracker + public void MarkBossAsDefeated(Boss boss, bool admittedGuilt = true, float? confidence = null, bool autoTracked = false) + { + if (boss.State.Defeated) { - if (boss.State.Defeated) - { - if (!autoTracked) - Say(x => x.BossAlreadyDefeated, boss.Name); - else - OnBossUpdated(new(boss, confidence, autoTracked)); - return; - } + if (!autoTracked) + Say(x => x.BossAlreadyDefeated, boss.Name); + else + OnBossUpdated(new(boss, confidence, autoTracked)); + return; + } - boss.State.Defeated = true; + boss.State.Defeated = true; - if (!admittedGuilt && boss.Metadata.WhenTracked != null) - Say(boss.Metadata.WhenTracked, boss.Name); - else - Say(boss.Metadata.WhenDefeated ?? Responses.BossDefeated, boss.Name); + if (!admittedGuilt && boss.Metadata.WhenTracked != null) + Say(boss.Metadata.WhenTracked, boss.Name); + else + Say(boss.Metadata.WhenDefeated ?? Responses.BossDefeated, boss.Name); - var addedEvent = History.AddEvent( - HistoryEventType.BeatBoss, - true, - boss.Name - ); + var addedEvent = History.AddEvent( + HistoryEventType.BeatBoss, + true, + boss.Name + ); - IsDirty = true; - ItemService.ResetProgression(); + IsDirty = true; + ItemService.ResetProgression(); - RestartIdleTimers(); - OnBossUpdated(new(boss, confidence, autoTracked)); + RestartIdleTimers(); + OnBossUpdated(new(boss, confidence, autoTracked)); - if (!autoTracked) + if (!autoTracked) + { + AddUndo(() => { - AddUndo(() => - { - boss.State.Defeated = false; - addedEvent.IsUndone = true; - }); - } + boss.State.Defeated = false; + addedEvent.IsUndone = true; + }); } + } - /// - /// Un-marks a boss as defeated. - /// - /// The boss that should be 'revived'. - /// The speech recognition confidence. - public void MarkBossAsNotDefeated(Boss boss, float? confidence = null) + /// + /// Un-marks a boss as defeated. + /// + /// The boss that should be 'revived'. + /// The speech recognition confidence. + public void MarkBossAsNotDefeated(Boss boss, float? confidence = null) + { + if (boss.State.Defeated != true) { - if (boss.State.Defeated != true) - { - Say(x => x.BossNotYetDefeated, boss.Name); - return; - } + Say(x => x.BossNotYetDefeated, boss.Name); + return; + } - boss.State.Defeated = false; - Say(Responses.BossUndefeated, boss.Name); + boss.State.Defeated = false; + Say(Responses.BossUndefeated, boss.Name); - IsDirty = true; - ItemService.ResetProgression(); + IsDirty = true; + ItemService.ResetProgression(); - OnBossUpdated(new(boss, confidence, false)); - AddUndo(() => boss.State.Defeated = true); - } + OnBossUpdated(new(boss, confidence, false)); + AddUndo(() => boss.State.Defeated = true); + } - /// - /// Un-marks a dungeon as cleared and, if possible, untracks the boss - /// reward. - /// - /// The dungeon that should be un-cleared. - /// The speech recognition confidence. - public void MarkDungeonAsIncomplete(IDungeon dungeon, float? confidence = null) + /// + /// Un-marks a dungeon as cleared and, if possible, untracks the boss + /// reward. + /// + /// The dungeon that should be un-cleared. + /// The speech recognition confidence. + public void MarkDungeonAsIncomplete(IDungeon dungeon, float? confidence = null) + { + if (!dungeon.DungeonState.Cleared) { - if (!dungeon.DungeonState.Cleared) - { - Say(Responses.DungeonBossNotYetCleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); - return; - } + Say(Responses.DungeonBossNotYetCleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); + return; + } - ItemService.ResetProgression(); - dungeon.DungeonState.Cleared = false; - Say(Responses.DungeonBossUncleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); + ItemService.ResetProgression(); + dungeon.DungeonState.Cleared = false; + Say(Responses.DungeonBossUncleared.Format(dungeon.DungeonMetadata.Name, dungeon.DungeonMetadata.Boss)); - // Try to untrack the associated boss reward item - Action? undoUnclear = null; - Action? undoUntrackTreasure = null; - Action? undoUntrack = null; - if (dungeon.DungeonMetadata.LocationId != null) + // Try to untrack the associated boss reward item + Action? undoUnclear = null; + Action? undoUntrackTreasure = null; + Action? undoUntrack = null; + if (dungeon.DungeonMetadata.LocationId != null) + { + var rewardLocation = _worldService.Location(dungeon.DungeonMetadata.LocationId.Value); + if (rewardLocation.Item.Type != ItemType.Nothing) { - var rewardLocation = _worldService.Location(dungeon.DungeonMetadata.LocationId.Value); - if (rewardLocation.Item.Type != ItemType.Nothing) + var item = rewardLocation.Item; + if (item.Type != ItemType.Nothing && item.State.TrackingState > 0) { - var item = rewardLocation.Item; - if (item.Type != ItemType.Nothing && item.State.TrackingState > 0) - { - UntrackItem(item); - undoUntrack = _undoHistory.Pop().Action; - } - - if (!rewardLocation.Item.IsDungeonItem) - { - dungeon.DungeonState.RemainingTreasure++; - undoUntrackTreasure = () => dungeon.DungeonState.RemainingTreasure--; - } + UntrackItem(item); + undoUntrack = _undoHistory.Pop().Action; } - if (rewardLocation.State.Cleared) + if (!rewardLocation.Item.IsDungeonItem) { - rewardLocation.State.Cleared = false; - OnLocationCleared(new(rewardLocation, null, false)); - undoUnclear = () => rewardLocation.State.Cleared = true; + dungeon.DungeonState.RemainingTreasure++; + undoUntrackTreasure = () => dungeon.DungeonState.RemainingTreasure--; } } - IsDirty = true; - - OnDungeonUpdated(new(dungeon, confidence, false)); - AddUndo(() => + if (rewardLocation.State.Cleared) { - dungeon.DungeonState.Cleared = false; - undoUntrack?.Invoke(); - undoUntrackTreasure?.Invoke(); - undoUnclear?.Invoke(); - ItemService.ResetProgression(); - }); + rewardLocation.State.Cleared = false; + OnLocationCleared(new(rewardLocation, null, false)); + undoUnclear = () => rewardLocation.State.Cleared = true; + } } - /// - /// Marks an item at the specified location. - /// - /// The location to mark. - /// - /// The item that is found at . - /// - /// The speech recognition confidence. - public void MarkLocation(Location location, Item item, float? confidence = null) - { - var locationName = location.Metadata.Name; - GiveLocationComment(item, location, isTracking: false, confidence); + IsDirty = true; - if (item.Type == ItemType.Nothing) - { - Clear(location); - Say(Responses.LocationMarkedAsBullshit.Format(locationName)); - } - else if (location.State.MarkedItem != null) - { - var oldType = location.State.MarkedItem; - location.State.MarkedItem = item.Type; - Say(Responses.LocationMarkedAgain.Format(locationName, item.Name, oldType.GetDescription())); - AddUndo(() => location.State.MarkedItem = oldType); - } - else - { - location.State.MarkedItem = item.Type; - Say(Responses.LocationMarked.Format(locationName, item.Name)); - AddUndo(() => location.State.MarkedItem = null); - } + OnDungeonUpdated(new(dungeon, confidence, false)); + AddUndo(() => + { + dungeon.DungeonState.Cleared = false; + undoUntrack?.Invoke(); + undoUntrackTreasure?.Invoke(); + undoUnclear?.Invoke(); + ItemService.ResetProgression(); + }); + } - IsDirty = true; + /// + /// Marks an item at the specified location. + /// + /// The location to mark. + /// + /// The item that is found at . + /// + /// The speech recognition confidence. + public void MarkLocation(Location location, Item item, float? confidence = null) + { + var locationName = location.Metadata.Name; + GiveLocationComment(item, location, isTracking: false, confidence); - OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); + if (item.Type == ItemType.Nothing) + { + Clear(location); + Say(Responses.LocationMarkedAsBullshit.Format(locationName)); } - - /// - /// Pegs a Peg World peg. - /// - /// The speech recognition confidence. - public void Peg(float? confidence = null) + else if (location.State.MarkedItem != null) { - if (!PegWorldMode) - return; + var oldType = location.State.MarkedItem; + location.State.MarkedItem = item.Type; + Say(Responses.LocationMarkedAgain.Format(locationName, item.Name, oldType.GetDescription())); + AddUndo(() => location.State.MarkedItem = oldType); + } + else + { + location.State.MarkedItem = item.Type; + Say(Responses.LocationMarked.Format(locationName, item.Name)); + AddUndo(() => location.State.MarkedItem = null); + } - PegsPegged++; + IsDirty = true; - if (PegsPegged < PegWorldModeModule.TotalPegs) - Say(Responses.PegWorldModePegged); - else - Say(Responses.PegWorldModeDone); - OnPegPegged(new TrackerEventArgs(confidence)); - AddUndo(() => PegsPegged--); + OnMarkedLocationsUpdated(new TrackerEventArgs(confidence)); + } - RestartIdleTimers(); - } + /// + /// Pegs a Peg World peg. + /// + /// The speech recognition confidence. + public void Peg(float? confidence = null) + { + if (!PegWorldMode) + return; - /// - /// Starts Peg World mode. - /// - /// The speech recognition confidence. - public void StartPegWorldMode(float? confidence = null) - { - ShutUp(); - PegWorldMode = true; - Say(Responses.PegWorldModeOn, wait: true); - OnPegWorldModeToggled(new TrackerEventArgs(confidence)); - AddUndo(() => PegWorldMode = false); - } + PegsPegged++; - /// - /// Turns Peg World mode off. - /// - /// The speech recognition confidence. - public void StopPegWorldMode(float? confidence = null) - { - PegWorldMode = false; + if (PegsPegged < PegWorldModeModule.TotalPegs) + Say(Responses.PegWorldModePegged); + else Say(Responses.PegWorldModeDone); - OnPegWorldModeToggled(new TrackerEventArgs(confidence)); - AddUndo(() => PegWorldMode = true); - } + OnPegPegged(new TrackerEventArgs(confidence)); + AddUndo(() => PegsPegged--); - /// - /// Starts Peg World mode. - /// - /// The speech recognition confidence. - public void StartShaktoolMode(float? confidence = null) - { - ShaktoolMode = true; - OnShaktoolModeToggled(new TrackerEventArgs(confidence)); - AddUndo(() => ShaktoolMode = false); - } + RestartIdleTimers(); + } - /// - /// Turns Peg World mode off. - /// - /// The speech recognition confidence. - public void StopShaktoolMode(float? confidence = null) - { - ShaktoolMode = false; - OnShaktoolModeToggled(new TrackerEventArgs(confidence)); - AddUndo(() => ShaktoolMode = true); - } + /// + /// Starts Peg World mode. + /// + /// The speech recognition confidence. + public void StartPegWorldMode(float? confidence = null) + { + ShutUp(); + PegWorldMode = true; + Say(Responses.PegWorldModeOn, wait: true); + OnPegWorldModeToggled(new TrackerEventArgs(confidence)); + AddUndo(() => PegWorldMode = false); + } - /// - /// Updates the region that the player is in - /// - /// The region the player is in - /// Set to true to update the map for the player to match the region - /// If the time should be reset if this is the first region update - public void UpdateRegion(Region region, bool updateMap = false, bool resetTime = false) - { - UpdateRegion(region.Metadata, updateMap, resetTime); - } + /// + /// Turns Peg World mode off. + /// + /// The speech recognition confidence. + public void StopPegWorldMode(float? confidence = null) + { + PegWorldMode = false; + Say(Responses.PegWorldModeDone); + OnPegWorldModeToggled(new TrackerEventArgs(confidence)); + AddUndo(() => PegWorldMode = true); + } - /// - /// Updates the region that the player is in - /// - /// The region the player is in - /// Set to true to update the map for the player to match the region - /// If the time should be reset if this is the first region update - public void UpdateRegion(RegionInfo? region, bool updateMap = false, bool resetTime = false) - { - if (region != CurrentRegion) - { - if (resetTime && History.GetHistory().Count == 0) - { - ResetTimer(true); - } + /// + /// Starts Peg World mode. + /// + /// The speech recognition confidence. + public void StartShaktoolMode(float? confidence = null) + { + ShaktoolMode = true; + OnShaktoolModeToggled(new TrackerEventArgs(confidence)); + AddUndo(() => ShaktoolMode = false); + } - History.AddEvent( - HistoryEventType.EnteredRegion, - true, - region?.Name.ToString() ?? "new region" - ); - } + /// + /// Turns Peg World mode off. + /// + /// The speech recognition confidence. + public void StopShaktoolMode(float? confidence = null) + { + ShaktoolMode = false; + OnShaktoolModeToggled(new TrackerEventArgs(confidence)); + AddUndo(() => ShaktoolMode = true); + } + + /// + /// Updates the region that the player is in + /// + /// The region the player is in + /// Set to true to update the map for the player to match the region + /// If the time should be reset if this is the first region update + public void UpdateRegion(Region region, bool updateMap = false, bool resetTime = false) + { + UpdateRegion(region.Metadata, updateMap, resetTime); + } - CurrentRegion = region; - if (updateMap && region != null) + /// + /// Updates the region that the player is in + /// + /// The region the player is in + /// Set to true to update the map for the player to match the region + /// If the time should be reset if this is the first region update + public void UpdateRegion(RegionInfo? region, bool updateMap = false, bool resetTime = false) + { + if (region != CurrentRegion) + { + if (resetTime && History.GetHistory().Count == 0) { - UpdateMap(region.MapName); + ResetTimer(true); } + + History.AddEvent( + HistoryEventType.EnteredRegion, + true, + region?.Name.ToString() ?? "new region" + ); } - /// - /// Updates the map to display for the user - /// - /// The name of the map - public void UpdateMap(string map) + CurrentRegion = region; + if (updateMap && region != null) { - CurrentMap = map; - MapUpdated?.Invoke(this, EventArgs.Empty); + UpdateMap(region.MapName); } + } + + /// + /// Updates the map to display for the user + /// + /// The name of the map + public void UpdateMap(string map) + { + CurrentMap = map; + MapUpdated?.Invoke(this, EventArgs.Empty); + } - /// - /// Called when the game is beaten by entering triforce room - /// or entering the ship after beating both bosses - /// - /// If this was triggered by the auto tracker - public void GameBeaten(bool autoTracked) + /// + /// Called when the game is beaten by entering triforce room + /// or entering the ship after beating both bosses + /// + /// If this was triggered by the auto tracker + public void GameBeaten(bool autoTracked) + { + if (!_beatenGame) { - if (!_beatenGame) + _beatenGame = true; + var pauseUndo = PauseTimer(false); + Say(x => x.BeatGame); + BeatGame?.Invoke(this, new TrackerEventArgs(autoTracked)); + if (!autoTracked) { - _beatenGame = true; - var pauseUndo = PauseTimer(false); - Say(x => x.BeatGame); - BeatGame?.Invoke(this, new TrackerEventArgs(autoTracked)); - if (!autoTracked) + AddUndo(() => { - AddUndo(() => + _beatenGame = false; + if (pauseUndo != null) { - _beatenGame = false; - if (pauseUndo != null) - { - pauseUndo(); - } - }); - } + pauseUndo(); + } + }); } } + } - /// - /// Called when the player has died - /// - public void TrackDeath(bool autoTracked) - { - PlayerDied?.Invoke(this, new TrackerEventArgs(autoTracked)); - } + /// + /// Called when the player has died + /// + public void TrackDeath(bool autoTracked) + { + PlayerDied?.Invoke(this, new TrackerEventArgs(autoTracked)); + } - /// - /// Updates the current track number being played - /// - /// The number of the track - public void UpdateTrackNumber(int number) - { - if (number <= 0 || number > 200 || number == CurrentTrackNumber) return; - CurrentTrackNumber = number; - TrackNumberUpdated?.Invoke(this, new TrackNumberEventArgs(number)); - } + /// + /// Updates the current track number being played + /// + /// The number of the track + public void UpdateTrackNumber(int number) + { + if (number <= 0 || number > 200 || number == CurrentTrackNumber) return; + CurrentTrackNumber = number; + TrackNumberUpdated?.Invoke(this, new TrackNumberEventArgs(number)); + } - /// - /// Updates the current track being played - /// - /// The current MSU pack - /// The current track - /// Formatted output text matching the requested style - public void UpdateTrack(Msu msu, Track track, string outputText) - { - TrackChanged?.Invoke(this, new TrackChangedEventArgs(msu, track, outputText)); - } + /// + /// Updates the current track being played + /// + /// The current MSU pack + /// The current track + /// Formatted output text matching the requested style + public void UpdateTrack(Msu msu, Track track, string outputText) + { + TrackChanged?.Invoke(this, new TrackChangedEventArgs(msu, track, outputText)); + } - internal void RestartIdleTimers() + public void RestartIdleTimers() + { + foreach (var item in _idleTimers) { - foreach (var item in _idleTimers) - { - var timeout = Parse.AsTimeSpan(item.Key, s_random) ?? Timeout.InfiniteTimeSpan; - var timer = item.Value; + var timeout = Parse.AsTimeSpan(item.Key, s_random) ?? Timeout.InfiniteTimeSpan; + var timer = item.Value; - timer.Change(timeout, Timeout.InfiniteTimeSpan); - } + timer.Change(timeout, Timeout.InfiniteTimeSpan); } + } - /// - /// Determines whether or not the specified reward is worth getting. - /// - /// The dungeon reward. - /// - /// if the reward leads to something good; - /// otherwise, . - /// - protected internal bool IsWorth(RewardType reward) + /// + /// Determines whether or not the specified reward is worth getting. + /// + /// The dungeon reward. + /// + /// if the reward leads to something good; + /// otherwise, . + /// + public bool IsWorth(RewardType reward) + { + var sahasrahlaItem = World.FindLocation(LocationId.Sahasrahla).Item; + if (sahasrahlaItem.Type != ItemType.Nothing && reward == RewardType.PendantGreen) { - var sahasrahlaItem = World.FindLocation(LocationId.Sahasrahla).Item; - if (sahasrahlaItem.Type != ItemType.Nothing && reward == RewardType.PendantGreen) + _logger.LogDebug("{Reward} leads to {Item}...", reward, sahasrahlaItem); + if (IsWorth(sahasrahlaItem)) { - _logger.LogDebug("{Reward} leads to {Item}...", reward, sahasrahlaItem); - if (IsWorth(sahasrahlaItem)) - { - _logger.LogDebug("{Reward} leads to {Item}, which is worth it", reward, sahasrahlaItem); - return true; - } - _logger.LogDebug("{Reward} leads to {Item}, which is junk", reward, sahasrahlaItem); + _logger.LogDebug("{Reward} leads to {Item}, which is worth it", reward, sahasrahlaItem); + return true; } + _logger.LogDebug("{Reward} leads to {Item}, which is junk", reward, sahasrahlaItem); + } - var pedItem = World.LightWorldNorthWest.MasterSwordPedestal.Item; - if (pedItem.Type != ItemType.Nothing && (reward is RewardType.PendantGreen or RewardType.PendantRed or RewardType.PendantBlue)) + var pedItem = World.LightWorldNorthWest.MasterSwordPedestal.Item; + if (pedItem.Type != ItemType.Nothing && (reward is RewardType.PendantGreen or RewardType.PendantRed or RewardType.PendantBlue)) + { + _logger.LogDebug("{Reward} leads to {Item}...", reward, pedItem); + if (IsWorth(pedItem)) { - _logger.LogDebug("{Reward} leads to {Item}...", reward, pedItem); - if (IsWorth(pedItem)) - { - _logger.LogDebug("{Reward} leads to {Item}, which is worth it", reward, pedItem); - return true; - } - _logger.LogDebug("{Reward} leads to {Item}, which is junk", reward, pedItem); + _logger.LogDebug("{Reward} leads to {Item}, which is worth it", reward, pedItem); + return true; } - - return false; + _logger.LogDebug("{Reward} leads to {Item}, which is junk", reward, pedItem); } - /// - /// Determines whether or not the specified item is worth getting. - /// - /// The item whose worth to consider. - /// - /// is the item is worth getting or leads to - /// another item that is worth getting; otherwise, . - /// - protected internal bool IsWorth(Item item) - { - var leads = new Dictionary() - { - [ItemType.Mushroom] = new[] { World.LightWorldNorthEast.MushroomItem }, - [ItemType.Powder] = new[] { World.LightWorldNorthWest.MagicBat }, - [ItemType.Book] = new[] - { - World.LightWorldDeathMountainWest.EtherTablet, - World.LightWorldSouth.BombosTablet - }, - [ItemType.Bottle] = new[] { World.LightWorldNorthWest.SickKid }, - [ItemType.BottleWithBee] = new[] { World.LightWorldNorthWest.SickKid }, - [ItemType.BottleWithFairy] = new[] { World.LightWorldNorthWest.SickKid }, - [ItemType.BottleWithBluePotion] = new[] { World.LightWorldNorthWest.SickKid }, - [ItemType.BottleWithGoldBee] = new[] { World.LightWorldNorthWest.SickKid }, - [ItemType.BottleWithGreenPotion] = new[] { World.LightWorldNorthWest.SickKid }, - [ItemType.BottleWithRedPotion] = new[] { World.LightWorldNorthWest.SickKid }, - }; - - if (leads.TryGetValue(item.Type, out var leadsToLocation) && !ItemService.IsTracked(item.Type)) - { - foreach (var location in leadsToLocation) + return false; + } + + /// + /// Determines whether or not the specified item is worth getting. + /// + /// The item whose worth to consider. + /// + /// is the item is worth getting or leads to + /// another item that is worth getting; otherwise, . + /// + public bool IsWorth(Item item) + { + var leads = new Dictionary() + { + [ItemType.Mushroom] = new[] { World.LightWorldNorthEast.MushroomItem }, + [ItemType.Powder] = new[] { World.LightWorldNorthWest.MagicBat }, + [ItemType.Book] = new[] + { + World.LightWorldDeathMountainWest.EtherTablet, + World.LightWorldSouth.BombosTablet + }, + [ItemType.Bottle] = new[] { World.LightWorldNorthWest.SickKid }, + [ItemType.BottleWithBee] = new[] { World.LightWorldNorthWest.SickKid }, + [ItemType.BottleWithFairy] = new[] { World.LightWorldNorthWest.SickKid }, + [ItemType.BottleWithBluePotion] = new[] { World.LightWorldNorthWest.SickKid }, + [ItemType.BottleWithGoldBee] = new[] { World.LightWorldNorthWest.SickKid }, + [ItemType.BottleWithGreenPotion] = new[] { World.LightWorldNorthWest.SickKid }, + [ItemType.BottleWithRedPotion] = new[] { World.LightWorldNorthWest.SickKid }, + }; + + if (leads.TryGetValue(item.Type, out var leadsToLocation) && !ItemService.IsTracked(item.Type)) + { + foreach (var location in leadsToLocation) + { + var reward = location.Item; + if (reward.Type != ItemType.Nothing) { - var reward = location.Item; - if (reward.Type != ItemType.Nothing) + _logger.LogDebug("{Item} leads to {OtherItem}...", item, reward); + if (IsWorth(reward)) { - _logger.LogDebug("{Item} leads to {OtherItem}...", item, reward); - if (IsWorth(reward)) - { - _logger.LogDebug("{Item} leads to {OtherItem}, which is worth it", item, reward); - return true; - } - _logger.LogDebug("{Item} leads to {OtherItem}, which is junk", item, reward); + _logger.LogDebug("{Item} leads to {OtherItem}, which is worth it", item, reward); + return true; } + _logger.LogDebug("{Item} leads to {OtherItem}, which is junk", item, reward); } } - - return item.Metadata.IsGood(World.Config); } - /// - /// Adds an action to be invoked to undo the last operation. - /// - /// - /// The action to invoke to undo the last operation. - /// - protected internal virtual void AddUndo(Action undo) => _undoHistory.Push((undo, DateTime.Now)); + return item.Metadata.IsGood(World.Config); + } + + /// + /// Adds an action to be invoked to undo the last operation. + /// + /// + /// The action to invoke to undo the last operation. + /// + public virtual void AddUndo(Action undo) => _undoHistory.Push((undo, DateTime.Now)); - /// - /// Cleans up resources used by this class. - /// - /// - /// true to dispose of managed resources. - /// - protected virtual void Dispose(bool disposing) + /// + /// Cleans up resources used by this class. + /// + /// + /// true to dispose of managed resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) { - if (!_disposed) + if (disposing) { - if (disposing) - { - _recognizer.Dispose(); - (_communicator as IDisposable)?.Dispose(); - - foreach (var timer in _idleTimers.Values) - timer.Dispose(); - } + _recognizer.Dispose(); + (_communicator as IDisposable)?.Dispose(); - _disposed = true; + foreach (var timer in _idleTimers.Values) + timer.Dispose(); } + + _disposed = true; } + } - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnItemTracked(ItemTrackedEventArgs e) - => ItemTracked?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnPegWorldModeToggled(TrackerEventArgs e) - => ToggledPegWorldModeOn?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnPegPegged(TrackerEventArgs e) - => PegPegged?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnShaktoolModeToggled(TrackerEventArgs e) - => ToggledShaktoolMode?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnDungeonUpdated(DungeonTrackedEventArgs e) - => DungeonUpdated?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnBossUpdated(BossTrackedEventArgs e) - => BossUpdated?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnMarkedLocationsUpdated(TrackerEventArgs e) - => MarkedLocationsUpdated?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnGoModeToggledOn(TrackerEventArgs e) - => GoModeToggledOn?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnLocationCleared(LocationClearedEventArgs e) - => LocationCleared?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnActionUndone(TrackerEventArgs e) - => ActionUndone?.Invoke(this, e); - - /// - /// Raises the event. - /// - protected virtual void OnStateLoaded() - => StateLoaded?.Invoke(this, EventArgs.Empty); - - /// - /// Raises the event. - /// - /// Event data. - protected virtual void OnSpeechRecognized(TrackerEventArgs e) - => SpeechRecognized?.Invoke(this, e); - - private static bool IsTreasure(Item? item) - => item is { IsDungeonItem: false }; - - private IDungeon? GetDungeonFromLocation(Location location) - { - if (location.Type == LocationType.NotInDungeon) - return null; - - return location.Region as IDungeon; - } - - private IDungeon? GetDungeonFromArea(IHasLocations area) - { - return area switch - { - Room room => room.Region as IDungeon, - Region region => region as IDungeon, - _ => null - }; + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnItemTracked(ItemTrackedEventArgs e) + => ItemTracked?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnPegWorldModeToggled(TrackerEventArgs e) + => ToggledPegWorldModeOn?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnPegPegged(TrackerEventArgs e) + => PegPegged?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnShaktoolModeToggled(TrackerEventArgs e) + => ToggledShaktoolMode?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnDungeonUpdated(DungeonTrackedEventArgs e) + => DungeonUpdated?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnBossUpdated(BossTrackedEventArgs e) + => BossUpdated?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnMarkedLocationsUpdated(TrackerEventArgs e) + => MarkedLocationsUpdated?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnGoModeToggledOn(TrackerEventArgs e) + => GoModeToggledOn?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnLocationCleared(LocationClearedEventArgs e) + => LocationCleared?.Invoke(this, e); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnActionUndone(TrackerEventArgs e) + => ActionUndone?.Invoke(this, e); + + /// + /// Raises the event. + /// + protected virtual void OnStateLoaded() + => StateLoaded?.Invoke(this, EventArgs.Empty); + + /// + /// Raises the event. + /// + /// Event data. + protected virtual void OnSpeechRecognized(TrackerEventArgs e) + => SpeechRecognized?.Invoke(this, e); + + private static bool IsTreasure(Item? item) + => item is { IsDungeonItem: false }; + + private IDungeon? GetDungeonFromLocation(Location location) + { + if (location.Type == LocationType.NotInDungeon) + return null; + + return location.Region as IDungeon; + } + + private IDungeon? GetDungeonFromArea(IHasLocations area) + { + return area switch + { + Room room => room.Region as IDungeon, + Region region => region as IDungeon, + _ => null + }; + } + + private Action? TryTrackDungeonTreasure(Item item, float? confidence) + { + if (confidence < Options.MinimumSassConfidence) + { + // Tracker response could give away the location of an item if + // it is in a dungeon but tracker misheard. + return null; } - private Action? TryTrackDungeonTreasure(Item item, float? confidence) + var dungeon = GetDungeonFromItem(item); + if (dungeon != null && (IsTreasure(item) || World.Config.ZeldaKeysanity)) { - if (confidence < Options.MinimumSassConfidence) - { - // Tracker response could give away the location of an item if - // it is in a dungeon but tracker misheard. - return null; - } + if (TrackDungeonTreasure(dungeon, confidence)) + return _undoHistory.Pop().Action; + } - var dungeon = GetDungeonFromItem(item); - if (dungeon != null && (IsTreasure(item) || World.Config.ZeldaKeysanity)) - { - if (TrackDungeonTreasure(dungeon, confidence)) - return _undoHistory.Pop().Action; - } + IsDirty = true; - IsDirty = true; + return null; + } + private Action? TryTrackDungeonTreasure(Location location, float? confidence, bool autoTracked = false, bool stateResponse = true) + { + if (confidence < Options.MinimumSassConfidence) + { + // Tracker response could give away the location of an item if + // it is in a dungeon but tracker misheard. return null; } - private Action? TryTrackDungeonTreasure(Location location, float? confidence, bool autoTracked = false, bool stateResponse = true) + var dungeon = GetDungeonFromLocation(location); + if (dungeon != null && (IsTreasure(location.Item) || World.Config.ZeldaKeysanity)) { - if (confidence < Options.MinimumSassConfidence) - { - // Tracker response could give away the location of an item if - // it is in a dungeon but tracker misheard. - return null; - } + if (TrackDungeonTreasure(dungeon, confidence, 1, autoTracked, stateResponse)) + return _undoHistory.Pop().Action; + } - var dungeon = GetDungeonFromLocation(location); - if (dungeon != null && (IsTreasure(location.Item) || World.Config.ZeldaKeysanity)) - { - if (TrackDungeonTreasure(dungeon, confidence, 1, autoTracked, stateResponse)) - return _undoHistory.Pop().Action; - } + IsDirty = true; - IsDirty = true; + return null; + } - return null; + private void GiveLocationComment(Item item, Location location, bool isTracking, float? confidence) + { + // If the plando config specifies a specific line for this location, say it + if (World.Config.PlandoConfig?.TrackerLocationLines.ContainsKey(location.ToString()) == true) + { + Say(World.Config.PlandoConfig?.TrackerLocationLines[location.ToString()]); } + // Give some sass if the user tracks or marks the wrong item at a + // location unless the user is clearing a useless item like missiles + else if (location.Item.Type != ItemType.Nothing && item.Type != location.Item.Type && (item.Type != ItemType.Nothing || location.Item.Metadata.IsProgression(World.Config))) + { + if (confidence == null || confidence < Options.MinimumSassConfidence) + return; - private void GiveLocationComment(Item item, Location location, bool isTracking, float? confidence) + var actualItemName = ItemService.GetName(location.Item.Type); + if (HintsEnabled) actualItemName = "another item"; + + Say(Responses.LocationHasDifferentItem?.Format(item.Metadata.NameWithArticle, actualItemName)); + } + else { - // If the plando config specifies a specific line for this location, say it - if (World.Config.PlandoConfig?.TrackerLocationLines.ContainsKey(location.ToString()) == true) + if (item.Type == location.VanillaItem && item.Type != ItemType.Nothing) { - Say(World.Config.PlandoConfig?.TrackerLocationLines[location.ToString()]); + Say(x => x.TrackedVanillaItem); + return; } - // Give some sass if the user tracks or marks the wrong item at a - // location unless the user is clearing a useless item like missiles - else if (location.Item.Type != ItemType.Nothing && item.Type != location.Item.Type && (item.Type != ItemType.Nothing || location.Item.Metadata.IsProgression(World.Config))) - { - if (confidence == null || confidence < Options.MinimumSassConfidence) - return; - var actualItemName = ItemService.GetName(location.Item.Type); - if (HintsEnabled) actualItemName = "another item"; - - Say(Responses.LocationHasDifferentItem?.Format(item.Metadata.NameWithArticle, actualItemName)); - } - else + var locationInfo = location.Metadata; + var isJunk = item.Metadata.IsJunk(World.Config); + if (isJunk) { - if (item.Type == location.VanillaItem && item.Type != ItemType.Nothing) - { - Say(x => x.TrackedVanillaItem); - return; - } - - var locationInfo = location.Metadata; - var isJunk = item.Metadata.IsJunk(World.Config); - if (isJunk) + if (!isTracking && locationInfo.WhenMarkingJunk?.Count > 0) { - if (!isTracking && locationInfo.WhenMarkingJunk?.Count > 0) - { - Say(locationInfo.WhenMarkingJunk.Random(s_random)!); - } - else if (locationInfo.WhenTrackingJunk?.Count > 0) - { - Say(locationInfo.WhenTrackingJunk.Random(s_random)!); - } + Say(locationInfo.WhenMarkingJunk.Random(s_random)!); } - else if (!isJunk) + else if (locationInfo.WhenTrackingJunk?.Count > 0) { - if (!isTracking && locationInfo.WhenMarkingProgression?.Count > 0) - { - Say(locationInfo.WhenMarkingProgression.Random(s_random)!); - } - else if (locationInfo.WhenTrackingProgression?.Count > 0) - { - Say(locationInfo.WhenTrackingProgression.Random(s_random)!); - } + Say(locationInfo.WhenTrackingJunk.Random(s_random)!); } } - } - - private IDungeon? GetDungeonFromItem(Item item, IDungeon? dungeon = null) - { - var locations = _worldService.Locations(itemFilter: item.Type) - .Where(x => x.Type != LocationType.NotInDungeon) - .ToImmutableList(); - - if (locations.Count == 1 && dungeon == null) + else if (!isJunk) { - // User didn't have a guess and there's only one location that - // has the tracker item - return GetDungeonFromLocation(locations[0]); - } - - if (locations.Count > 0 && dungeon != null) - { - // Does the dungeon even have that item? - if (locations.All(x => dungeon != x.Region)) + if (!isTracking && locationInfo.WhenMarkingProgression?.Count > 0) + { + Say(locationInfo.WhenMarkingProgression.Random(s_random)!); + } + else if (locationInfo.WhenTrackingProgression?.Count > 0) { - // Be a smart-ass about it - Say(Responses.ItemTrackedInIncorrectDungeon?.Format(dungeon.DungeonMetadata.Name, item.Metadata.NameWithArticle)); + Say(locationInfo.WhenTrackingProgression.Random(s_random)!); } } - - // - If tracker was started before generating a seed, we don't know - // better. - // - If we do know better, we should still go with the user's - // choice. - // - If there are multiple copies of the item, we don't know which - // was tracked. Either way, we have to assume `dungeon` is correct. - // If it's `null`, nobody knows. - return dungeon; } + } + + private IDungeon? GetDungeonFromItem(Item item, IDungeon? dungeon = null) + { + var locations = _worldService.Locations(itemFilter: item.Type) + .Where(x => x.Type != LocationType.NotInDungeon) + .ToImmutableList(); - private void IdleTimerElapsed(object? state) + if (locations.Count == 1 && dungeon == null) { - var key = (string)state!; - Say(Responses.Idle[key]); + // User didn't have a guess and there's only one location that + // has the tracker item + return GetDungeonFromLocation(locations[0]); } - private void Recognizer_SpeechRecognized(object? sender, SpeechRecognizedEventArgs e) + if (locations.Count > 0 && dungeon != null) { - RestartIdleTimers(); - OnSpeechRecognized(new(e.Result.Confidence, e.Result.Text)); + // Does the dungeon even have that item? + if (locations.All(x => dungeon != x.Region)) + { + // Be a smart-ass about it + Say(Responses.ItemTrackedInIncorrectDungeon?.Format(dungeon.DungeonMetadata.Name, item.Metadata.NameWithArticle)); + } } - private void GiveLocationHint(IEnumerable accessibleBefore) + // - If tracker was started before generating a seed, we don't know + // better. + // - If we do know better, we should still go with the user's + // choice. + // - If there are multiple copies of the item, we don't know which + // was tracked. Either way, we have to assume `dungeon` is correct. + // If it's `null`, nobody knows. + return dungeon; + } + + private void IdleTimerElapsed(object? state) + { + var key = (string)state!; + Say(Responses.Idle[key]); + } + + private void Recognizer_SpeechRecognized(object? sender, SpeechRecognizedEventArgs e) + { + RestartIdleTimers(); + OnSpeechRecognized(new(e.Result.Confidence, e.Result.Text)); + } + + private void GiveLocationHint(IEnumerable accessibleBefore) + { + var accessibleAfter = _worldService.AccessibleLocations(false); + var newlyAccessible = accessibleAfter.Except(accessibleBefore); + if (newlyAccessible.Any()) { - var accessibleAfter = _worldService.AccessibleLocations(false); - var newlyAccessible = accessibleAfter.Except(accessibleBefore); - if (newlyAccessible.Any()) - { - var regions = newlyAccessible.GroupBy(x => x.Region) - .OrderByDescending(x => x.Count()) - .ThenBy(x => x.Key.Name); + var regions = newlyAccessible.GroupBy(x => x.Region) + .OrderByDescending(x => x.Count()) + .ThenBy(x => x.Key.Name); - if (newlyAccessible.Contains(World.FindLocation(LocationId.InnerMaridiaSpringBall))) - Say(Responses.ShaktoolAvailable); + if (newlyAccessible.Contains(World.FindLocation(LocationId.InnerMaridiaSpringBall))) + Say(Responses.ShaktoolAvailable); - if (newlyAccessible.Contains(World.DarkWorldNorthWest.PegWorld)) - Say(Responses.PegWorldAvailable); - } - else if (Responses.TrackedUselessItem != null) - { - Say(Responses.TrackedUselessItem); - } + if (newlyAccessible.Contains(World.DarkWorldNorthWest.PegWorld)) + Say(Responses.PegWorldAvailable); + } + else if (Responses.TrackedUselessItem != null) + { + Say(Responses.TrackedUselessItem); } - } + } diff --git a/src/Randomizer.SMZ3.Tracking/TrackerEventArgs.cs b/src/Randomizer.SMZ3.Tracking/TrackerEventArgs.cs deleted file mode 100644 index ba4a09996..000000000 --- a/src/Randomizer.SMZ3.Tracking/TrackerEventArgs.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; - -namespace Randomizer.SMZ3.Tracking -{ - /// - /// Contains event data for tracking events. - /// - public class TrackerEventArgs : EventArgs - { - /// - /// Initializes a new instance of the - /// class. - /// - /// The speech recognition confidence. - /// If the event was triggered by auto tracker - public TrackerEventArgs(float? confidence, bool autoTracked = false) - { - Confidence = confidence; - AutoTracked = autoTracked; - } - - /// - /// Initializes a new instance of the - /// class. - /// - /// The speech recognition confidence. - /// The phrase that was recognized. - public TrackerEventArgs(float? confidence, string? phrase) - { - Confidence = confidence; - Phrase = phrase; - } - - /// - /// Initializes a new instance of the - /// class. - /// - /// If the event was triggered by auto tracker - public TrackerEventArgs(bool autoTracked) - { - AutoTracked = autoTracked; - } - - /// - /// Gets the speech recognition confidence as a value between 0.0 and - /// 1.0, or null if the event was not initiated by speech - /// recognition. - /// - public float? Confidence { get; } - - /// - /// Gets the phrase Tracker recognized, or null. - /// - public string? Phrase { get; } - - /// - /// If the event was triggered by auto tracker - /// - public bool AutoTracked { get; init; } - } -} diff --git a/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs b/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs index 56d05e26d..96b83b1ee 100644 --- a/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs +++ b/src/Randomizer.SMZ3.Tracking/TrackerServiceCollectionExtensions.cs @@ -4,102 +4,98 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; - -using Randomizer.SMZ3.Contracts; +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.AutoTracking.MetroidStateChecks; using Randomizer.SMZ3.Tracking.AutoTracking.ZeldaStateChecks; -using Randomizer.Data.Configuration; using Randomizer.SMZ3.Tracking.Services; using Randomizer.SMZ3.Tracking.VoiceCommands; -using Randomizer.Data; using Randomizer.Data.Options; -namespace Randomizer.SMZ3.Tracking +namespace Randomizer.SMZ3.Tracking; + +/// +/// Provides methods for adding tracking services to a service collection. +/// +public static class TrackerServiceCollectionExtensions { /// - /// Provides methods for adding tracking services to a service collection. + /// Adds the services required to start using Tracker. /// - public static class TrackerServiceCollectionExtensions + /// + /// The service collection to add Tracker to. + /// + /// A reference to . + public static IServiceCollection AddTracker(this IServiceCollection services) { - /// - /// Adds the services required to start using Tracker. - /// - /// - /// The service collection to add Tracker to. - /// - /// A reference to . - public static IServiceCollection AddTracker(this IServiceCollection services) - { - services.AddBasicTrackerModules(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddBasicTrackerModules(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - if (OperatingSystem.IsWindows()) - { - services.AddScoped(); - } - else - { - services.AddScoped(); - } - - var assemblies = new[] { Assembly.GetExecutingAssembly() }; + if (OperatingSystem.IsWindows()) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } - var zeldaStateChecks = assemblies - .SelectMany(a => a.GetTypes()) - .Where(t => !t.IsAbstract && t.IsClass && t.GetInterface(nameof(IZeldaStateCheck)) == typeof(IZeldaStateCheck)); - foreach (var stateCheck in zeldaStateChecks) - { - services.Add(new ServiceDescriptor(typeof(IZeldaStateCheck), stateCheck, ServiceLifetime.Transient)); - } + var assemblies = new[] { Assembly.GetExecutingAssembly() }; - var metroidStateChecks = assemblies - .SelectMany(a => a.GetTypes()) - .Where(t => !t.IsAbstract && t.IsClass && t.GetInterface(nameof(IMetroidStateCheck)) == typeof(IMetroidStateCheck)); - foreach (var stateCheck in metroidStateChecks) - { - services.Add(new ServiceDescriptor(typeof(IMetroidStateCheck), stateCheck, ServiceLifetime.Transient)); - } + var zeldaStateChecks = assemblies + .SelectMany(a => a.GetTypes()) + .Where(t => !t.IsAbstract && t.IsClass && t.GetInterface(nameof(IZeldaStateCheck)) == typeof(IZeldaStateCheck)); + foreach (var stateCheck in zeldaStateChecks) + { + services.Add(new ServiceDescriptor(typeof(IZeldaStateCheck), stateCheck, ServiceLifetime.Transient)); + } - return services; + var metroidStateChecks = assemblies + .SelectMany(a => a.GetTypes()) + .Where(t => !t.IsAbstract && t.IsClass && t.GetInterface(nameof(IMetroidStateCheck)) == typeof(IMetroidStateCheck)); + foreach (var stateCheck in metroidStateChecks) + { + services.Add(new ServiceDescriptor(typeof(IMetroidStateCheck), stateCheck, ServiceLifetime.Transient)); } + return services; + } - /// - /// Enables the specified tracker module. - /// - /// The type of module to enable. - /// - /// The service collection to add the tracker module to. - /// - /// A reference to . - public static IServiceCollection AddOptionalModule(this IServiceCollection services) - where TModule : TrackerModule - { - services.TryAddEnumerable(ServiceDescriptor.Scoped()); - return services; - } - private static IServiceCollection AddBasicTrackerModules(this IServiceCollection services) - { - var moduleTypes = typeof(TAssembly).Assembly.GetTypes() - .Where(x => x.IsSubclassOf(typeof(TrackerModule))); + /// + /// Enables the specified tracker module. + /// + /// The type of module to enable. + /// + /// The service collection to add the tracker module to. + /// + /// A reference to . + public static IServiceCollection AddOptionalModule(this IServiceCollection services) + where TModule : TrackerModule + { + services.TryAddEnumerable(ServiceDescriptor.Scoped()); + return services; + } - foreach (var moduleType in moduleTypes) - { - services.TryAddEnumerable(ServiceDescriptor.Scoped(typeof(TrackerModule), moduleType)); - } + private static IServiceCollection AddBasicTrackerModules(this IServiceCollection services) + { + var moduleTypes = typeof(TAssembly).Assembly.GetTypes() + .Where(x => x.IsSubclassOf(typeof(TrackerModule))); - return services; + foreach (var moduleType in moduleTypes) + { + services.TryAddEnumerable(ServiceDescriptor.Scoped(typeof(TrackerModule), moduleType)); } + + return services; } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs index fb1ee2c0c..ff18ed5f5 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/AutoTrackerModule.cs @@ -1,66 +1,64 @@ using System; using Microsoft.Extensions.Logging; -using Randomizer.SMZ3.Tracking.AutoTracking; +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; using Randomizer.Data.Options; -namespace Randomizer.SMZ3.Tracking.VoiceCommands +namespace Randomizer.SMZ3.Tracking.VoiceCommands; + +/// +/// Module for creating the auto tracker and interacting with the auto tracker +/// +public class AutoTrackerModule : TrackerModule, IDisposable { + private readonly IAutoTracker _autoTracker; + /// - /// Module for creating the auto tracker and interacting with the auto tracker + /// Initializes a new instance of the + /// class. /// - public class AutoTrackerModule : TrackerModule, IDisposable + /// The tracker instance. + /// Service to get item information + /// Service to get world information + /// Used to write logging information. + /// The auto tracker to associate with this module + public AutoTrackerModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, IAutoTracker autoTracker) + : base(tracker, itemService, worldService, logger) { - private readonly AutoTracker _autoTracker; - - /// - /// Initializes a new instance of the - /// class. - /// - /// The tracker instance. - /// Service to get item information - /// Service to get world information - /// Used to write logging information. - /// The auto tracker to associate with this module - public AutoTrackerModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, AutoTracker autoTracker) - : base(tracker, itemService, worldService, logger) - { - Tracker.AutoTracker = autoTracker; - _autoTracker = autoTracker; - } + Tracker.AutoTracker = autoTracker; + _autoTracker = autoTracker; + } - private GrammarBuilder GetLookAtGameRule() - { - return new GrammarBuilder() - .Append("Hey tracker, ") - .Optional("please", "would you please") - .OneOf("look at this", "look here", "record this", "log this", "take a look at this", "get a load of this") - .Optional("shit", "crap"); - } + private GrammarBuilder GetLookAtGameRule() + { + return new GrammarBuilder() + .Append("Hey tracker, ") + .Optional("please", "would you please") + .OneOf("look at this", "look here", "record this", "log this", "take a look at this", "get a load of this") + .Optional("shit", "crap"); + } - private void LookAtGame() + private void LookAtGame() + { + if (_autoTracker.LatestViewAction == null || _autoTracker.LatestViewAction.Invoke() == false) { - if (_autoTracker.LatestViewAction == null || _autoTracker.LatestViewAction.Invoke() == false) - { - Tracker.Say(x => x.AutoTracker.LookedAtNothing); - } + Tracker.Say(x => x.AutoTracker.LookedAtNothing); } + } - /// - /// Called when the module is destroyed - /// - public void Dispose() - { - _autoTracker.SetConnector(EmulatorConnectorType.None, ""); - } + /// + /// Called when the module is destroyed + /// + public void Dispose() + { + _autoTracker.SetConnector(EmulatorConnectorType.None, ""); + } - public override void AddCommands() + public override void AddCommands() + { + AddCommand("Look at this", GetLookAtGameRule(), (result) => { - AddCommand("Look at this", GetLookAtGameRule(), (result) => - { - LookAtGame(); - }); - } + LookAtGame(); + }); } - } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs index e1a197698..5267ef249 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/BossTrackingModule.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.Shared; using Randomizer.SMZ3.Tracking.Services; @@ -20,7 +20,7 @@ public class BossTrackingModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to write logging information. - public BossTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public BossTrackingModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs index 3299a9e46..2c9c06c95 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ChatIntegrationModule.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Shared.Enums; using Randomizer.SMZ3.ChatIntegration; using Randomizer.SMZ3.ChatIntegration.Models; @@ -45,7 +46,7 @@ public class ChatIntegrationModule : TrackerModule, IDisposable /// /// The chat client to use. /// Used to write logging information. - public ChatIntegrationModule(Tracker tracker, IChatClient chatClient, IItemService itemService, IWorldService worldService, ITrackerTimerService timerService, ILogger logger) + public ChatIntegrationModule(ITracker tracker, IChatClient chatClient, IItemService itemService, IWorldService worldService, ITrackerTimerService timerService, ILogger logger) : base(tracker, itemService, worldService, logger) { ChatClient = chatClient; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs index 5d5c041c9..3af68628b 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/CheatsModule.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Speech.Recognition; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Shared; using Randomizer.SMZ3.Tracking.Services; using Randomizer.Data.WorldData; @@ -34,7 +35,7 @@ public class CheatsModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to write logging information. - public CheatsModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public CheatsModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs index 02bfbb8da..0082711da 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/GoModeModule.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.SMZ3.Tracking.Services; @@ -21,7 +22,7 @@ public class GoModeModule : TrackerModule /// Service to get world information /// Used to log information. /// - public GoModeModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, ResponseConfig responseConfig) + public GoModeModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, ResponseConfig responseConfig) : base(tracker, itemService, worldService, logger) { _responseConfig = responseConfig; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs index bb7c4cf38..3772da801 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ItemTrackingModule.cs @@ -2,7 +2,7 @@ using System.Speech.Recognition; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -22,7 +22,7 @@ public class ItemTrackingModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to log information. - public ItemTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public ItemTrackingModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs index fa845ceab..8854307a6 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -18,7 +18,7 @@ public class LocationTrackingModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to log information. - public LocationTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public LocationTrackingModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs index 8da986550..1ae098118 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MapModule.cs @@ -1,7 +1,9 @@ using System.Linq; using System.Speech.Recognition; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Data.Configuration.ConfigFiles; +using Randomizer.Shared.Enums; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -23,7 +25,7 @@ public class MapModule : TrackerModule /// Service to get world information /// /// - public MapModule(Tracker tracker, IItemService itemService, ILogger logger, IWorldService worldService, TrackerMapConfig config) + public MapModule(ITracker tracker, IItemService itemService, ILogger logger, IWorldService worldService, TrackerMapConfig config) : base(tracker, itemService, worldService, logger) { _logger = logger; @@ -91,7 +93,7 @@ public override void AddCommands() AddCommand("Show dark room map", DarkRoomRule(), (result) => { // If the player is not in a Zelda cave/dungeon - if (Tracker.AutoTracker?.CurrentGame != AutoTracking.Game.Zelda || Tracker.AutoTracker?.ZeldaState?.OverworldScreen != 0) + if (Tracker.AutoTracker?.CurrentGame != Game.Zelda || Tracker.AutoTracker?.ZeldaState?.OverworldScreen != 0) { Tracker.Say(x => x.Map.NotInDarkRoom); return; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs index f78967682..b604adb86 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MetaModule.cs @@ -3,7 +3,7 @@ using System.Speech.Recognition; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -29,7 +29,7 @@ public class MetaModule : TrackerModule /// Service to get world information /// Used to write logging information. /// Used to communicate information to the user - public MetaModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, ICommunicator communicator) + public MetaModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, ICommunicator communicator) : base(tracker, itemService, worldService, logger) { _communicator = communicator; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs index 6d142670d..0eca607f1 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MsuModule.cs @@ -9,8 +9,10 @@ using MSURandomizerLibrary.Configs; using MSURandomizerLibrary.Models; using MSURandomizerLibrary.Services; +using Randomizer.Abstractions; using Randomizer.Data.Configuration.ConfigFiles; using Randomizer.Data.Options; +using Randomizer.Data.Tracking; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands; @@ -43,7 +45,7 @@ public class MsuModule : TrackerModule, IDisposable /// /// /// - public MsuModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, IMsuLookupService msuLookupService, IMsuSelectorService msuSelectorService, IMsuTypeService msuTypeService, MsuConfig msuConfig) + public MsuModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger, IMsuLookupService msuLookupService, IMsuSelectorService msuSelectorService, IMsuTypeService msuTypeService, MsuConfig msuConfig) : base(tracker, itemService, worldService, logger) { _msuSelectorService = msuSelectorService; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs index 1a60dda90..cbc03f977 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/MultiplayerModule.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; +using Randomizer.Data.Tracking; using Randomizer.Multiplayer.Client; using Randomizer.Multiplayer.Client.EventHandlers; using Randomizer.Shared; @@ -25,8 +27,8 @@ public class MultiplayerModule : TrackerModule /// Used to write logging information. /// The multiplayer game service /// - public MultiplayerModule(Tracker tracker, IItemService itemService, IWorldService worldService, - ILogger logger, MultiplayerGameService multiplayerGameService, AutoTracker autoTracker) + public MultiplayerModule(ITracker tracker, IItemService itemService, IWorldService worldService, + ILogger logger, MultiplayerGameService multiplayerGameService, IAutoTracker autoTracker) : base(tracker, itemService, worldService, logger) { _multiplayerGameService = multiplayerGameService; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs index 1d0a6f421..9f48a0ef4 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PegWorldModeModule.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -20,7 +20,7 @@ public class PegWorldModeModule : TrackerModule, IOptionalModule /// Service to get item information /// Service to get world information /// Used to log information. - public PegWorldModeModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public PegWorldModeModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs index 45129233f..7605d53cd 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/PersonalityModule.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -20,7 +20,7 @@ public class PersonalityModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to write logging information. - public PersonalityModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public PersonalityModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs index 1a7646a55..910e1acc3 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Data.Logic; using Randomizer.Data.WorldData.Regions; using Randomizer.Data.WorldData; @@ -42,7 +43,7 @@ public class SpoilerModule : TrackerModule, IOptionalModule /// Service to get world information /// Used to write logging information. /// Service for retrieving the randomizer config for the world - public SpoilerModule(Tracker tracker, IItemService itemService, ILogger logger, IWorldService worldService, IRandomizerConfigService randomizerConfigService) + public SpoilerModule(ITracker tracker, IItemService itemService, ILogger logger, IWorldService worldService, IRandomizerConfigService randomizerConfigService) : base(tracker, itemService, worldService, logger) { Tracker.HintsEnabled = tracker.World.Config is { Race: false, DisableTrackerHints: false } && tracker.Options.HintsEnabled; @@ -372,8 +373,8 @@ private bool GiveLocationHints(Location location) // Who's it for and is it any good? case 0: var characterName = location.Item.Type.IsInCategory(ItemCategory.Metroid) - ? Tracker.CorrectPronunciation(location.World.Config.SamusName) - : Tracker.CorrectPronunciation(location.World.Config.LinkName); + ? ITracker.CorrectPronunciation(location.World.Config.SamusName) + : ITracker.CorrectPronunciation(location.World.Config.LinkName); if (Tracker.IsWorth(location.Item)) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs index 14fb845b5..2ab5f8d5c 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs @@ -6,6 +6,7 @@ using System.Speech.Recognition; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Data.WorldData.Regions; using Randomizer.Data.WorldData; using Randomizer.Shared; @@ -66,7 +67,7 @@ public abstract class TrackerModule /// Service to get item information /// Service to get world information /// Used to log information. - protected TrackerModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + protected TrackerModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) { Tracker = tracker; ItemService = itemService; @@ -92,7 +93,7 @@ public IReadOnlyDictionary> Syntax /// /// Gets the Tracker instance. /// - protected Tracker Tracker { get; } + protected ITracker Tracker { get; } /// /// Service for getting item data @@ -138,7 +139,7 @@ public void LoadInto(SpeechRecognitionEngine engine) /// /// A from the recognition result. /// - protected static IDungeon GetDungeonFromResult(Tracker tracker, RecognitionResult result) + protected static IDungeon GetDungeonFromResult(ITracker tracker, RecognitionResult result) { var name = (string)result.Semantics[DungeonKey].Value; var dungeon = tracker.World.Dungeons.FirstOrDefault(x => x.DungeonName == name); @@ -154,7 +155,7 @@ protected static IDungeon GetDungeonFromResult(Tracker tracker, RecognitionResul /// /// A from the recognition result. /// - protected static IDungeon? GetBossDungeonFromResult(Tracker tracker, RecognitionResult result) + protected static IDungeon? GetBossDungeonFromResult(ITracker tracker, RecognitionResult result) { var name = (string)result.Semantics[BossKey].Value; return tracker.World.Dungeons.FirstOrDefault(x => x.DungeonName == name); @@ -169,7 +170,7 @@ protected static IDungeon GetDungeonFromResult(Tracker tracker, RecognitionResul /// /// A from the recognition result. /// - protected static Boss? GetBossFromResult(Tracker tracker, RecognitionResult result) + protected static Boss? GetBossFromResult(ITracker tracker, RecognitionResult result) { var bossName = (string)result.Semantics[BossKey].Value; return tracker.World.AllBosses.SingleOrDefault(x => x.Name.Contains(bossName, StringComparison.Ordinal)); @@ -187,7 +188,7 @@ protected static IDungeon GetDungeonFromResult(Tracker tracker, RecognitionResul /// /// An from the recognition result. /// - protected Item GetItemFromResult(Tracker tracker, RecognitionResult result, out string itemName) + protected Item GetItemFromResult(ITracker tracker, RecognitionResult result, out string itemName) { itemName = (string)result.Semantics[ItemNameKey].Value; var item = ItemService.FirstOrDefault(itemName); @@ -204,7 +205,7 @@ protected Item GetItemFromResult(Tracker tracker, RecognitionResult result, out /// /// A from the recognition result. /// - protected static Location GetLocationFromResult(Tracker tracker, RecognitionResult result) + protected static Location GetLocationFromResult(ITracker tracker, RecognitionResult result) { var id = (LocationId)result.Semantics[LocationKey].Value; var location = tracker.World.Locations.First(x => x.Id == id); @@ -218,7 +219,7 @@ protected static Location GetLocationFromResult(Tracker tracker, RecognitionResu /// The tracker instance. /// The speech recognition result. /// A from the recognition result. - protected static Room GetRoomFromResult(Tracker tracker, RecognitionResult result) + protected static Room GetRoomFromResult(ITracker tracker, RecognitionResult result) { var roomTypeName = (string)result.Semantics[RoomKey].Value; var room = tracker.World.Rooms.First(x => x.GetType().FullName == roomTypeName); @@ -234,7 +235,7 @@ protected static Room GetRoomFromResult(Tracker tracker, RecognitionResult resul /// /// A from the recognition result. /// - protected static Region GetRegionFromResult(Tracker tracker, RecognitionResult result) + protected static Region GetRegionFromResult(ITracker tracker, RecognitionResult result) { var regionTypeName = (string)result.Semantics[RegionKey].Value; var region = tracker.World.Regions.First(x => x.GetType().FullName == regionTypeName); @@ -250,7 +251,7 @@ protected static Region GetRegionFromResult(Tracker tracker, RecognitionResult r /// /// The recognized area from the recognition result. /// - protected static IHasLocations GetAreaFromResult(Tracker tracker, RecognitionResult result) + protected static IHasLocations GetAreaFromResult(ITracker tracker, RecognitionResult result) { if (result.Semantics.ContainsKey(RegionKey)) return GetRegionFromResult(tracker, result); diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs index f833d0c49..6744ce9a1 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModuleFactory.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; namespace Randomizer.SMZ3.Tracking.VoiceCommands { @@ -44,7 +45,7 @@ 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(ITracker tracker, SpeechRecognitionEngine? engine, out bool moduleLoadError) { moduleLoadError = false; _trackerModules = _serviceProvider.GetServices(); diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs index d5e0135b0..e97add6be 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/UndoModule.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; - +using Randomizer.Abstractions; using Randomizer.SMZ3.Tracking.Services; namespace Randomizer.SMZ3.Tracking.VoiceCommands @@ -17,7 +17,7 @@ public class UndoModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to log information. - public UndoModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public UndoModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs index 48ce41890..7c4cedca2 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/ZeldaDungeonTrackingModule.cs @@ -3,6 +3,7 @@ using System.Speech.Recognition; using Microsoft.Extensions.Logging; +using Randomizer.Abstractions; using Randomizer.Shared; using Randomizer.SMZ3.Tracking.Services; @@ -27,7 +28,7 @@ public class ZeldaDungeonTrackingModule : TrackerModule /// Service to get item information /// Service to get world information /// Used to log information. - public ZeldaDungeonTrackingModule(Tracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) + public ZeldaDungeonTrackingModule(ITracker tracker, IItemService itemService, IWorldService worldService, ILogger logger) : base(tracker, itemService, worldService, logger) { diff --git a/src/Randomizer.SMZ3/Playthrough.cs b/src/Randomizer.SMZ3/Playthrough.cs index b0f118d7b..2870f63b7 100644 --- a/src/Randomizer.SMZ3/Playthrough.cs +++ b/src/Randomizer.SMZ3/Playthrough.cs @@ -7,6 +7,7 @@ using Randomizer.Data.WorldData; using Randomizer.Shared; using Randomizer.Data.Options; +using Randomizer.Shared.Enums; namespace Randomizer.SMZ3 { diff --git a/src/Randomizer.Shared/Enums/EmulatorActionType.cs b/src/Randomizer.Shared/Enums/EmulatorActionType.cs new file mode 100644 index 000000000..533c0e49c --- /dev/null +++ b/src/Randomizer.Shared/Enums/EmulatorActionType.cs @@ -0,0 +1,17 @@ +namespace Randomizer.Shared.Enums; + +/// +/// The type of action +/// +public enum EmulatorActionType +{ + /// + /// Read a block from memory + /// + ReadBlock, + + /// + /// Write data to memory + /// + WriteBytes +} diff --git a/src/Randomizer.Shared/Enums/Game.cs b/src/Randomizer.Shared/Enums/Game.cs new file mode 100644 index 000000000..b9ba94f92 --- /dev/null +++ b/src/Randomizer.Shared/Enums/Game.cs @@ -0,0 +1,32 @@ +namespace Randomizer.Shared.Enums; + +/// +/// Which game(s) the message should be sent to the emulator in +/// +public enum Game +{ + /// + /// Send if the player has not started the game + /// + Neither, + + /// + /// Send if the player is in Super Metroid + /// + SM, + + /// + /// Send if the player is in Zelda + /// + Zelda, + + /// + /// Send if the player is in either game + /// + Both, + + /// + /// Send if the player is viewing the credits + /// + Credits +} diff --git a/src/Randomizer.Shared/Enums/KeysanityMode.cs b/src/Randomizer.Shared/Enums/KeysanityMode.cs index 0a23c19ce..76231be56 100644 --- a/src/Randomizer.Shared/Enums/KeysanityMode.cs +++ b/src/Randomizer.Shared/Enums/KeysanityMode.cs @@ -1,26 +1,20 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel; -namespace Randomizer.Shared +namespace Randomizer.Shared.Enums; + +[DefaultValue(None)] +[TypeConverter(typeof(EnumDescriptionTypeConverter))] +public enum KeysanityMode { - [DefaultValue(None)] - [TypeConverter(typeof(EnumDescriptionTypeConverter))] - public enum KeysanityMode - { - [Description("None")] - None, + [Description("None")] + None, - [Description("Both Games")] - Both, + [Description("Both Games")] + Both, - [Description("Zelda Only")] - Zelda, + [Description("Zelda Only")] + Zelda, - [Description("Metroid Only")] - SuperMetroid, - } + [Description("Metroid Only")] + SuperMetroid, } diff --git a/src/Randomizer.Shared/Enums/MemoryDomain.cs b/src/Randomizer.Shared/Enums/MemoryDomain.cs new file mode 100644 index 000000000..ac1e46130 --- /dev/null +++ b/src/Randomizer.Shared/Enums/MemoryDomain.cs @@ -0,0 +1,22 @@ +namespace Randomizer.Shared.Enums; + +/// +/// The type of memory +/// +public enum MemoryDomain +{ + /// + /// SNES Memory + /// + WRAM, + + /// + /// Cartridge Memory / Save File (AKA SRAM) + /// + CartRAM, + + /// + /// Game data saved on cartridge + /// + CartROM +} diff --git a/src/Randomizer.Tools/Program.cs b/src/Randomizer.Tools/Program.cs index 6b18b1094..adbe21f96 100644 --- a/src/Randomizer.Tools/Program.cs +++ b/src/Randomizer.Tools/Program.cs @@ -19,6 +19,7 @@ using Randomizer.Data.Logic; using Randomizer.Data.Options; using Randomizer.Shared; +using Randomizer.Shared.Enums; using Randomizer.SMZ3; using Randomizer.SMZ3.Generation; using Randomizer.SMZ3.Infrastructure;