From bdff32162bbb89bec18e9961610468c1a24d4574 Mon Sep 17 00:00:00 2001 From: aedenthorn Date: Thu, 17 Mar 2022 09:26:52 -0400 Subject: [PATCH] etc --- CustomSigns/CodePatches.cs | 120 ++++++++++ CustomSigns/CustomSignData.cs | 30 +++ CustomSigns/CustomSigns.csproj | 25 ++ CustomSigns/IGenericModConfigMenuApi.cs | 75 ++++++ CustomSigns/Methods.cs | 71 ++++++ CustomSigns/ModConfig.cs | 12 + CustomSigns/ModEntry.cs | 138 +++++++++++ CustomSigns/i18n/default.json | 4 + CustomSigns/manifest.json | 22 ++ NewProject/CodePatches.cs | 9 + NewProject/CustomSigns.csproj | 17 ++ NewProject/IGenericModConfigMenuApi.cs | 75 ++++++ NewProject/Methods.cs | 160 +++++++++++++ NewProject/ModConfig.cs | 16 ++ NewProject/ModEntry.cs | 295 ++++++++++++++++++++++++ NewProject/manifest.json | 22 ++ StardewValleyMods.sln | 22 +- WallPlanter/CodePatches.cs | 160 +++++++++++++ WallPlanter/IGenericModConfigMenuApi.cs | 75 ++++++ WallPlanter/Methods.cs | 18 ++ WallPlanter/ModConfig.cs | 15 ++ WallPlanter/ModEntry.cs | 124 ++++++++++ WallPlanter/WallPlanter.csproj | 31 +++ WallPlanter/assets/wall_pot.png | Bin 0 -> 606 bytes WallPlanter/assets/wall_pot_wet.png | Bin 0 -> 534 bytes WallPlanter/i18n/default.json | 4 + WallPlanter/manifest.json | 22 ++ 27 files changed, 1561 insertions(+), 1 deletion(-) create mode 100644 CustomSigns/CodePatches.cs create mode 100644 CustomSigns/CustomSignData.cs create mode 100644 CustomSigns/CustomSigns.csproj create mode 100644 CustomSigns/IGenericModConfigMenuApi.cs create mode 100644 CustomSigns/Methods.cs create mode 100644 CustomSigns/ModConfig.cs create mode 100644 CustomSigns/ModEntry.cs create mode 100644 CustomSigns/i18n/default.json create mode 100644 CustomSigns/manifest.json create mode 100644 NewProject/CodePatches.cs create mode 100644 NewProject/CustomSigns.csproj create mode 100644 NewProject/IGenericModConfigMenuApi.cs create mode 100644 NewProject/Methods.cs create mode 100644 NewProject/ModConfig.cs create mode 100644 NewProject/ModEntry.cs create mode 100644 NewProject/manifest.json create mode 100644 WallPlanter/CodePatches.cs create mode 100644 WallPlanter/IGenericModConfigMenuApi.cs create mode 100644 WallPlanter/Methods.cs create mode 100644 WallPlanter/ModConfig.cs create mode 100644 WallPlanter/ModEntry.cs create mode 100644 WallPlanter/WallPlanter.csproj create mode 100644 WallPlanter/assets/wall_pot.png create mode 100644 WallPlanter/assets/wall_pot_wet.png create mode 100644 WallPlanter/i18n/default.json create mode 100644 WallPlanter/manifest.json diff --git a/CustomSigns/CodePatches.cs b/CustomSigns/CodePatches.cs new file mode 100644 index 00000000..7d1003e8 --- /dev/null +++ b/CustomSigns/CodePatches.cs @@ -0,0 +1,120 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Netcode; +using StardewValley; +using StardewValley.Tools; +using System; +using Object = StardewValley.Object; + +namespace CustomSigns +{ + public partial class ModEntry + { + private static Object placedSign; + + [HarmonyPatch(typeof(GameLocation), nameof(GameLocation.isCollidingPosition), new Type[] { typeof(Rectangle), typeof(xTile.Dimensions.Rectangle), typeof(bool), typeof(int), typeof(bool), typeof(Character), typeof(bool), typeof(bool), typeof(bool) })] + public class isCollidingPosition_Patch + { + public static bool Prefix(GameLocation __instance, Rectangle position, xTile.Dimensions.Rectangle viewport, bool isFarmer, int damagesFarmer, bool glider, Character character, bool pathfinding, bool projectile, bool ignoreCharacterRequirement, ref bool __result) + { + if (!Config.EnableMod) + return true; + foreach(var obj in __instance.objects.Values) + { + if (!customSignTypeDict.ContainsKey(obj.Name) || !obj.modData.ContainsKey(templateKey)) + continue; + if(obj.getBoundingBox(obj.TileLocation).Intersects(position)) + { + __result = true; + return false; + } + } + return true; + } + } + [HarmonyPatch(typeof(Object), nameof(Object.placementAction))] + public class placementAction_Patch + { + public static void Postfix(Object __instance, GameLocation location, int x, int y, bool __result) + { + if (!Config.EnableMod || !__result || !SHelper.Input.IsDown(Config.ModKey) || !customSignTypeDict.ContainsKey(__instance.Name)) + return; + Vector2 placementTile = new Vector2(x / 64, y / 64); + if (!location.objects.TryGetValue(placementTile, out Object obj)) + return; + placedSign = obj; + ReloadSignData(); + OpenPlacementDialogue(); + } + } + [HarmonyPatch(typeof(Object), nameof(Object.getBoundingBox))] + public class getBoundingBox_Patch + { + public static void Postfix(Object __instance, Vector2 tileLocation, ref Rectangle __result, NetRectangle ___boundingBox) + { + if (!Config.EnableMod || !customSignTypeDict.ContainsKey(__instance.Name) || !__instance.modData.TryGetValue(templateKey, out string template) || !customSignDataDict.TryGetValue(template, out CustomSignData data)) + return; + var x = Environment.StackTrace; + __result = new Rectangle((int)tileLocation.X * 64 + 32 / 2 - data.tileWidth * 64 / 2, (int)tileLocation.Y * 64 + 64 - data.tileHeight * 64, data.tileWidth * 64, data.tileHeight * 64); + ___boundingBox.Set(__result); + } + } + [HarmonyPatch(typeof(Pickaxe), nameof(Pickaxe.DoFunction))] + public class Pickaxe_DoFunction_Patch + { + public static void Postfix(Pickaxe __instance, GameLocation location, int x, int y, int power, Farmer who) + { + if (!Config.EnableMod) + return; + foreach(var kvp in location.objects.Pairs) + { + if(kvp.Value.boundingBox.Value.Contains(x, y) && customSignTypeDict.ContainsKey(kvp.Value.Name) && kvp.Value.modData.TryGetValue(templateKey, out string template) && customSignDataDict.ContainsKey(template)) + { + if (kvp.Value.performToolAction(__instance, location)) + { + kvp.Value.performRemoveAction(kvp.Key, location); + Game1.currentLocation.debris.Add(new Debris(kvp.Value.bigCraftable.Value ? (-kvp.Value.ParentSheetIndex) : kvp.Value.ParentSheetIndex, who.GetToolLocation(false), new Vector2(who.GetBoundingBox().Center.X, who.GetBoundingBox().Center.Y))); + Game1.currentLocation.Objects.Remove(kvp.Key); + return; + } + + } + } + } + } + [HarmonyPatch(typeof(Object), nameof(Object.draw), new Type[] { typeof(SpriteBatch), typeof(int), typeof(int), typeof(float) })] + public class draw_Patch + { + + public static bool Prefix(Object __instance, SpriteBatch spriteBatch, int x, int y, float alpha) + { + if (!Config.EnableMod || __instance.isTemporarilyInvisible || !__instance.bigCraftable.Value || !customSignTypeDict.ContainsKey(__instance.Name) || !__instance.modData.TryGetValue(templateKey, out string template) || !customSignDataDict.TryGetValue(template, out CustomSignData data) || data.texture == null) + return true; + Vector2 position = Game1.GlobalToLocal(Game1.viewport, new Vector2(x * 64 + 32, y * 64 + 64)) - new Vector2(data.texture.Width / 2, data.texture.Height) * data.scale; + float draw_layer = Math.Max(0f, ((y + 1) * 64 - 24) / 10000f) + x * 1E-05f; + spriteBatch.Draw(data.texture, position, null, Color.White * alpha, 0f, Vector2.Zero, data.scale, SpriteEffects.None, draw_layer); + if(data.text != null) + { + for(int i = 0; i < data.text.Length; i++) + { + var text = data.text[i]; + if (!fontDict.ContainsKey(text.fontPath)) + continue; + Vector2 pos; + if (text.center) + { + pos = new Vector2(position.X + text.X - fontDict[text.fontPath].MeasureString(text.text).X / 2 * text.scale, position.Y + text.Y); + } + else + { + pos = new Vector2(position.X + text.X, position.Y + text.Y); + } + spriteBatch.DrawString(fontDict[text.fontPath], text.text, pos, text.color, 0, Vector2.Zero, text.scale, SpriteEffects.None, draw_layer + 1 / 10000f * (i+1)); + } + } + return false; + } + } + } +} \ No newline at end of file diff --git a/CustomSigns/CustomSignData.cs b/CustomSigns/CustomSignData.cs new file mode 100644 index 00000000..36bc97bf --- /dev/null +++ b/CustomSigns/CustomSignData.cs @@ -0,0 +1,30 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace CustomSigns +{ + public class CustomSignData + { + public string texturePath; + public string packID; + public int tileWidth; + public int tileHeight; + public int heldObjectX; + public int heldObjectY; + public float scale = 4; + public string[] types; + public SignText[] text; + public Texture2D texture; + } + + public class SignText + { + public string text; + public int X; + public int Y; + public bool center = true; + public string fontPath; + public float scale; + public Color color = Color.White; + } +} \ No newline at end of file diff --git a/CustomSigns/CustomSigns.csproj b/CustomSigns/CustomSigns.csproj new file mode 100644 index 00000000..a1d778d8 --- /dev/null +++ b/CustomSigns/CustomSigns.csproj @@ -0,0 +1,25 @@ + + + 1.0.0 + net5.0 + true + + + + + + + Always + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/CustomSigns/IGenericModConfigMenuApi.cs b/CustomSigns/IGenericModConfigMenuApi.cs new file mode 100644 index 00000000..2b5d47d2 --- /dev/null +++ b/CustomSigns/IGenericModConfigMenuApi.cs @@ -0,0 +1,75 @@ +using StardewModdingAPI; +using System; + +namespace CustomSigns +{ + /// 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/CustomSigns/Methods.cs b/CustomSigns/Methods.cs new file mode 100644 index 00000000..12662495 --- /dev/null +++ b/CustomSigns/Methods.cs @@ -0,0 +1,71 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +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 CustomSigns +{ + public partial class ModEntry : Mod + { + public static void OpenPlacementDialogue() + { + if (customSignDataDict.Count == 0) + { + SMonitor.Log("No custom sign templates.", LogLevel.Warn); + return; + } + + List responses = new List(); + foreach(var key in customSignDataDict.Keys) + { + responses.Add(new Response(key, key)); + } + responses.Add(new Response("cancel", SHelper.Translation.Get("cancel"))); + Game1.player.currentLocation.createQuestionDialogue(SHelper.Translation.Get("which-template"), responses.ToArray(), "CS_Choose_Template"); + } + + private static void ReloadSignData() + { + customSignDataDict.Clear(); + customSignTypeDict.Clear(); + fontDict.Clear(); + var dict = SHelper.Content.Load>(dictPath, ContentSource.GameContent); + if (dict == null) + { + SMonitor.Log($"No custom signs found", LogLevel.Debug); + return; + } + foreach (var kvp in dict) + { + CustomSignData data = kvp.Value; + foreach (string type in data.types) + { + if (!customSignTypeDict.ContainsKey(type)) + { + customSignTypeDict.Add(type, new List() { type }); + } + else + { + customSignTypeDict[type].Add(type); + } + } + if (data.packID != null && !loadedContentPacks.Contains(data.packID)) + loadedContentPacks.Add(data.packID); + data.texture = SHelper.Content.Load(data.texturePath, ContentSource.GameContent); + foreach(var text in data.text) + { + if (!fontDict.ContainsKey(text.fontPath)) + fontDict.Add(text.fontPath, Game1.content.Load(text.fontPath)); + } + } + customSignDataDict = dict; + } + } +} \ No newline at end of file diff --git a/CustomSigns/ModConfig.cs b/CustomSigns/ModConfig.cs new file mode 100644 index 00000000..6d9e623f --- /dev/null +++ b/CustomSigns/ModConfig.cs @@ -0,0 +1,12 @@ + +using StardewModdingAPI; + +namespace CustomSigns +{ + public class ModConfig + { + public bool EnableMod { get; set; } = true; + public SButton ModKey { get; set; } = SButton.LeftShift; + public SButton ResetKey { get; set; } = SButton.F5; + } +} diff --git a/CustomSigns/ModEntry.cs b/CustomSigns/ModEntry.cs new file mode 100644 index 00000000..20868bba --- /dev/null +++ b/CustomSigns/ModEntry.cs @@ -0,0 +1,138 @@ +using Force.DeepCloner; +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Menus; +using StardewValley.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using Rectangle = Microsoft.Xna.Framework.Rectangle; + +namespace CustomSigns +{ + /// The mod entry point. + public partial class ModEntry : Mod, IAssetLoader + { + + public static IMonitor SMonitor; + public static IModHelper SHelper; + public static ModConfig Config; + public static ModEntry context; + + private static List loadedContentPacks = new List(); + private static Dictionary customSignDataDict = new Dictionary(); + private static Dictionary> customSignTypeDict = new Dictionary>(); + private static Dictionary fontDict = new Dictionary(); + public static readonly string templateKey = "aedenthorn.CustomSigns/template"; + public static readonly string dictPath = "aedenthorn.CustomSigns/dictionary"; + + /// 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.SaveLoaded += GameLoop_SaveLoaded; + helper.Events.Input.ButtonPressed += Input_ButtonPressed; + var harmony = new Harmony(ModManifest.UniqueID); + harmony.PatchAll(); + } + + private void GameLoop_SaveLoaded(object sender, SaveLoadedEventArgs e) + { + ReloadSignData(); + } + + + private void GameLoop_GameLaunched(object sender, 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: () => "Modifier Key", + getValue: () => Config.ModKey, + setValue: value => Config.ModKey = value + ); + } + + private void Input_ButtonPressed(object sender, ButtonPressedEventArgs e) + { + if (!Config.EnableMod) + return; + if (placedSign != null && Game1.activeClickableMenu != null && Game1.player?.currentLocation?.lastQuestionKey?.Equals("CS_Choose_Template") == true) + { + + IClickableMenu menu = Game1.activeClickableMenu; + if (menu == null || menu.GetType() != typeof(DialogueBox)) + return; + + DialogueBox db = menu as DialogueBox; + int resp = db.selectedResponse; + List resps = db.responses; + + if (resp < 0 || resps == null || resp >= resps.Count || resps[resp] == null || resps[resp].responseKey == "cancel") + return; + Monitor.Log($"Answered {Game1.player.currentLocation.lastQuestionKey} with {resps[resp].responseKey}"); + + placedSign.modData[templateKey] = resps[resp].responseKey; + placedSign = null; + } + else if (Helper.Input.IsDown(Config.ModKey) && e.Button == Config.ResetKey) + { + foreach(var pack in loadedContentPacks) + { + Helper.ConsoleCommands.Trigger("patch", new string[] { "reload", pack }); + } + ReloadSignData(); + Helper.Input.Suppress(Config.ResetKey); + } + } + + /// Get whether this instance can load the initial version of the given asset. + /// Basic metadata about the asset being loaded. + public bool CanLoad(IAssetInfo asset) + { + if (!Config.EnableMod) + return false; + + return asset.AssetNameEquals(dictPath); + } + + /// Load a matched asset. + /// Basic metadata about the asset being loaded. + public T Load(IAssetInfo asset) + { + Monitor.Log("Loading dictionary"); + + return (T)(object)new Dictionary(); + } + } +} \ No newline at end of file diff --git a/CustomSigns/i18n/default.json b/CustomSigns/i18n/default.json new file mode 100644 index 00000000..7751bd9f --- /dev/null +++ b/CustomSigns/i18n/default.json @@ -0,0 +1,4 @@ +{ + "which-template": "Assign Template:", + "cancel": "Cancel" +} \ No newline at end of file diff --git a/CustomSigns/manifest.json b/CustomSigns/manifest.json new file mode 100644 index 00000000..1c5651f6 --- /dev/null +++ b/CustomSigns/manifest.json @@ -0,0 +1,22 @@ +{ + "Name": "Custom Signs", + "Author": "aedenthorn", + "Version": "0.1.0", + "Description": "Custom Signs.", + "UniqueID": "aedenthorn.CustomSigns", + "EntryDll": "CustomSigns.dll", + "MinimumApiVersion": "3.13.0", + "ModUpdater": { + "Repository": "StardewValleyMods", + "User": "aedenthorn", + "Directory": "_releases", + "ModFolder": "CustomSigns" + }, + "UpdateKeys": [ "Nexus:11336" ], + "Dependencies": [ + { + "UniqueID": "Platonymous.ModUpdater", + "IsRequired": false + } + ] +} \ No newline at end of file diff --git a/NewProject/CodePatches.cs b/NewProject/CodePatches.cs new file mode 100644 index 00000000..a1a2eec1 --- /dev/null +++ b/NewProject/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/NewProject/CustomSigns.csproj b/NewProject/CustomSigns.csproj new file mode 100644 index 00000000..dc882f9d --- /dev/null +++ b/NewProject/CustomSigns.csproj @@ -0,0 +1,17 @@ + + + 1.0.0 + net5.0 + true + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/NewProject/IGenericModConfigMenuApi.cs b/NewProject/IGenericModConfigMenuApi.cs new file mode 100644 index 00000000..fc93c00e --- /dev/null +++ b/NewProject/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/NewProject/Methods.cs b/NewProject/Methods.cs new file mode 100644 index 00000000..b1c13c8b --- /dev/null +++ b/NewProject/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/NewProject/ModConfig.cs b/NewProject/ModConfig.cs new file mode 100644 index 00000000..93844be4 --- /dev/null +++ b/NewProject/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/NewProject/ModEntry.cs b/NewProject/ModEntry.cs new file mode 100644 index 00000000..e224b8dc --- /dev/null +++ b/NewProject/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/NewProject/manifest.json b/NewProject/manifest.json new file mode 100644 index 00000000..90375821 --- /dev/null +++ b/NewProject/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/StardewValleyMods.sln b/StardewValleyMods.sln index c772e1e0..65b588c3 100644 --- a/StardewValleyMods.sln +++ b/StardewValleyMods.sln @@ -79,7 +79,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Murdercrows", "Murdercrows\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomSpousePatio", "CustomSpousePatio\CustomSpousePatio.csproj", "{C6719EE7-0B8B-4C05-A6D5-BA94DAA8C032}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomResourceClumps", "CustomResourceClumps\CustomResourceClumps.csproj", "{5BE0FDBC-2CCF-406D-B690-B15F15302438}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomResourceClumps", "CustomResourceClumps\CustomResourceClumps.csproj", "{5BE0FDBC-2CCF-406D-B690-B15F15302438}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GemIsles", "GemIsles\GemIsles.csproj", "{C1507CF8-507E-475E-8387-9E1E5D148BB1}" EndProject @@ -266,6 +266,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomToolbar", "CustomTool EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Email", "Email\Email.csproj", "{9C00F8E7-2094-454F-8F95-955E9EFBDA3C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomSigns", "CustomSigns\CustomSigns.csproj", "{B370CE30-5831-487F-8B20-1343161705E5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WallPlanter", "WallPlanter\WallPlanter.csproj", "{2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution _releases\_releases.projitems*{047040e0-3d07-46e5-852b-d1d45cd57bb8}*SharedItemsImports = 13 @@ -1301,6 +1305,22 @@ Global {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 + {B370CE30-5831-487F-8B20-1343161705E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Debug|x86.Build.0 = Debug|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Release|Any CPU.Build.0 = Release|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Release|x86.ActiveCfg = Release|Any CPU + {B370CE30-5831-487F-8B20-1343161705E5}.Release|x86.Build.0 = Release|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Debug|x86.Build.0 = Debug|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Release|Any CPU.Build.0 = Release|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Release|x86.ActiveCfg = Release|Any CPU + {2B4DD4DB-22E9-4448-87AD-8A1384FF4CF4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WallPlanter/CodePatches.cs b/WallPlanter/CodePatches.cs new file mode 100644 index 00000000..44e4b255 --- /dev/null +++ b/WallPlanter/CodePatches.cs @@ -0,0 +1,160 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Netcode; +using StardewValley; +using StardewValley.Locations; +using StardewValley.Objects; +using StardewValley.TerrainFeatures; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Object = StardewValley.Object; + +namespace WallPlanter +{ + public partial class ModEntry + { + public static bool drawingWallPot; + public static int drawingWallPotOffset; + public static int drawingWallPotInnerOffset; + [HarmonyPatch(typeof(Utility), nameof(Utility.playerCanPlaceItemHere))] + public class playerCanPlaceItemHere_Patch + { + public static bool Prefix(GameLocation location, Item item, int x, int y, Farmer f, ref bool __result) + { + if (!Config.EnableMod || item is not Object || !(item as Object).bigCraftable.Value || item.ParentSheetIndex != 62 || !typeof(DecoratableLocation).IsAssignableFrom(location.GetType()) || !(location as DecoratableLocation).isTileOnWall(x / 64, y / 64) || !Utility.isWithinTileWithLeeway(x, y, item, f)) + return true; + SMonitor.Log($"Placing planter on wall"); + __result = true; + return false; + } + } + [HarmonyPatch(typeof(IndoorPot), nameof(IndoorPot.draw), new Type[] { typeof(SpriteBatch), typeof(int), typeof(int), typeof(float) })] + public class IndoorPot_draw_Patch + { + public static void Prefix(IndoorPot __instance, int x, int y) + { + if (!Config.EnableMod || !typeof(DecoratableLocation).IsAssignableFrom(Game1.currentLocation.GetType()) || !(Game1.currentLocation as DecoratableLocation).isTileOnWall(x, y)) + return; + if (!__instance.modData.TryGetValue("aedenthorn.WallPlanter/offset", out string offsetString)) + { + __instance.modData["aedenthorn.WallPlanter/offset"] = Config.OffsetY + ""; + } + if (!__instance.modData.TryGetValue("aedenthorn.WallPlanter/innerOffset", out string innerOffsetString)) + { + __instance.modData["aedenthorn.WallPlanter/innerOffset"] = Config.InnerOffsetY + ""; + } + drawingWallPotOffset = int.Parse(__instance.modData["aedenthorn.WallPlanter/offset"]); + drawingWallPotInnerOffset = int.Parse(__instance.modData["aedenthorn.WallPlanter/innerOffset"]); + drawingWallPot = true; + } + public static IEnumerable Transpiler(IEnumerable instructions) + { + SMonitor.Log($"Transpiling IndoorPot.draw"); + bool found1 = false; + bool found2 = false; + bool found3 = false; + bool found4 = false; + bool found5 = false; + var codes = new List(instructions); + for (int i = 0; i < codes.Count; i++) + { + if (!found1 && codes[i].opcode == OpCodes.Callvirt && (MethodInfo)codes[i].operand == AccessTools.Method(typeof(SpriteBatch), nameof(SpriteBatch.Draw), new Type[] { typeof(Texture2D), typeof(Rectangle), typeof(Rectangle?), typeof(Color), typeof(float), typeof(Vector2), typeof(SpriteEffects), typeof(float) })) + { + SMonitor.Log("replacing first draw method"); + codes[i].opcode = OpCodes.Call; + codes[i].operand = AccessTools.Method(typeof(ModEntry), nameof(ModEntry.DrawIndoorPot)); + codes.Insert(i, new CodeInstruction(OpCodes.Ldarg_0)); + found1 = true; + } + if (!found2 && codes[i].opcode == OpCodes.Callvirt && (MethodInfo)codes[i].operand == AccessTools.Method(typeof(SpriteBatch), nameof(SpriteBatch.Draw), new Type[] { typeof(Texture2D), typeof(Vector2), typeof(Rectangle?), typeof(Color), typeof(float), typeof(Vector2), typeof(float), typeof(SpriteEffects), typeof(float) })) + { + SMonitor.Log("replacing second draw method"); + codes[i].opcode = OpCodes.Call; + codes[i].operand = AccessTools.Method(typeof(ModEntry), nameof(ModEntry.DrawIndoorPotFertilizer)); + found2 = true; + } + if (!found3 && codes[i].opcode == OpCodes.Callvirt && (MethodInfo)codes[i].operand == AccessTools.Method(typeof(Crop), nameof(Crop.drawWithOffset))) + { + SMonitor.Log("replacing third draw method"); + codes[i].opcode = OpCodes.Call; + codes[i].operand = AccessTools.Method(typeof(ModEntry), nameof(ModEntry.DrawIndoorPotCrop)); + found3 = true; + } + if (!found4 && codes[i].opcode == OpCodes.Callvirt && (MethodInfo)codes[i].operand == AccessTools.Method(typeof(Object), nameof(Object.draw), new Type[] { typeof(SpriteBatch), typeof(int), typeof(int), typeof(float), typeof(float) })) + { + SMonitor.Log("replacing fourth draw method"); + codes[i].opcode = OpCodes.Call; + codes[i].operand = AccessTools.Method(typeof(ModEntry), nameof(ModEntry.DrawIndoorPotObject)); + found4 = true; + } + if (!found5 && codes[i].opcode == OpCodes.Callvirt && (MethodInfo)codes[i].operand == AccessTools.Method(typeof(Bush), nameof(Bush.draw), new Type[] { typeof(SpriteBatch), typeof(Vector2), typeof(float) })) + { + SMonitor.Log("replacing fifth draw method"); + codes[i].opcode = OpCodes.Call; + codes[i].operand = AccessTools.Method(typeof(ModEntry), nameof(ModEntry.DrawIndoorPotBush)); + found5 = true; + } + if (found1 && found2 && found3 && found4 && found5) + break; + } + + return codes.AsEnumerable(); + } + public static void Postfix() + { + drawingWallPot = false; + } + } + + private static void DrawIndoorPot(SpriteBatch spriteBatch, Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth, IndoorPot pot) + { + int x = (destinationRectangle.X + Game1.viewport.X) / 64; + int y = (destinationRectangle.Y + Game1.viewport.Y) / 64 + 1; + if (!Config.EnableMod || !drawingWallPot) + { + spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color, rotation, origin, effects, layerDepth); + } + else + { + destinationRectangle = new Rectangle(destinationRectangle.Location - new Point(0, drawingWallPotOffset), destinationRectangle.Size); + spriteBatch.Draw(pot.showNextIndex.Value ? wallPotTextureWet : (pot.bush.Value != null && pot.hoeDirt.Value.state.Value == 1 ? wallPotTextureWet : wallPotTexture), destinationRectangle, null, color, rotation, origin, effects, layerDepth); + } + } + private static void DrawIndoorPotFertilizer(SpriteBatch spriteBatch, Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) + { + if (drawingWallPot) + { + position -= new Vector2(0, drawingWallPotOffset + drawingWallPotInnerOffset); + } + spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth); + } + private static void DrawIndoorPotCrop(Crop crop, SpriteBatch spriteBatch, Vector2 tileLocation, Color color, float rotation, Vector2 offset) + { + if (drawingWallPot) + { + offset -= new Vector2(0, drawingWallPotOffset + drawingWallPotInnerOffset); + } + crop.drawWithOffset(spriteBatch, tileLocation, color, rotation, offset); + } + private static void DrawIndoorPotObject(Object obj, SpriteBatch spriteBatch, int xNonTile, int yNonTile, float layerDepth, float alpha) + { + if (drawingWallPot) + { + yNonTile -= drawingWallPotOffset + drawingWallPotInnerOffset; + } + obj.draw(spriteBatch, xNonTile, yNonTile, layerDepth, alpha); + } + private static void DrawIndoorPotBush(Bush bush, SpriteBatch spriteBatch, Vector2 tileLocation, float yDrawOffset) + { + if (drawingWallPot) + { + yDrawOffset -= drawingWallPotOffset + drawingWallPotInnerOffset; + } + bush.draw(spriteBatch, tileLocation, yDrawOffset); + } + } +} \ No newline at end of file diff --git a/WallPlanter/IGenericModConfigMenuApi.cs b/WallPlanter/IGenericModConfigMenuApi.cs new file mode 100644 index 00000000..d92e7d55 --- /dev/null +++ b/WallPlanter/IGenericModConfigMenuApi.cs @@ -0,0 +1,75 @@ +using StardewModdingAPI; +using System; + +namespace WallPlanter +{ + /// 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/WallPlanter/Methods.cs b/WallPlanter/Methods.cs new file mode 100644 index 00000000..0f7e86e9 --- /dev/null +++ b/WallPlanter/Methods.cs @@ -0,0 +1,18 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +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 WallPlanter +{ + public partial class ModEntry : Mod + { + } +} \ No newline at end of file diff --git a/WallPlanter/ModConfig.cs b/WallPlanter/ModConfig.cs new file mode 100644 index 00000000..8264535e --- /dev/null +++ b/WallPlanter/ModConfig.cs @@ -0,0 +1,15 @@ + +using StardewModdingAPI; + +namespace WallPlanter +{ + public class ModConfig + { + public bool EnableMod { get; set; } = true; + public SButton ModKey { get; set; } = SButton.LeftShift; + public SButton UpButton { get; set; } = SButton.Up; + public SButton DownButton { get; set; } = SButton.Down; + public int OffsetY { get; set; } = 64; + public int InnerOffsetY { get; set; } = 0; + } +} diff --git a/WallPlanter/ModEntry.cs b/WallPlanter/ModEntry.cs new file mode 100644 index 00000000..40f0458a --- /dev/null +++ b/WallPlanter/ModEntry.cs @@ -0,0 +1,124 @@ +using Force.DeepCloner; +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using Rectangle = Microsoft.Xna.Framework.Rectangle; +using Object = StardewValley.Object; + +namespace WallPlanter +{ + /// 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 List loadedContentPacks = new List(); + private static Texture2D wallPotTexture; + private static Texture2D wallPotTextureWet; + + /// 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; + var harmony = new Harmony(ModManifest.UniqueID); + harmony.PatchAll(); + } + + private void GameLoop_UpdateTicking(object sender, UpdateTickingEventArgs e) + { + if (!Config.EnableMod) + return; + int delta = Helper.Input.IsDown(Config.UpButton) ? 1 : (Helper.Input.IsDown(Config.DownButton) ? -1 : 0); + if (delta != 0 && typeof(DecoratableLocation).IsAssignableFrom(Game1.currentLocation.GetType()) && Game1.currentLocation.objects.TryGetValue(Game1.currentCursorTile, out Object obj) && obj is IndoorPot && (Game1.currentLocation as DecoratableLocation).isTileOnWall((int)obj.TileLocation.X, (int)obj.TileLocation.Y)) + { + int offset = Config.OffsetY; + string key = Helper.Input.IsDown(Config.ModKey) ? "aedenthorn.WallPlanter/innerOffset" : "aedenthorn.WallPlanter/offset"; + if (obj.modData.TryGetValue(key, out string offsetString)) + { + int.TryParse(offsetString, out offset); + } + obj.modData[key] = (offset + delta) + ""; + } + } + + private void Input_ButtonPressed(object sender, ButtonPressedEventArgs e) + { + + } + + private void GameLoop_GameLaunched(object sender, GameLaunchedEventArgs e) + { + wallPotTexture = Helper.Content.Load("assets/wall_pot.png"); + wallPotTextureWet = Helper.Content.Load("assets/wall_pot_wet.png"); + // 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: () => "Mod Button", + tooltip: () => "When held down, the up and down buttons move the contents instead", + getValue: () => Config.UpButton, + setValue: value => Config.UpButton = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "Up Button", + tooltip: () => "Moves the pot up when held down while hovering over it", + getValue: () => Config.UpButton, + setValue: value => Config.UpButton = value + ); + configMenu.AddKeybind( + mod: ModManifest, + name: () => "Down Button", + tooltip: () => "Moves the pot down when held down while hovering over it", + getValue: () => Config.ModKey, + setValue: value => Config.ModKey = value + ); + configMenu.AddNumberOption( + mod: ModManifest, + name: () => "Y Offset", + tooltip: () => "Default wall position offset (positive is up)", + getValue: () => Config.OffsetY, + setValue: value => Config.OffsetY = value + ); + } + } +} \ No newline at end of file diff --git a/WallPlanter/WallPlanter.csproj b/WallPlanter/WallPlanter.csproj new file mode 100644 index 00000000..4466e9a4 --- /dev/null +++ b/WallPlanter/WallPlanter.csproj @@ -0,0 +1,31 @@ + + + 1.0.0 + net5.0 + true + + + + + + + Always + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + \ No newline at end of file diff --git a/WallPlanter/assets/wall_pot.png b/WallPlanter/assets/wall_pot.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d10ce948254cb23ac94d8f7844a7a239e03dda GIT binary patch literal 606 zcmV-k0-^nhP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00G8HL_t(Y$L*9oZ`x24h95gt z;t+7b0ukY&mZ6fT4(Zr`(99VsrCa|{_l`&%GPhFuADW?Juu@gvrX>&@gE1ct7~H{F zIE6SL>Qv=SzW4Zj?z!)486N7P{)-Z#hkaGJlklS*ECF{F$05?bD)9E;5df9ENV$?t z6rI+4)UA2AIC798;c-*NthG^^Vj`e#Pf^q&dnFqn1e7aTj+^l8sdnSoMMdL#<2U7( z1)z(#Y)EX%$)~UDOYz0Tn_nv)jssw|FW&aFo$d8W35y7W`+i3f5ZpJ$_eW0fZ(@v6 zC@LDmaj3Hf7pJ#Wru)NJHV#Di^mt=NTfx#FIugB+;0Lx8P+n1v^ z8)rDRIK&xE@Wl-I?Fp9eq03ozRe_cr@a|ie9~WS`sp{j)!lyHw+Lg>+@Z|NctHq)$07*qoM6N<$f(W@0NdN!< literal 0 HcmV?d00001 diff --git a/WallPlanter/assets/wall_pot_wet.png b/WallPlanter/assets/wall_pot_wet.png new file mode 100644 index 0000000000000000000000000000000000000000..f0999a9ec1881391f0d47ff91b63a0e550fd6601 GIT binary patch literal 534 zcmV+x0_pvUP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00DkUL_t(Y$L*6lOT$nAg}=mT zA2!;C7Nl6DI+&q2=pS&_*+J0FKjeQXI5;clqUb0(NQWSZMnx-$NkeL1wqA#{(AHAL zQF^xf_|84|TrP|>(nury8%Y4`nk4=LA2>o%)-_4&Y)k`?P0D1l@uAS;U5PvUGZ^f9 zLg|24&l=|^w{YVAcyexWf#tLVfC7-o#yKs#k_rjlt1ubq1qRIpNnPZ7S#WmYu$mhu zGNpdo2)%G*N@Y3iaCYHvFN+|7S!@BYnj0sRo&Qw_9Yz5-z5(6xFjFxoSU+~~V3jf4 zSYTlL(LDHDpklQ9(-Y;P28J8Mw0vwonpgT4^+e#_G10>Vo;6XzYzH0FTj_AI^-=w3 zzAwcmL)f*69@warNGf4wG>LK*j9Q7K8-eXd^RM;F_Sg$vq1(i(G-%2y$w&jcSwj!U znb9PQj$rrN<@OD1KboJdrw(863Q~8n73MadLe^SRt!xdtdk4J4-0S%Z{U*Z-?1llp Y04s8#im1v~KmY&$07*qoM6N<$g2p)CVE_OC literal 0 HcmV?d00001 diff --git a/WallPlanter/i18n/default.json b/WallPlanter/i18n/default.json new file mode 100644 index 00000000..7751bd9f --- /dev/null +++ b/WallPlanter/i18n/default.json @@ -0,0 +1,4 @@ +{ + "which-template": "Assign Template:", + "cancel": "Cancel" +} \ No newline at end of file diff --git a/WallPlanter/manifest.json b/WallPlanter/manifest.json new file mode 100644 index 00000000..d7e13897 --- /dev/null +++ b/WallPlanter/manifest.json @@ -0,0 +1,22 @@ +{ + "Name": "Wall Planter", + "Author": "aedenthorn", + "Version": "0.1.0", + "Description": "Wall Planter.", + "UniqueID": "aedenthorn.WallPlanter", + "EntryDll": "WallPlanter.dll", + "MinimumApiVersion": "3.13.0", + "ModUpdater": { + "Repository": "StardewValleyMods", + "User": "aedenthorn", + "Directory": "_releases", + "ModFolder": "WallPlanter" + }, + "UpdateKeys": [ "Nexus:" ], + "Dependencies": [ + { + "UniqueID": "Platonymous.ModUpdater", + "IsRequired": false + } + ] +} \ No newline at end of file