diff --git a/AdvancedSigns/AdvancedMenuPositioning.csproj b/AdvancedSigns/AdvancedMenuPositioning.csproj new file mode 100644 index 00000000..dc882f9d --- /dev/null +++ b/AdvancedSigns/AdvancedMenuPositioning.csproj @@ -0,0 +1,17 @@ + + + 1.0.0 + net5.0 + true + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/AdvancedSigns/CodePatches.cs b/AdvancedSigns/CodePatches.cs new file mode 100644 index 00000000..a1a2eec1 --- /dev/null +++ b/AdvancedSigns/CodePatches.cs @@ -0,0 +1,9 @@ +using StardewValley; +using StardewValley.Menus; + +namespace AdvancedMenuPositioning +{ + public partial class ModEntry + { + } +} \ No newline at end of file diff --git a/AdvancedSigns/IGenericModConfigMenuApi.cs b/AdvancedSigns/IGenericModConfigMenuApi.cs new file mode 100644 index 00000000..fc93c00e --- /dev/null +++ b/AdvancedSigns/IGenericModConfigMenuApi.cs @@ -0,0 +1,75 @@ +using StardewModdingAPI; +using System; + +namespace AdvancedMenuPositioning +{ + /// The API which lets other mods add a config UI through Generic Mod Config Menu. + public interface IGenericModConfigMenuApi + { + /********* + ** Methods + *********/ + /**** + ** Must be called first + ****/ + /// Register a mod whose config can be edited through the UI. + /// The mod's manifest. + /// Reset the mod's config to its default values. + /// Save the mod's current config to the config.json file. + /// Whether the options can only be edited from the title screen. + /// Each mod can only be registered once, unless it's deleted via before calling this again. + void Register(IManifest mod, Action reset, Action save, bool titleScreenOnly = false); + + /// Add a section title at the current position in the form. + /// The mod's manifest. + /// The title text shown in the form. + /// The tooltip text shown when the cursor hovers on the title, or null to disable the tooltip. + void AddSectionTitle(IManifest mod, Func text, Func tooltip = null); + + /// Add a key binding at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. + /// The unique field ID for use with , or null to auto-generate a randomized ID. + void AddKeybind(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string fieldId = null); + + /// Add a boolean option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. + /// The unique field ID for use with , or null to auto-generate a randomized ID. + void AddBoolOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string fieldId = null); + + + /// Add an integer option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. + /// The minimum allowed value, or null to allow any. + /// The maximum allowed value, or null to allow any. + /// The interval of values that can be selected. + /// The unique field ID for use with , or null to auto-generate a randomized ID. + void AddNumberOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, int? min = null, int? max = null, int? interval = null, string fieldId = null); + + /// Add a string option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. + /// The values that can be selected, or null to allow any. + /// Get the display text to show for a value from , or null to show the values as-is. + /// The unique field ID for use with , or null to auto-generate a randomized ID. + void AddTextOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string[] allowedValues = null, Func formatAllowedValue = null, string fieldId = null); + + /// Remove a mod from the config UI and delete all its options and pages. + /// The mod's manifest. + void Unregister(IManifest mod); + } +} diff --git a/AdvancedSigns/Methods.cs b/AdvancedSigns/Methods.cs new file mode 100644 index 00000000..b1c13c8b --- /dev/null +++ b/AdvancedSigns/Methods.cs @@ -0,0 +1,160 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI; +using StardewValley; +using StardewValley.Menus; +using System; +using System.Collections.Generic; +using System.Linq; +using Rectangle = Microsoft.Xna.Framework.Rectangle; + +namespace AdvancedMenuPositioning +{ + public partial class ModEntry : Mod + { + + private static void AdjustMenu(IClickableMenu menu, Point delta, bool first = false) + { + if (first) + { + adjustedMenus.Clear(); + adjustedComponents.Clear(); + } + if (menu is null || adjustedMenus.Contains(menu)) + return; + menu.xPositionOnScreen += delta.X; + menu.yPositionOnScreen += delta.Y; + var types = AccessTools.GetDeclaredFields(menu.GetType()); + if (menu is ItemGrabMenu) + { + types.AddRange(AccessTools.GetDeclaredFields(typeof(MenuWithInventory))); + } + foreach (var f in types) + { + + if (f.FieldType.IsSubclassOf(typeof(ClickableComponent)) || f.FieldType == typeof(ClickableComponent)) + { + AdjustComponent((ClickableComponent)f.GetValue(menu), delta); + + } + else if (f.FieldType.IsSubclassOf(typeof(IClickableMenu)) || f.FieldType == typeof(IClickableMenu)) + { + AdjustMenu((IClickableMenu)f.GetValue(menu), delta); + + } + else if (f.FieldType == typeof(InventoryMenu)) + { + AdjustMenu((IClickableMenu)f.GetValue(menu), delta); + } + else if (f.Name == "scrollBarRunner") + { + var c = (Rectangle)f.GetValue(menu); + c = new Rectangle(c.X + delta.X, c.Y + delta.Y, c.Width, c.Height); + f.SetValue(menu, c); + } + else if (f.FieldType == typeof(List)) + { + var ol = (List)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol) + { + AdjustComponent(o, delta); + } + } + else if (f.FieldType == typeof(List)) + { + var ol = (List)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol) + { + AdjustMenu(o, delta); + } + } + else if (f.FieldType == typeof(Dictionary)) + { + var ol = (Dictionary)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol.Values) + { + AdjustComponent(o, delta); + } + } + else if (f.FieldType == typeof(Dictionary)) + { + var ol = (Dictionary)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol.Values) + { + AdjustComponent(o, delta); + } + } + else if (f.FieldType == typeof(Dictionary>>)) + { + var ol = (Dictionary>>)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol.Values) + { + foreach (var o2 in o) + { + foreach (var o3 in o2) + { + AdjustComponent(o3, delta); + } + } + } + } + else if (f.FieldType == typeof(Dictionary>>)) + { + var ol = (Dictionary>>)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol.Values) + { + foreach (var o2 in o) + { + foreach (var o3 in o2) + { + AdjustComponent(o3, delta); + } + } + } + } + else if (f.FieldType == typeof(List)) + { + var ol = (List)f.GetValue(menu); + if (ol is null) + continue; + foreach (var o in ol) + { + AdjustComponent(o, delta); + } + } + } + if (menu is GameMenu) + { + for (int i = 0; i < (menu as GameMenu).pages.Count; i++) + { + if (i != (menu as GameMenu).currentTab) + AdjustMenu((menu as GameMenu).pages[i], delta); + } + } + AdjustComponent(menu.upperRightCloseButton, delta); + adjustedMenus.Add(menu); + } + + private static void AdjustComponent(ClickableComponent c, Point delta) + { + if (c is not null && !adjustedComponents.Contains(c)) + { + c.bounds = new Rectangle(c.bounds.X + delta.X, c.bounds.Y + delta.Y, c.bounds.Width, c.bounds.Height); + adjustedComponents.Add(c); + } + } + } +} \ No newline at end of file diff --git a/AdvancedSigns/ModConfig.cs b/AdvancedSigns/ModConfig.cs new file mode 100644 index 00000000..93844be4 --- /dev/null +++ b/AdvancedSigns/ModConfig.cs @@ -0,0 +1,16 @@ + +using StardewModdingAPI; + +namespace AdvancedMenuPositioning +{ + public class ModConfig + { + public bool EnableMod { get; set; } = true; + public SButton DetachKey { get; set; } = SButton.X; + public SButton MoveKey { get; set; } = SButton.MouseLeft; + public SButton CloseKey { get; set; } = SButton.Z; + public SButton DetachModKey { get; set; } = SButton.LeftShift; + public SButton MoveModKey { get; set; } = SButton.LeftShift; + public SButton CloseModKey { get; set; } = SButton.LeftShift; + } +} diff --git a/AdvancedSigns/ModEntry.cs b/AdvancedSigns/ModEntry.cs new file mode 100644 index 00000000..e224b8dc --- /dev/null +++ b/AdvancedSigns/ModEntry.cs @@ -0,0 +1,295 @@ +using Force.DeepCloner; +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI; +using StardewValley; +using StardewValley.Menus; +using StardewValley.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using Rectangle = Microsoft.Xna.Framework.Rectangle; + +namespace AdvancedMenuPositioning +{ + /// The mod entry point. + public partial class ModEntry : Mod + { + + public static IMonitor SMonitor; + public static IModHelper SHelper; + public static ModConfig Config; + public static ModEntry context; + private static Point lastMousePosition; + private static IClickableMenu currentlyDragging; + + private static List adjustedComponents = new List(); + private static List adjustedMenus = new List(); + private static List detachedMenus = new List(); + + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + Config = Helper.ReadConfig(); + + context = this; + + SMonitor = Monitor; + SHelper = helper; + + helper.Events.GameLoop.GameLaunched += GameLoop_GameLaunched; + helper.Events.GameLoop.UpdateTicking += GameLoop_UpdateTicking; + helper.Events.Input.ButtonPressed += Input_ButtonPressed; + helper.Events.Input.MouseWheelScrolled += Input_MouseWheelScrolled; + helper.Events.Display.RenderedWorld += Display_RenderedWorld; + + } + + private void Input_MouseWheelScrolled(object sender, StardewModdingAPI.Events.MouseWheelScrolledEventArgs e) + { + foreach (var m in detachedMenus) + { + m.receiveScrollWheelAction(e.Delta); + } + } + + private void Display_RenderedWorld(object sender, StardewModdingAPI.Events.RenderedWorldEventArgs e) + { + if (detachedMenus.Any()) + { + var back = Game1.options.showMenuBackground; + Game1.options.showMenuBackground = true; + foreach (var m in detachedMenus) + { + var f = AccessTools.Field(m.GetType(), "drawBG"); + if (f != null) + f.SetValue(m, false); + m.draw(e.SpriteBatch); + } + Game1.options.showMenuBackground = back; + } + } + + private void Input_ButtonPressed(object sender, StardewModdingAPI.Events.ButtonPressedEventArgs e) + { + if (!Config.EnableMod) + return; + if (Game1.activeClickableMenu != null && Helper.Input.IsDown(Config.DetachModKey) && e.Button == Config.DetachKey && new Rectangle(Game1.activeClickableMenu.xPositionOnScreen, Game1.activeClickableMenu.yPositionOnScreen, Game1.activeClickableMenu.width, Game1.activeClickableMenu.height).Contains(Game1.getMouseX(), Game1.getMouseY())) + { + detachedMenus.Add(Game1.activeClickableMenu); + Game1.activeClickableMenu = null; + Helper.Input.Suppress(e.Button); + Game1.playSound("bigDeSelect"); + return; + } + else if(detachedMenus.Count > 0) + { + if (Helper.Input.IsDown(Config.CloseModKey) && e.Button == Config.CloseKey) + { + for (int i = 0; i < detachedMenus.Count; i++) + { + if(detachedMenus[i].isWithinBounds(Game1.getMouseX(), Game1.getMouseY())) + { + detachedMenus.RemoveAt(i); + Helper.Input.Suppress(e.Button); + Game1.playSound("bigDeSelect"); + return; + } + } + } + if (Helper.Input.IsDown(Config.DetachModKey) && e.Button == Config.DetachKey && Game1.activeClickableMenu == null) + { + for (int i = 0; i < detachedMenus.Count; i++) + { + if(detachedMenus[i].isWithinBounds(Game1.getMouseX(), Game1.getMouseY())) + { + Game1.activeClickableMenu = detachedMenus[i]; + detachedMenus.RemoveAt(i); + Helper.Input.Suppress(e.Button); + Game1.playSound("bigSelect"); + return; + } + } + } + else if (e.Button == SButton.MouseLeft) + { + for (int i = 0; i < detachedMenus.Count; i++) + { + bool toBreak = detachedMenus[i].isWithinBounds(Game1.getMouseX(), Game1.getMouseY()); + + var menu = Game1.activeClickableMenu; + Game1.activeClickableMenu = detachedMenus[i]; + Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(), Game1.getMouseY()); + if (Game1.activeClickableMenu != null) + { + var d = new Point(detachedMenus[i].xPositionOnScreen - Game1.activeClickableMenu.xPositionOnScreen, detachedMenus[i].yPositionOnScreen - Game1.activeClickableMenu.yPositionOnScreen); + if (d != Point.Zero) + { + detachedMenus[i] = Game1.activeClickableMenu; + AdjustMenu(detachedMenus[i], d, true); + Game1.activeClickableMenu = menu; + } + + } + else + detachedMenus.RemoveAt(i); + Game1.activeClickableMenu = menu; + if (toBreak) + { + Helper.Input.Suppress(e.Button); + return; + } + + } + } + else if (e.Button == SButton.MouseRight) + { + for (int i = 0; i < detachedMenus.Count; i++) + { + bool toBreak = detachedMenus[i].isWithinBounds(Game1.getMouseX(), Game1.getMouseY()); + + var menu = Game1.activeClickableMenu; + Game1.activeClickableMenu = detachedMenus[i]; + Game1.activeClickableMenu.receiveRightClick(Game1.getMouseX(), Game1.getMouseY()); + if (Game1.activeClickableMenu != null) + { + var d = new Point(detachedMenus[i].xPositionOnScreen - Game1.activeClickableMenu.xPositionOnScreen, detachedMenus[i].yPositionOnScreen - Game1.activeClickableMenu.yPositionOnScreen); + if(d != Point.Zero) + { + detachedMenus[i] = Game1.activeClickableMenu; + AdjustMenu(detachedMenus[i], d, true); + Game1.activeClickableMenu = menu; + } + } + else + detachedMenus.RemoveAt(i); + Game1.activeClickableMenu = menu; + if (toBreak) + { + Helper.Input.Suppress(e.Button); + return; + } + } + } + } + } + + private void GameLoop_UpdateTicking(object sender, StardewModdingAPI.Events.UpdateTickingEventArgs e) + { + if(Config.EnableMod && Helper.Input.IsDown(Config.MoveModKey) && (Helper.Input.IsDown(Config.MoveKey) || Helper.Input.IsSuppressed(Config.MoveKey))) + { + if(Game1.activeClickableMenu != null) + { + if (currentlyDragging == Game1.activeClickableMenu || Game1.activeClickableMenu.isWithinBounds(Game1.getMouseX(), Game1.getMouseY())) + { + currentlyDragging = Game1.activeClickableMenu; + AdjustMenu(Game1.activeClickableMenu, Game1.getMousePosition() - lastMousePosition, true); + Helper.Input.Suppress(Config.MoveKey); + if (Game1.activeClickableMenu is ItemGrabMenu && Helper.ModRegistry.IsLoaded("Pathoschild.ChestsAnywhere")) + { + Game1.activeClickableMenu = Game1.activeClickableMenu.ShallowClone(); + } + goto next; + } + } + foreach (var menu in Game1.onScreenMenus) + { + if (menu is null) + continue; + if (currentlyDragging == menu || menu.isWithinBounds(Game1.getMouseX(), Game1.getMouseY())) + { + currentlyDragging = menu; + + AdjustMenu(menu, Game1.getMousePosition() - lastMousePosition, true); + Helper.Input.Suppress(Config.MoveKey); + goto next; + } + } + foreach (var menu in detachedMenus) + { + if (menu is null) + continue; + if (currentlyDragging == menu || menu.isWithinBounds(Game1.getMouseX(), Game1.getMouseY())) + { + currentlyDragging = menu; + + AdjustMenu(menu, Game1.getMousePosition() - lastMousePosition, true); + Helper.Input.Suppress(Config.MoveKey); + goto next; + } + } + } + currentlyDragging = null; + next: + lastMousePosition = Game1.getMousePosition(); + foreach (var menu in detachedMenus) + { + if (menu.isWithinBounds(Game1.getMouseX(), Game1.getMouseY())) + { + menu.performHoverAction(Game1.getMouseX(), Game1.getMouseY()); + } + } + } + + + private void GameLoop_GameLaunched(object sender, StardewModdingAPI.Events.GameLaunchedEventArgs e) + { + // get Generic Mod Config Menu's API (if it's installed) + var configMenu = Helper.ModRegistry.GetApi("spacechase0.GenericModConfigMenu"); + if (configMenu is null) + return; + + // register mod + configMenu.Register( + mod: ModManifest, + reset: () => Config = new ModConfig(), + save: () => Helper.WriteConfig(Config) + ); + + configMenu.AddBoolOption( + mod: ModManifest, + name: () => "Mod Enabled", + getValue: () => Config.EnableMod, + setValue: value => Config.EnableMod = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "Move Key", + getValue: () => Config.MoveKey, + setValue: value => Config.MoveKey = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "Detach Key", + getValue: () => Config.DetachKey, + setValue: value => Config.DetachKey = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "Close Key", + getValue: () => Config.CloseKey, + setValue: value => Config.CloseKey = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "Move Mod Key", + getValue: () => Config.MoveModKey, + setValue: value => Config.MoveModKey = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "DetachModKey Key", + getValue: () => Config.DetachModKey, + setValue: value => Config.DetachModKey = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "CloseModKey Key", + getValue: () => Config.CloseModKey, + setValue: value => Config.CloseModKey = value + ); + } + } +} \ No newline at end of file diff --git a/AdvancedSigns/manifest.json b/AdvancedSigns/manifest.json new file mode 100644 index 00000000..90375821 --- /dev/null +++ b/AdvancedSigns/manifest.json @@ -0,0 +1,22 @@ +{ + "Name": "Advanced Menu Positioning", + "Author": "aedenthorn", + "Version": "0.2.0", + "Description": "Advanced Menu Positioning.", + "UniqueID": "aedenthorn.AdvancedMenuPositioning", + "EntryDll": "AdvancedMenuPositioning.dll", + "MinimumApiVersion": "3.13.0", + "ModUpdater": { + "Repository": "StardewValleyMods", + "User": "aedenthorn", + "Directory": "_releases", + "ModFolder": "AdvancedMenuPositioning" + }, + "UpdateKeys": [ "Nexus:11315" ], + "Dependencies": [ + { + "UniqueID": "Platonymous.ModUpdater", + "IsRequired": false + } + ] +} \ No newline at end of file diff --git a/Email/Catalogues.cs b/Email/Catalogues.cs new file mode 100644 index 00000000..d36f012a --- /dev/null +++ b/Email/Catalogues.cs @@ -0,0 +1,70 @@ +using StardewValley; +using StardewValley.Menus; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Email +{ + public partial class ModEntry + { + public void OpenMail(int index) + { + string id = Game1.player.mailbox[index]; + if (!Game1.player.mailReceived.Contains(id) && !id.Contains("passedOut") && !id.Contains("Cooking")) + { + Game1.player.mailReceived.Add(id); + } + string mailTitle = id; + Game1.mailbox.RemoveAt(index); + Dictionary mails = Game1.content.Load>("Data\\mail"); + string mail = mails.ContainsKey(mailTitle) ? mails[mailTitle] : ""; + if (mailTitle.StartsWith("passedOut ")) + { + string[] split = mailTitle.Split(' ', StringSplitOptions.None); + int moneyTaken = (split.Length > 1) ? Convert.ToInt32(split[1]) : 0; + switch (new Random(moneyTaken).Next((Game1.player.getSpouse() != null && Game1.player.getSpouse().Name.Equals("Harvey")) ? 2 : 3)) + { + case 0: + if (Game1.MasterPlayer.hasCompletedCommunityCenter() && !Game1.MasterPlayer.mailReceived.Contains("JojaMember")) + { + mail = string.Format(mails["passedOut4"], moneyTaken); + } + else + { + mail = string.Format(mails["passedOut1_" + ((moneyTaken > 0) ? "Billed" : "NotBilled") + "_" + (Game1.player.IsMale ? "Male" : "Female")], moneyTaken); + } + break; + case 1: + mail = string.Format(mails["passedOut2"], moneyTaken); + break; + case 2: + mail = string.Format(mails["passedOut3_" + ((moneyTaken > 0) ? "Billed" : "NotBilled")], moneyTaken); + break; + } + } + else if (mailTitle.StartsWith("passedOut")) + { + string[] split2 = mailTitle.Split(' ', StringSplitOptions.None); + if (split2.Length > 1) + { + int moneyTaken2 = Convert.ToInt32(split2[1]); + mail = string.Format(mails[split2[0]], moneyTaken2); + } + } + if (mail.Length == 0) + { + return; + } + Game1.activeClickableMenu = new LetterViewerMenu(mail, mailTitle, false); + } + + + private async void DelayedOpen(ShopMenu menu) + { + await Task.Delay(100); + Monitor.Log("Really opening email"); + Game1.activeClickableMenu = menu; + } + } +} diff --git a/Email/Email.csproj b/Email/Email.csproj new file mode 100644 index 00000000..eed00f01 --- /dev/null +++ b/Email/Email.csproj @@ -0,0 +1,21 @@ + + + 1.0.0 + net5.0 + true + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file diff --git a/Email/EmailApp.cs b/Email/EmailApp.cs new file mode 100644 index 00000000..730e09dd --- /dev/null +++ b/Email/EmailApp.cs @@ -0,0 +1,42 @@ +using Microsoft.Xna.Framework; +using StardewModdingAPI; +using StardewValley; +using StardewValley.Menus; +using System; +using System.Collections.Generic; + +namespace Email +{ + public partial class ModEntry + { + public bool opening; + + public void OpenEmailApp() + { + Helper.Events.Input.ButtonPressed += Input_ButtonPressed; + api.SetAppRunning(true); + api.SetRunningApp(Helper.ModRegistry.ModID); + Helper.Events.Display.RenderedWorld += Display_RenderedWorld; + opening = true; + } + + public void CloseApp() + { + api.SetAppRunning(false); + api.SetRunningApp(null); + Helper.Events.Input.ButtonPressed -= Input_ButtonPressed; + Helper.Events.Display.RenderedWorld -= Display_RenderedWorld; + } + + + public void ClickRow(Point mousePos) + { + int idx = (int)((mousePos.Y - api.GetScreenPosition().Y - Config.MarginY - offsetY - Config.AppHeaderHeight) / (Config.MarginY + Config.AppRowHeight)); + Monitor.Log($"clicked index: {idx}"); + if (idx < Game1.player.mailbox.Count && idx >= 0) + { + OpenMail(idx); + } + } + } +} \ No newline at end of file diff --git a/Email/HelperEvents.cs b/Email/HelperEvents.cs new file mode 100644 index 00000000..1372f648 --- /dev/null +++ b/Email/HelperEvents.cs @@ -0,0 +1,57 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using System.IO; + +namespace Email +{ + public partial class ModEntry + { + public void GameLoop_GameLaunched(object sender, GameLaunchedEventArgs e) + { + if (!Config.EnableMod) + return; + api = Helper.ModRegistry.GetApi("aedenthorn.MobilePhone"); + if (api != null) + { + Texture2D appIcon; + bool success; + + appIcon = Helper.Content.Load(Path.Combine("assets", "app_icon.png")); + success = api.AddApp(Helper.ModRegistry.ModID, Helper.Translation.Get("email"), OpenEmailApp, appIcon); + Monitor.Log($"loaded email app successfully: {success}", LogLevel.Debug); + + MakeTextures(); + } + } + public void Input_ButtonPressed(object sender, ButtonPressedEventArgs e) + { + if (api.IsCallingNPC() || api.GetRunningApp() != Helper.ModRegistry.ModID) + return; + + if (e.Button == SButton.MouseLeft) + { + Point mousePos = Game1.getMousePosition(); + if (!api.GetScreenRectangle().Contains(mousePos)) + { + return; + } + + Helper.Input.Suppress(SButton.MouseLeft); + Vector2 screenPos = api.GetScreenPosition(); + Vector2 screenSize = api.GetScreenSize(); + if (!opening && new Rectangle((int)(screenPos.X + screenSize.X - Config.AppHeaderHeight), (int)(screenPos.Y), (int)Config.AppHeaderHeight, (int)Config.AppHeaderHeight).Contains(mousePos)) + { + Monitor.Log($"Closing app"); + CloseApp(); + return; + } + opening = false; + clicking = true; + lastMousePosition = mousePos; + } + } + } +} diff --git a/Email/IMobilePhoneApi.cs b/Email/IMobilePhoneApi.cs new file mode 100644 index 00000000..e4515761 --- /dev/null +++ b/Email/IMobilePhoneApi.cs @@ -0,0 +1,31 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using System; + +namespace Email +{ + public interface IMobilePhoneApi + { + bool AddApp(string id, string name, Action action, Texture2D icon); + + Vector2 GetScreenPosition(); + Vector2 GetScreenSize(); + Vector2 GetScreenSize(bool rotated); + Rectangle GetPhoneRectangle(); + Rectangle GetScreenRectangle(); + bool GetPhoneRotated(); + void SetPhoneRotated(bool value); + bool GetPhoneOpened(); + void SetPhoneOpened(bool value); + bool GetAppRunning(); + void SetAppRunning(bool value); + string GetRunningApp(); + void SetRunningApp(string value); + + void PlayRingTone(); + void PlayNotificationTone(); + NPC GetCallingNPC(); + bool IsCallingNPC(); + } +} \ No newline at end of file diff --git a/Email/ModConfig.cs b/Email/ModConfig.cs new file mode 100644 index 00000000..09715497 --- /dev/null +++ b/Email/ModConfig.cs @@ -0,0 +1,53 @@ +using Microsoft.Xna.Framework; + +namespace Email +{ + public class ModConfig + { + public bool EnableMod { get; set; } = true; + public bool EnableCatalogue { get; set; } = true; + public bool EnableFurnitureCatalogue { get; set; } = true; + public bool EnableSeedCatalogue { get; set; } = true; + public bool EnableTravelingCatalogue { get; set; } = true; + public bool LimitTravelingCatalogToInTown { get; set; } = true; + public bool EnableDesertCatalogue { get; set; } = true; + public bool LimitDesertCatalogToBusFixed { get; set; } = true; + public bool EnableHatCatalogue { get; set; } = true; + public bool EnableClothingCatalogue { get; set; } = true; + public bool EnableDwarfCatalogue { get; set; } = true; + public bool EnableKrobusCatalogue { get; set; } = true; + public bool EnableGuildCatalogue { get; set; } = true; + public bool RequireCataloguePurchase { get; set; } = true; + public int PriceCatalogue { get; set; } = 30000; + public int PriceFurnitureCatalogue { get; set; } = 100000; + public int PriceSeedCatalogue { get; set; } = 300000; + public int PriceTravelingCatalogue { get; set; } = 500000; + public int PriceDesertCatalogue { get; set; } = 500000; + public int PriceHatCatalogue { get; set; } = 10000; + public int PriceClothingCatalogue { get; set; } = 200000; + public int PriceDwarfCatalogue { get; set; } = 200000; + public int PriceKrobusCatalogue { get; set; } = 200000; + public int PriceGuildCatalogue { get; set; } = 100000; + public bool FreeCatalogue { get; set; } = false; + public bool FreeFurnitureCatalogue { get; set; } = false; + public bool FreeSeedCatalogue { get; set; } = false; + public bool FreeDesertCatalogue { get; set; } = false; + public bool FreeTravelingCatalogue { get; set; } = false; + public bool FreeHatCatalogue { get; set; } = false; + public bool FreeClothingCatalogue { get; set; } = false; + public string SeedsToInclude { get; set; } = "season"; + public float PriceMult { get; set; } = 1f; + public int AppHeaderHeight { get; set; } = 32; + public int AppRowHeight { get; set; } = 32; + public Color BackgroundColor { get; set; } = Color.White; + public Color HighlightColor { get; set; } = new Color(230,230,255); + public Color GreyedColor { get; set; } = new Color(230, 230, 230); + public Color HeaderColor { get; set; } = new Color(100, 100, 200); + public Color TextColor { get; set; } = Color.Black; + public Color HeaderTextColor { get; set; } = Color.White; + public int MarginX { get; set; } = 4; + public int MarginY { get; set; } = 4; + public float HeaderTextScale { get; set; } = 0.5f; + public float TextScale { get; set; } = 0.5f; + } +} diff --git a/Email/ModEntry.cs b/Email/ModEntry.cs new file mode 100644 index 00000000..0ebbe88e --- /dev/null +++ b/Email/ModEntry.cs @@ -0,0 +1,41 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Objects; +using StardewValley.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Object = StardewValley.Object; + +namespace Email +{ + public partial class ModEntry : Mod + { + public static ModEntry context; + + public static ModConfig Config; + public static IModHelper SHelper; + public static IMonitor SMonitor; + public static IMobilePhoneApi api; + + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + context = this; + Config = Helper.ReadConfig(); + SHelper = Helper; + SMonitor = Monitor; + + Helper.Events.GameLoop.GameLaunched += GameLoop_GameLaunched; + } + + } +} diff --git a/Email/Visuals.cs b/Email/Visuals.cs new file mode 100644 index 00000000..19bbf00e --- /dev/null +++ b/Email/Visuals.cs @@ -0,0 +1,119 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using System; +using System.IO; + +namespace Email +{ + public partial class ModEntry + { + private static Texture2D backgroundTexture; + private static Texture2D hightlightTexture; + private static Texture2D headerTexture; + public static bool clicking; + private static bool dragging; + public static Point lastMousePosition; + public static float offsetY; + + public void MakeTextures() + { + Vector2 screenSize = api.GetScreenSize(); + Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, (int)screenSize.X, (int)screenSize.Y); + Color[] data = new Color[texture.Width * texture.Height]; + for (int pixel = 0; pixel < data.Length; pixel++) + { + data[pixel] = Config.BackgroundColor; + } + texture.SetData(data); + backgroundTexture = texture; + texture = new Texture2D(Game1.graphics.GraphicsDevice, (int)screenSize.X, Config.AppRowHeight); + data = new Color[texture.Width * texture.Height]; + for (int pixel = 0; pixel < data.Length; pixel++) + { + data[pixel] = Config.HighlightColor; + } + texture.SetData(data); + hightlightTexture = texture; + + texture = new Texture2D(Game1.graphics.GraphicsDevice, (int)screenSize.X, Config.AppHeaderHeight); + data = new Color[texture.Width * texture.Height]; + for (int pixel = 0; pixel < data.Length; pixel++) + { + data[pixel] = Config.HeaderColor; + } + texture.SetData(data); + headerTexture = texture; + + } + + public void Display_RenderedWorld(object sender, RenderedWorldEventArgs e) + { + + if (api.IsCallingNPC()) + return; + + Vector2 screenPos = api.GetScreenPosition(); + Vector2 screenSize = api.GetScreenSize(); + if (!api.GetPhoneOpened() || !api.GetAppRunning() || api.GetRunningApp() != Helper.ModRegistry.ModID) + { + Monitor.Log($"Closing app: phone opened {api.GetPhoneOpened()} app running {api.GetAppRunning()} running app {api.GetRunningApp()}"); + CloseApp(); + return; + } + + if (!clicking) + dragging = false; + + Point mousePos = Game1.getMousePosition(); + if (clicking) + { + if (mousePos.Y != lastMousePosition.Y && (dragging || api.GetScreenRectangle().Contains(mousePos))) + { + dragging = true; + offsetY += mousePos.Y - lastMousePosition.Y; + //Monitor.Log($"offsetY {offsetY} max {screenSize.Y - Config.MarginY + (Config.MarginY + Game1.dialogueFont.LineSpacing * 0.9f) * audio.Length}"); + offsetY = Math.Min(0, Math.Max(offsetY, (int)(screenSize.Y - (Config.AppHeaderHeight + Config.MarginY + (Config.MarginY + Config.AppRowHeight) * Game1.player.mailbox.Count)))); + lastMousePosition = mousePos; + } + } + + if (clicking && !Helper.Input.IsSuppressed(SButton.MouseLeft)) + { + Monitor.Log($"unclicking; dragging = {dragging}"); + if (dragging) + dragging = false; + else if (api.GetScreenRectangle().Contains(mousePos) && !new Rectangle((int)screenPos.X, (int)screenPos.Y, (int)screenSize.X, Config.AppHeaderHeight).Contains(mousePos)) + { + ClickRow(mousePos); + } + clicking = false; + } + + + e.SpriteBatch.Draw(backgroundTexture, new Rectangle((int)screenPos.X, (int)screenPos.Y, (int)screenSize.X, (int)screenSize.Y), Color.White); + for (int i = 0; i < Game1.player.mailbox.Count; i++) + { + string a = Game1.player.mailbox[i]; + float posY = screenPos.Y + Config.AppHeaderHeight + Config.MarginY * (i + 1) + i * Config.AppRowHeight + offsetY; + Rectangle r = new Rectangle((int)screenPos.X, (int)posY, (int)screenSize.X, Config.AppRowHeight); + + if(r.Contains(mousePos)) + e.SpriteBatch.Draw(hightlightTexture, r, Color.White); + + float textHeight = Game1.dialogueFont.MeasureString(a).Y * Config.TextScale; + if (posY > screenPos.Y && posY < screenPos.Y + screenSize.Y - Config.AppRowHeight / 2f + textHeight / 2f) + { + e.SpriteBatch.DrawString(Game1.dialogueFont, a, new Vector2(screenPos.X + Config.MarginX, posY + Config.AppRowHeight / 2f - textHeight / 2f), Config.TextColor, 0f, Vector2.Zero, Config.TextScale, SpriteEffects.None, 0.86f); + } + } + e.SpriteBatch.Draw(headerTexture, new Rectangle((int)screenPos.X, (int)screenPos.Y, (int)screenSize.X, Config.AppHeaderHeight), Color.White); + float headerTextHeight = Game1.dialogueFont.MeasureString(Helper.Translation.Get("email")).Y * Config.HeaderTextScale; + Vector2 xSize = Game1.dialogueFont.MeasureString("x") * Config.HeaderTextScale; + e.SpriteBatch.DrawString(Game1.dialogueFont, Helper.Translation.Get("email"), new Vector2(screenPos.X + Config.MarginX, screenPos.Y + Config.AppHeaderHeight / 2f - headerTextHeight / 2f), Config.HeaderTextColor, 0f, Vector2.Zero, Config.HeaderTextScale, SpriteEffects.None, 0.86f); + e.SpriteBatch.DrawString(Game1.dialogueFont, "x", new Vector2(screenPos.X + screenSize.X - Config.AppHeaderHeight / 2f - xSize.X / 2f, screenPos.Y + Config.AppHeaderHeight / 2f - xSize.Y / 2f), Config.HeaderTextColor, 0f, Vector2.Zero, Config.HeaderTextScale, SpriteEffects.None, 0.86f); + } + } +} diff --git a/Email/assets/app_icon.png b/Email/assets/app_icon.png new file mode 100644 index 00000000..8af180f2 Binary files /dev/null and b/Email/assets/app_icon.png differ diff --git a/Email/i18n/default.json b/Email/i18n/default.json new file mode 100644 index 00000000..acf7aa0b --- /dev/null +++ b/Email/i18n/default.json @@ -0,0 +1,3 @@ +{ + "email": "Email" +} \ No newline at end of file diff --git a/Email/manifest.json b/Email/manifest.json new file mode 100644 index 00000000..73e74e59 --- /dev/null +++ b/Email/manifest.json @@ -0,0 +1,26 @@ +{ + "Name": "Email", + "Author": "aedenthorn", + "Version": "0.1.0", + "Description": "Check mail on your mobile phone.", + "UniqueID": "aedenthorn.Email", + "EntryDll": "Email.dll", + "MinimumApiVersion": "3.13.0", + "UpdateKeys": [ "Nexus:" ], + "ModUpdater": { + "Repository": "StardewValleyMods", + "User": "aedenthorn", + "Directory": "_releases", + "ModFolder": "Email" + }, + "Dependencies": [ + { + "UniqueID": "Platonymous.ModUpdater", + "IsRequired": false + }, + { + "UniqueID": "aedenthorn.MobilePhone", + "IsRequired": true + } + ] +} \ No newline at end of file diff --git a/MoveableMailbox/ModEntry.cs b/MoveableMailbox/ModEntry.cs index a540234b..cca3872b 100644 --- a/MoveableMailbox/ModEntry.cs +++ b/MoveableMailbox/ModEntry.cs @@ -1,4 +1,4 @@ -using Harmony; +using HarmonyLib; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; @@ -39,7 +39,7 @@ public override void Entry(IModHelper helper) Helper.Events.GameLoop.SaveLoaded += GameLoop_SaveLoaded; Helper.Events.GameLoop.SaveLoaded += GameLoop_SaveLoaded; - var harmony = HarmonyInstance.Create(ModManifest.UniqueID); + var harmony = new Harmony(ModManifest.UniqueID); harmony.Patch( original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), @@ -58,7 +58,7 @@ public override void Entry(IModHelper helper) harmony.Patch( original: AccessTools.Method(typeof(Object), nameof(Object.performRemoveAction)), - prefix: new HarmonyMethod(typeof(ModEntry), nameof(performRemoveAction_Prefix)) { prioritiy = Priority.First } + prefix: new HarmonyMethod(typeof(ModEntry), nameof(performRemoveAction_Prefix)) ); } @@ -142,7 +142,7 @@ private static void loadForNewGame_Postfix() private static void placementAction_Postfix(Object __instance, bool __result, int x, int y, Farmer who) { - if (!__result || !__instance.Name.EndsWith("Mailbox") || who == null || !(who.currentLocation is Farm)) + if (!__result || !__instance.Name.EndsWith("Mailbox") || who == null) return; (who.currentLocation as Farm).mapMainMailboxPosition = new Point(x / 64, y / 64); @@ -152,7 +152,7 @@ private static void placementAction_Postfix(Object __instance, bool __result, in private static bool checkForAction_Prefix(Object __instance, ref bool __result, Farmer who, bool justCheckingForActivity) { - if (__instance.Name.EndsWith("Mailbox") && who.currentLocation is Farm && !justCheckingForActivity) + if (__instance.Name.EndsWith("Mailbox") && !justCheckingForActivity) { PMonitor.Log("Clicked on mailbox"); Point mailbox_position = Game1.player.getMailboxPosition(); diff --git a/StardewValleyMods.sln b/StardewValleyMods.sln index 9d6cd8c9..c772e1e0 100644 --- a/StardewValleyMods.sln +++ b/StardewValleyMods.sln @@ -264,6 +264,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedMenuPositioning", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomToolbar", "CustomToolbar\CustomToolbar.csproj", "{8AD9149E-CA85-4C62-B549-6CA594FE5AA2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Email", "Email\Email.csproj", "{9C00F8E7-2094-454F-8F95-955E9EFBDA3C}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution _releases\_releases.projitems*{047040e0-3d07-46e5-852b-d1d45cd57bb8}*SharedItemsImports = 13 @@ -1291,6 +1293,14 @@ Global {8AD9149E-CA85-4C62-B549-6CA594FE5AA2}.Release|Any CPU.Build.0 = Release|Any CPU {8AD9149E-CA85-4C62-B549-6CA594FE5AA2}.Release|x86.ActiveCfg = Release|Any CPU {8AD9149E-CA85-4C62-B549-6CA594FE5AA2}.Release|x86.Build.0 = Release|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Debug|x86.Build.0 = Debug|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Release|Any CPU.Build.0 = Release|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Release|x86.ActiveCfg = Release|Any CPU + {9C00F8E7-2094-454F-8F95-955E9EFBDA3C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE