diff --git a/stardew-access/HarmonyPatches.cs b/stardew-access/HarmonyPatches.cs index c5c26cf..f2dd7ff 100644 --- a/stardew-access/HarmonyPatches.cs +++ b/stardew-access/HarmonyPatches.cs @@ -21,17 +21,17 @@ namespace stardew_access harmony.Patch( original: AccessTools.Method(typeof(DialogueBox), nameof(DialogueBox.receiveLeftClick)), - postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.ClearDialogueString)) + postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.RecieveLeftClickPatch)) ); harmony.Patch( original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawHoverText), new Type[] { typeof(SpriteBatch), typeof(string), typeof(SpriteFont), typeof(int), typeof(int), typeof(int), typeof(string), typeof(int), typeof(string[]), typeof(Item), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(float), typeof(CraftingRecipe), typeof(IList) }), - postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.HoverTextPatch)) + postfix: new HarmonyMethod(typeof(IClickableMenuPatch), nameof(IClickableMenuPatch.DrawHoverTextPatch)) ); harmony.Patch( original: AccessTools.Method(typeof(NPC), nameof(NPC.drawAboveAlwaysFrontLayer)), - postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.drawAboveAlwaysFrontLayerPatch)) + postfix: new HarmonyMethod(typeof(NPCPatch), nameof(NPCPatch.DrawAboveAlwaysFrontLayerPatch)) ); #endregion @@ -129,7 +129,7 @@ namespace stardew_access #region Menu Patches harmony.Patch( original: AccessTools.Method(typeof(LetterViewerMenu), nameof(LetterViewerMenu.draw), new Type[] { typeof(SpriteBatch) }), - postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.LetterViewerMenuPatch)) + postfix: new HarmonyMethod(typeof(LetterViwerMenuPatch), nameof(LetterViwerMenuPatch.DrawPatch)) ); harmony.Patch( diff --git a/stardew-access/Patches/DialogueBoxPatch.cs b/stardew-access/Patches/DialogueBoxPatch.cs new file mode 100644 index 0000000..5f036e7 --- /dev/null +++ b/stardew-access/Patches/DialogueBoxPatch.cs @@ -0,0 +1,131 @@ +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewValley; +using StardewValley.Menus; + +namespace stardew_access.Patches +{ + internal class DialoguePatches + { + private static string currentDialogue = ""; + private static string previousSpeakerName = ""; + private static bool isDialogueAppearingFirstTime = true; + + internal static void DialoguePatch(DialogueBox __instance, SpriteBatch b) + { + try + { + if (__instance.transitioning) return; + + if (narrateCharacterDialogue(__instance)) return; + if (narrateQuestionDialogue(__instance)) return; + narrateBasicDialogue(__instance.getCurrentString()); + } + catch (Exception e) + { + MainClass.ErrorLog($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}"); + } + } + + internal static void RecieveLeftClickPatch() + { + // CLears the currentDialogue string on closing dialog + Cleanup(); + } + + private static bool narrateCharacterDialogue(DialogueBox __instance) + { + if (__instance.characterDialogue == null) return false; + + // For Normal Character dialogues + Dialogue dialogue = __instance.characterDialogue; + string speakerName = dialogue.speaker.displayName; + string dialogueText = ""; + string responseText = ""; + bool hasResponses = dialogue.isCurrentDialogueAQuestion(); + + dialogueText = $"{speakerName} said {__instance.getCurrentString()}"; + + if (hasResponses) + { + responseText = getCurrentResponseText(__instance); + + CheckAndSpeak(isDialogueAppearingFirstTime ? $"{dialogueText} \n\t {responseText}" : responseText, responseText); + if (isDialogueAppearingFirstTime) isDialogueAppearingFirstTime = false; + } + else + { + CheckAndSpeak(dialogueText); + } + + return true; + } + + private static bool narrateQuestionDialogue(DialogueBox __instance) + { + if (!__instance.isQuestion) return false; + + // For Dialogues with responses/answers like the dialogue when we click on tv + string questionText = ""; + string responseText = ""; + bool hasResponses = false; + + if (__instance.responses.Count > 0) hasResponses = true; + if (!hasResponses) return false; + + questionText = __instance.getCurrentString(); + + responseText = getCurrentResponseText(__instance); + + CheckAndSpeak(isDialogueAppearingFirstTime ? $"{questionText} \n\t {responseText}" : responseText, responseText); + if (isDialogueAppearingFirstTime) isDialogueAppearingFirstTime = false; + + return true; + } + + private static void narrateBasicDialogue(string dialogue) + { + // Basic dialogues like `No mails in the mail box` + if (Game1.activeClickableMenu is not DialogueBox) return; + CheckAndSpeak(dialogue); + } + + private static string getCurrentResponseText(DialogueBox __instance) + { + List responses = __instance.responses; + if (__instance.selectedResponse >= 0 && __instance.selectedResponse < responses.Count) + { + return $"{__instance.selectedResponse + 1}: {responses[__instance.selectedResponse].responseText}"; + } + else + { + // When the dialogue is not finished writing then the selectedResponse is <0 and this results + // in the first response not being detcted, so this sets the first response option to be the default + // if the current dialogue is a question or has responses + return $"1: {responses[0].responseText}"; + } + } + + private static void CheckAndSpeak(string toSpeak) + { + if (currentDialogue == toSpeak) return; + currentDialogue = toSpeak; + + MainClass.ScreenReader.Say(toSpeak, true); + } + + private static void CheckAndSpeak(string toSpeak, string checkQuery) + { + if (currentDialogue == checkQuery) return; + currentDialogue = checkQuery; + + MainClass.ScreenReader.Say(toSpeak, true); + } + + internal static void Cleanup() + { + currentDialogue = ""; + isDialogueAppearingFirstTime = true; + } + } +} diff --git a/stardew-access/Patches/DialoguePatches.cs b/stardew-access/Patches/DialoguePatches.cs deleted file mode 100644 index e131952..0000000 --- a/stardew-access/Patches/DialoguePatches.cs +++ /dev/null @@ -1,427 +0,0 @@ -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI; -using StardewValley; -using StardewValley.Menus; - -namespace stardew_access.Patches -{ - internal class DialoguePatches - { - internal static string currentLetterText = " "; - internal static string currentDialogue = " "; - internal static bool isDialogueAppearingFirstTime = true; - - internal static void DialoguePatch(DialogueBox __instance, SpriteBatch b) - { - try - { - if (__instance.transitioning) - return; - - if (__instance.characterDialogue != null) - { - // For Normal Character dialogues - Dialogue dialogue = __instance.characterDialogue; - string speakerName = dialogue.speaker.displayName; - List responses = __instance.responses; - string toSpeak = " "; - string dialogueText = ""; - string response = ""; - bool hasResponses = dialogue.isCurrentDialogueAQuestion(); - - dialogueText = $"{speakerName} said {__instance.getCurrentString()}"; - - if (hasResponses) - { - if (__instance.selectedResponse >= 0 && __instance.selectedResponse < responses.Count) - response = $"{__instance.selectedResponse + 1}: {responses[__instance.selectedResponse].responseText}"; - else - // When the dialogue is not finished writing then the selectedResponse is <0 and this results - // in the first response not being detcted, so this sets the first response option to be the default - // if the current dialogue is a question or has responses - response = $"1: {responses[0].responseText}"; - } - - if (hasResponses) - { - if (currentDialogue != response) - { - currentDialogue = response; - - if (isDialogueAppearingFirstTime) - { - toSpeak = $"{dialogueText} \n\t {response}"; - isDialogueAppearingFirstTime = false; - } - else - toSpeak = response; - - MainClass.ScreenReader.Say(toSpeak, true); - } - } - else - { - if (currentDialogue != dialogueText) - { - currentDialogue = dialogueText; - MainClass.ScreenReader.Say(dialogueText, true); - } - } - } - else if (__instance.isQuestion) - { - // For Dialogues with responses/answers like the dialogue when we click on tv - string toSpeak = ""; - string dialogueText = ""; - string response = ""; - bool hasResponses = false; - - if (__instance.responses.Count > 0) - hasResponses = true; - - dialogueText = __instance.getCurrentString(); - - if (hasResponses) - if (__instance.selectedResponse >= 0 && __instance.selectedResponse < __instance.responses.Count) - response = $"{__instance.selectedResponse + 1}: {__instance.responses[__instance.selectedResponse].responseText}"; - else - // When the dialogue is not finished writing then the selectedResponse is <0 and this results - // in the first response not being detcted, so this sets the first response option to be the default - // if the current dialogue is a question or has responses - response = $"1: {__instance.responses[0].responseText}"; - - - if (hasResponses) - { - if (currentDialogue != response) - { - currentDialogue = response; - - if (isDialogueAppearingFirstTime) - { - toSpeak = $"{dialogueText} \n\t {response}"; - isDialogueAppearingFirstTime = false; - } - else - toSpeak = response; - - MainClass.ScreenReader.Say(toSpeak, true); - } - } - else - { - if (currentDialogue != dialogueText) - { - currentDialogue = dialogueText; - MainClass.ScreenReader.Say(dialogueText, true); - } - } - } - else if (Game1.activeClickableMenu is DialogueBox) - { - // Basic dialogues like `No mails in the mail box` - if (currentDialogue != __instance.getCurrentString()) - { - currentDialogue = __instance.getCurrentString(); - MainClass.ScreenReader.Say(__instance.getCurrentString(), true); - } - } - } - catch (Exception e) - { - MainClass.ErrorLog($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}"); - } - - } - - internal static void ClearDialogueString() - { - // CLears the currentDialogue string on closing dialog - currentDialogue = " "; - isDialogueAppearingFirstTime = true; - } - - internal static void HoverTextPatch(string? text, int moneyAmountToDisplayAtBottom = -1, string? boldTitleText = null, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, string[]? buffIconsToDisplay = null, Item? hoveredItem = null, CraftingRecipe? craftingIngredients = null) - { - try - { - #region Skip narrating hover text for certain menus - if (Game1.activeClickableMenu is TitleMenu && !(((TitleMenu)Game1.activeClickableMenu).GetChildMenu() is CharacterCustomization)) - return; - else if (Game1.activeClickableMenu is LetterViewerMenu || Game1.activeClickableMenu is QuestLog) - return; - else if (Game1.activeClickableMenu is Billboard) - return; - else if (Game1.activeClickableMenu is GeodeMenu) - return; - else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is InventoryPage) - return; - else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is CraftingPage) - return; - else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is OptionsPage) - return; - else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is ExitPage) - return; - else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is SocialPage) - return; - else if (Game1.activeClickableMenu is ItemGrabMenu) - return; - else if (Game1.activeClickableMenu is ShopMenu) - return; - else if (Game1.activeClickableMenu is ConfirmationDialog) - return; - else if (Game1.activeClickableMenu is JunimoNoteMenu) - return; - else if (Game1.activeClickableMenu is CarpenterMenu) - return; - else if (Game1.activeClickableMenu is PurchaseAnimalsMenu) - return; - else if (Game1.activeClickableMenu is CraftingPage) - return; - else if (Game1.activeClickableMenu is AnimalQueryMenu) - return; - else if (Game1.activeClickableMenu is ConfirmationDialog) - return; - else if (Game1.activeClickableMenu is ReadyCheckDialog) - return; - else if (Game1.activeClickableMenu is JojaCDMenu) - return; - else if (Game1.activeClickableMenu is TailoringMenu) - return; - else if (Game1.activeClickableMenu is PondQueryMenu) - return; - else if (Game1.activeClickableMenu is ForgeMenu) - return; - else if (Game1.activeClickableMenu is ItemListMenu) - return; - else if (Game1.activeClickableMenu is FieldOfficeMenu) - return; - else if (Game1.activeClickableMenu is MuseumMenu) - return; - #endregion - - string toSpeak = " "; - - #region Add item count before title - if (hoveredItem != null && hoveredItem.HasBeenInInventory) - { - int count = hoveredItem.Stack; - if (count > 1) - toSpeak = $"{toSpeak} {count} "; - } - #endregion - - #region Add title if any - if (boldTitleText != null) - toSpeak = $"{toSpeak} {boldTitleText}\n"; - #endregion - - #region Add quality of item - if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Quality > 0) - { - int quality = ((StardewValley.Object)hoveredItem).Quality; - if (quality == 1) - { - toSpeak = $"{toSpeak} Silver quality"; - } - else if (quality == 2 || quality == 3) - { - toSpeak = $"{toSpeak} Gold quality"; - } - else if (quality >= 4) - { - toSpeak = $"{toSpeak} Iridium quality"; - } - } - #endregion - - #region Narrate hovered required ingredients - if (extraItemToShowIndex != -1) - { - string itemName = Game1.objectInformation[extraItemToShowIndex].Split('/')[0]; - - if (extraItemToShowAmount != -1) - toSpeak = $"{toSpeak} Required: {extraItemToShowAmount} {itemName}"; - else - toSpeak = $"{toSpeak} Required: {itemName}"; - } - #endregion - - #region Add money - if (moneyAmountToDisplayAtBottom != -1) - toSpeak = $"{toSpeak} \nCost: {moneyAmountToDisplayAtBottom}g\n"; - #endregion - - #region Add the base text - if (text == "???") - toSpeak = "unknown"; - else - toSpeak = $"{toSpeak} {text}"; - #endregion - - #region Add crafting ingredients - if (craftingIngredients != null) - { - toSpeak = $"{toSpeak} \n{craftingIngredients.description}"; - toSpeak = $"{toSpeak} \nIngredients\n"; - - craftingIngredients.recipeList.ToList().ForEach(recipe => - { - int count = recipe.Value; - int item = recipe.Key; - string name = craftingIngredients.getNameFromIndex(item); - - toSpeak = $"{toSpeak} ,{count} {name}"; - }); - } - #endregion - - #region Add health & stamina - if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Edibility != -300) - { - int stamina_recovery = ((StardewValley.Object)hoveredItem).staminaRecoveredOnConsumption(); - toSpeak = $"{toSpeak} {stamina_recovery} Energy\n"; - if (stamina_recovery >= 0) - { - int health_recovery = ((StardewValley.Object)hoveredItem).healthRecoveredOnConsumption(); - toSpeak = $"{toSpeak} {health_recovery} Health"; - } - } - #endregion - - #region Add buff items (effects like +1 walking speed) - if (buffIconsToDisplay != null) - { - for (int i = 0; i < buffIconsToDisplay.Length; i++) - { - string buffName = ((Convert.ToInt32(buffIconsToDisplay[i]) > 0) ? "+" : "") + buffIconsToDisplay[i] + " "; - if (i <= 11) - { - buffName = Game1.content.LoadString("Strings\\UI:ItemHover_Buff" + i, buffName); - } - try - { - int count = int.Parse(buffName.Substring(0, buffName.IndexOf(' '))); - if (count != 0) - toSpeak = $"{toSpeak} {buffName}\n"; - } - catch (Exception) { } - } - } - #endregion - - #region Narrate toSpeak - // To prevent it from getting conflicted by two hover texts at the same time, two seperate methods are used. - // For example, sometimes `Welcome to Pierre's` and the items in seeds shop get conflicted causing it to speak infinitely. - - if (toSpeak.ToString() != " ") - { - if (Context.IsPlayerFree) - MainClass.ScreenReader.SayWithChecker(toSpeak.ToString(), true); // Normal Checker - else - MainClass.ScreenReader.SayWithMenuChecker(toSpeak.ToString(), true); // Menu Checker - } - #endregion - } - catch (Exception e) - { - MainClass.ErrorLog($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}"); - } - } - - internal static void LetterViewerMenuPatch(LetterViewerMenu __instance) - { - try - { - if (!__instance.IsActive()) - return; - - NarrateLetterContent(__instance); - } - catch (Exception e) - { - MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}"); - } - } - - internal static void NarrateLetterContent(LetterViewerMenu __instance) - { - int x = Game1.getMousePosition().X, y = Game1.getMousePosition().Y; - #region Texts in the letter - string message = __instance.mailMessage[__instance.page]; - - string toSpeak = $"{message}"; - - if (__instance.ShouldShowInteractable()) - { - if (__instance.moneyIncluded > 0) - { - string moneyText = Game1.content.LoadString("Strings\\UI:LetterViewer_MoneyIncluded", __instance.moneyIncluded); - toSpeak += $"\t\n\t ,Included money: {moneyText}"; - } - else if (__instance.learnedRecipe != null && __instance.learnedRecipe.Length > 0) - { - string recipeText = Game1.content.LoadString("Strings\\UI:LetterViewer_LearnedRecipe", __instance.cookingOrCrafting); - toSpeak += $"\t\n\t ,Learned Recipe: {recipeText}"; - } - } - - if (currentLetterText != toSpeak) - { - currentLetterText = toSpeak; - - // snap mouse to accept quest button - if (__instance.acceptQuestButton != null && __instance.questID != -1) - { - toSpeak += "\t\n Left click to accept quest."; - __instance.acceptQuestButton.snapMouseCursorToCenter(); - } - if (__instance.mailMessage.Count > 1) - toSpeak = $"Page {__instance.page + 1} of {__instance.mailMessage.Count}:\n\t{toSpeak}"; - - MainClass.ScreenReader.Say(toSpeak, true); - } - #endregion - - #region Narrate items given in the mail - if (__instance.ShouldShowInteractable()) - { - foreach (ClickableComponent c in __instance.itemsToGrab) - { - if (c.item == null) - continue; - - string name = c.item.DisplayName; - - if (c.containsPoint(x, y)) - MainClass.ScreenReader.SayWithChecker($"Left click to collect {name}", false); - } - } - #endregion - - #region Narrate buttons - if (__instance.backButton != null && __instance.backButton.visible && __instance.backButton.containsPoint(x, y)) - MainClass.ScreenReader.SayWithChecker($"Previous page button", false); - - if (__instance.forwardButton != null && __instance.forwardButton.visible && __instance.forwardButton.containsPoint(x, y)) - MainClass.ScreenReader.SayWithChecker($"Next page button", false); - - #endregion - } - - internal static void drawAboveAlwaysFrontLayerPatch(NPC __instance, string ___textAboveHead, int ___textAboveHeadTimer) - { - try - { - if (___textAboveHeadTimer > 2900 && ___textAboveHead != null) - { - MainClass.ScreenReader.SayWithChecker($"{__instance.displayName} says {___textAboveHead}", true); - } - } - catch (Exception e) - { - MainClass.ErrorLog($"Error in patch:NPCShowTextAboveHeadPatch \n{e.Message}\n{e.StackTrace}"); - } - } - } -} diff --git a/stardew-access/Patches/GameMenuPatches/CollectionsPagePatch.cs b/stardew-access/Patches/GameMenuPatches/CollectionsPagePatch.cs index a708930..fdbaee4 100644 --- a/stardew-access/Patches/GameMenuPatches/CollectionsPagePatch.cs +++ b/stardew-access/Patches/GameMenuPatches/CollectionsPagePatch.cs @@ -9,7 +9,7 @@ namespace stardew_access.Patches int x = StardewValley.Game1.getMousePosition().X, y = StardewValley.Game1.getMousePosition().Y; if (__instance.letterviewerSubMenu != null) { - DialoguePatches.NarrateLetterContent(__instance.letterviewerSubMenu); + LetterViwerMenuPatch.narrateLetterContent(__instance.letterviewerSubMenu); } } catch (System.Exception e) diff --git a/stardew-access/Patches/IClickableMenuPatch.cs b/stardew-access/Patches/IClickableMenuPatch.cs index b0a6c8c..8e92817 100644 --- a/stardew-access/Patches/IClickableMenuPatch.cs +++ b/stardew-access/Patches/IClickableMenuPatch.cs @@ -1,3 +1,4 @@ +using StardewValley; using StardewValley.Menus; namespace stardew_access.Patches @@ -5,6 +6,194 @@ namespace stardew_access.Patches // These patches are global, i.e. work on every menus internal class IClickableMenuPatch { + internal static void DrawHoverTextPatch(string? text, int moneyAmountToDisplayAtBottom = -1, string? boldTitleText = null, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, string[]? buffIconsToDisplay = null, Item? hoveredItem = null, CraftingRecipe? craftingIngredients = null) + { + try + { + #region Skip narrating hover text for certain menus + if (Game1.activeClickableMenu is TitleMenu && !(((TitleMenu)Game1.activeClickableMenu).GetChildMenu() is CharacterCustomization)) + return; + else if (Game1.activeClickableMenu is LetterViewerMenu || Game1.activeClickableMenu is QuestLog) + return; + else if (Game1.activeClickableMenu is Billboard) + return; + else if (Game1.activeClickableMenu is GeodeMenu) + return; + else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is InventoryPage) + return; + else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is CraftingPage) + return; + else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is OptionsPage) + return; + else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is ExitPage) + return; + else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is SocialPage) + return; + else if (Game1.activeClickableMenu is ItemGrabMenu) + return; + else if (Game1.activeClickableMenu is ShopMenu) + return; + else if (Game1.activeClickableMenu is ConfirmationDialog) + return; + else if (Game1.activeClickableMenu is JunimoNoteMenu) + return; + else if (Game1.activeClickableMenu is CarpenterMenu) + return; + else if (Game1.activeClickableMenu is PurchaseAnimalsMenu) + return; + else if (Game1.activeClickableMenu is CraftingPage) + return; + else if (Game1.activeClickableMenu is AnimalQueryMenu) + return; + else if (Game1.activeClickableMenu is ConfirmationDialog) + return; + else if (Game1.activeClickableMenu is ReadyCheckDialog) + return; + else if (Game1.activeClickableMenu is JojaCDMenu) + return; + else if (Game1.activeClickableMenu is TailoringMenu) + return; + else if (Game1.activeClickableMenu is PondQueryMenu) + return; + else if (Game1.activeClickableMenu is ForgeMenu) + return; + else if (Game1.activeClickableMenu is ItemListMenu) + return; + else if (Game1.activeClickableMenu is FieldOfficeMenu) + return; + else if (Game1.activeClickableMenu is MuseumMenu) + return; + #endregion + + string toSpeak = " "; + + #region Add item count before title + if (hoveredItem != null && hoveredItem.HasBeenInInventory) + { + int count = hoveredItem.Stack; + if (count > 1) + toSpeak = $"{toSpeak} {count} "; + } + #endregion + + #region Add title if any + if (boldTitleText != null) + toSpeak = $"{toSpeak} {boldTitleText}\n"; + #endregion + + #region Add quality of item + if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Quality > 0) + { + int quality = ((StardewValley.Object)hoveredItem).Quality; + if (quality == 1) + { + toSpeak = $"{toSpeak} Silver quality"; + } + else if (quality == 2 || quality == 3) + { + toSpeak = $"{toSpeak} Gold quality"; + } + else if (quality >= 4) + { + toSpeak = $"{toSpeak} Iridium quality"; + } + } + #endregion + + #region Narrate hovered required ingredients + if (extraItemToShowIndex != -1) + { + string itemName = Game1.objectInformation[extraItemToShowIndex].Split('/')[0]; + + if (extraItemToShowAmount != -1) + toSpeak = $"{toSpeak} Required: {extraItemToShowAmount} {itemName}"; + else + toSpeak = $"{toSpeak} Required: {itemName}"; + } + #endregion + + #region Add money + if (moneyAmountToDisplayAtBottom != -1) + toSpeak = $"{toSpeak} \nCost: {moneyAmountToDisplayAtBottom}g\n"; + #endregion + + #region Add the base text + if (text == "???") + toSpeak = "unknown"; + else + toSpeak = $"{toSpeak} {text}"; + #endregion + + #region Add crafting ingredients + if (craftingIngredients != null) + { + toSpeak = $"{toSpeak} \n{craftingIngredients.description}"; + toSpeak = $"{toSpeak} \nIngredients\n"; + + craftingIngredients.recipeList.ToList().ForEach(recipe => + { + int count = recipe.Value; + int item = recipe.Key; + string name = craftingIngredients.getNameFromIndex(item); + + toSpeak = $"{toSpeak} ,{count} {name}"; + }); + } + #endregion + + #region Add health & stamina + if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Edibility != -300) + { + int stamina_recovery = ((StardewValley.Object)hoveredItem).staminaRecoveredOnConsumption(); + toSpeak = $"{toSpeak} {stamina_recovery} Energy\n"; + if (stamina_recovery >= 0) + { + int health_recovery = ((StardewValley.Object)hoveredItem).healthRecoveredOnConsumption(); + toSpeak = $"{toSpeak} {health_recovery} Health"; + } + } + #endregion + + #region Add buff items (effects like +1 walking speed) + if (buffIconsToDisplay != null) + { + for (int i = 0; i < buffIconsToDisplay.Length; i++) + { + string buffName = ((Convert.ToInt32(buffIconsToDisplay[i]) > 0) ? "+" : "") + buffIconsToDisplay[i] + " "; + if (i <= 11) + { + buffName = Game1.content.LoadString("Strings\\UI:ItemHover_Buff" + i, buffName); + } + try + { + int count = int.Parse(buffName.Substring(0, buffName.IndexOf(' '))); + if (count != 0) + toSpeak = $"{toSpeak} {buffName}\n"; + } + catch (Exception) { } + } + } + #endregion + + #region Narrate toSpeak + // To prevent it from getting conflicted by two hover texts at the same time, two seperate methods are used. + // For example, sometimes `Welcome to Pierre's` and the items in seeds shop get conflicted causing it to speak infinitely. + + if (toSpeak.ToString() != " ") + { + if (StardewModdingAPI.Context.IsPlayerFree) + MainClass.ScreenReader.SayWithChecker(toSpeak.ToString(), true); // Normal Checker + else + MainClass.ScreenReader.SayWithMenuChecker(toSpeak.ToString(), true); // Menu Checker + } + #endregion + } + catch (Exception e) + { + MainClass.ErrorLog($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}"); + } + } + internal static void ExitThisMenuPatch(IClickableMenu __instance) { try @@ -37,7 +226,7 @@ namespace stardew_access.Patches } else if (menu is LetterViewerMenu) { - DialoguePatches.currentLetterText = " "; + LetterViwerMenuPatch.Cleanup(); } else if (menu is LevelUpMenu) { @@ -94,8 +283,7 @@ namespace stardew_access.Patches } else if (menu is DialogueBox) { - DialoguePatches.isDialogueAppearingFirstTime = true; - DialoguePatches.currentDialogue = " "; + DialoguePatches.Cleanup(); } else if (menu is JojaCDMenu) { diff --git a/stardew-access/Patches/LetterViewerMenuPatch.cs b/stardew-access/Patches/LetterViewerMenuPatch.cs new file mode 100644 index 0000000..de7eabb --- /dev/null +++ b/stardew-access/Patches/LetterViewerMenuPatch.cs @@ -0,0 +1,95 @@ +using StardewValley; +using StardewValley.Menus; + +namespace stardew_access.Patches +{ + internal class LetterViwerMenuPatch + { + private static string currentLetterText = ""; + + internal static void DrawPatch(LetterViewerMenu __instance) + { + try + { + if (!__instance.IsActive()) + return; + + narrateLetterContent(__instance); + } + catch (Exception e) + { + MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}"); + } + } + + internal static void narrateLetterContent(LetterViewerMenu __instance) + { + int x = Game1.getMousePosition().X, y = Game1.getMousePosition().Y; + #region Texts in the letter + string message = __instance.mailMessage[__instance.page]; + + string toSpeak = $"{message}"; + + if (__instance.ShouldShowInteractable()) + { + if (__instance.moneyIncluded > 0) + { + string moneyText = Game1.content.LoadString("Strings\\UI:LetterViewer_MoneyIncluded", __instance.moneyIncluded); + toSpeak += $"\t\n\t ,Included money: {moneyText}"; + } + else if (__instance.learnedRecipe != null && __instance.learnedRecipe.Length > 0) + { + string recipeText = Game1.content.LoadString("Strings\\UI:LetterViewer_LearnedRecipe", __instance.cookingOrCrafting); + toSpeak += $"\t\n\t ,Learned Recipe: {recipeText}"; + } + } + + if (currentLetterText != toSpeak) + { + currentLetterText = toSpeak; + + // snap mouse to accept quest button + if (__instance.acceptQuestButton != null && __instance.questID != -1) + { + toSpeak += "\t\n Left click to accept quest."; + __instance.acceptQuestButton.snapMouseCursorToCenter(); + } + if (__instance.mailMessage.Count > 1) + toSpeak = $"Page {__instance.page + 1} of {__instance.mailMessage.Count}:\n\t{toSpeak}"; + + MainClass.ScreenReader.Say(toSpeak, true); + } + #endregion + + #region Narrate items given in the mail + if (__instance.ShouldShowInteractable()) + { + foreach (ClickableComponent c in __instance.itemsToGrab) + { + if (c.item == null) + continue; + + string name = c.item.DisplayName; + + if (c.containsPoint(x, y)) + MainClass.ScreenReader.SayWithChecker($"Left click to collect {name}", false); + } + } + #endregion + + #region Narrate buttons + if (__instance.backButton != null && __instance.backButton.visible && __instance.backButton.containsPoint(x, y)) + MainClass.ScreenReader.SayWithChecker($"Previous page button", false); + + if (__instance.forwardButton != null && __instance.forwardButton.visible && __instance.forwardButton.containsPoint(x, y)) + MainClass.ScreenReader.SayWithChecker($"Next page button", false); + + #endregion + } + + internal static void Cleanup() + { + currentLetterText = ""; + } + } +} diff --git a/stardew-access/Patches/MiniGamesPatches.cs b/stardew-access/Patches/MiniGamesPatches.cs index 8addd64..07480bf 100644 --- a/stardew-access/Patches/MiniGamesPatches.cs +++ b/stardew-access/Patches/MiniGamesPatches.cs @@ -49,7 +49,7 @@ namespace stardew_access.Patches if (___letterView != null) { - DialoguePatches.NarrateLetterContent(___letterView); + LetterViwerMenuPatch.narrateLetterContent(___letterView); } if (MainClass.ModHelper == null) @@ -117,4 +117,4 @@ namespace stardew_access.Patches return new Rectangle((int)Utility.getTopLeftPositionForCenteringOnScreen(Game1.viewport, 1294, 730).X + (286 - ___parallaxPan) * 4, (int)Utility.getTopLeftPositionForCenteringOnScreen(Game1.viewport, 1294, 730).Y + 218 + Math.Max(0, Math.Min(60, (___grandpaSpeechTimer - 5000) / 8)), 524, 344); } } -} \ No newline at end of file +} diff --git a/stardew-access/Patches/NPCPatch.cs b/stardew-access/Patches/NPCPatch.cs new file mode 100644 index 0000000..41f9306 --- /dev/null +++ b/stardew-access/Patches/NPCPatch.cs @@ -0,0 +1,22 @@ +using StardewValley; + +namespace stardew_access.Patches +{ + internal class NPCPatch + { + internal static void DrawAboveAlwaysFrontLayerPatch(NPC __instance, string ___textAboveHead, int ___textAboveHeadTimer) + { + try + { + if (___textAboveHeadTimer > 2900 && ___textAboveHead != null) + { + MainClass.ScreenReader.SayWithChecker($"{__instance.displayName} says {___textAboveHead}", true); + } + } + catch (Exception e) + { + MainClass.ErrorLog($"Error in patch:NPCShowTextAboveHeadPatch \n{e.Message}\n{e.StackTrace}"); + } + } + } +}