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