diff --git a/MegaStorage/ISaveAnywhere.cs b/MegaStorage/ISaveAnywhere.cs new file mode 100644 index 0000000..ad3f7ca --- /dev/null +++ b/MegaStorage/ISaveAnywhere.cs @@ -0,0 +1,67 @@ +using System; + +namespace MegaStorage +{ + public interface ISaveAnywhereApi + { + /********* + ** Events + *********/ + /// + /// Event that fires before game save + /// + event EventHandler BeforeSave; + + /// + /// Event that fires after game save + /// + event EventHandler AfterSave; + + /// + /// Event that fires after game load + /// + event EventHandler AfterLoad; + + /// + /// Add in an event that can trigger before saving begins. + /// + /// + /// + void addBeforeSaveEvent(string ID, Action BeforeSave); + + /// + /// Remove an event that can trigger before saving begins. + /// + /// + /// + void removeBeforeSaveEvent(string ID, Action BeforeSave); + + /// + /// Add an event that tiggers after saving has finished. + /// + /// + /// + void addAfterSaveEvent(string ID, Action AfterSave); + + /// + ///Remove an event that triggers after saving has occured. + /// + /// + /// + void removeAfterSaveEvent(string ID, Action AfterSave); + + /// + /// Add in an event that triggers afer loading has occured. + /// + /// + /// + void addAfterLoadEvent(string ID, Action AfterLoad); + + /// + /// Remove an event that occurs after loading has occured. + /// + /// + /// + void removeAfterLoadEvent(string ID, Action AfterLoad); + } +} diff --git a/MegaStorage/ItemPatcher.cs b/MegaStorage/ItemPatcher.cs index 0c202bd..e5b2be9 100644 --- a/MegaStorage/ItemPatcher.cs +++ b/MegaStorage/ItemPatcher.cs @@ -21,6 +21,7 @@ public ItemPatcher(IModHelper modHelper, IMonitor monitor) public void Start() { _modHelper.Events.Player.InventoryChanged += OnInventoryChanged; + _modHelper.Events.World.ChestInventoryChanged += OnChestInventoryChanged; _modHelper.Events.World.ObjectListChanged += OnObjectListChanged; _modHelper.Events.GameLoop.SaveLoaded += OnSaveLoaded; } @@ -60,6 +61,25 @@ private void OnInventoryChanged(object sender, InventoryChangedEventArgs e) Game1.player.Items[index] = addedItem.ToCustomChest(); } + private void OnChestInventoryChanged(object sender, ChestInventoryChangedEventArgs e) + { + _monitor.VerboseLog("OnChestInventoryChanged"); + if (e.Added.Count() != 1) + return; + + var addedItem = e.Added.Single(); + if (addedItem is CustomChest) + return; + + if (!CustomChestFactory.ShouldBeCustomChest(addedItem)) + return; + + _monitor.VerboseLog("OnChestInventoryChanged: converting"); + + var index = Game1.player.Items.IndexOf(addedItem); + Game1.player.Items[index] = addedItem.ToCustomChest(); + } + private void OnObjectListChanged(object sender, ObjectListChangedEventArgs e) { _monitor.VerboseLog("OnObjectListChanged"); diff --git a/MegaStorage/Mapping/AutomationFactory.cs b/MegaStorage/Mapping/AutomationFactory.cs new file mode 100644 index 0000000..23b55d7 --- /dev/null +++ b/MegaStorage/Mapping/AutomationFactory.cs @@ -0,0 +1,59 @@ +using System; +using MegaStorage.Models; +using Microsoft.Xna.Framework; +using Pathoschild.Stardew.Automate; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; +using StardewValley.TerrainFeatures; +using SObject = StardewValley.Object; + +namespace MegaStorage.Mapping +{ + internal class AutomationFactory : IAutomationFactory + { + /// Get a machine, container, or connector instance for a given object. + /// The in-game object. + /// The location to check. + /// The tile position to check. + /// Returns an instance or null. + public IAutomatable GetFor(SObject obj, GameLocation location, in Vector2 tile) + { + if (obj.ParentSheetIndex == ModConfig.Instance.LargeChest.Id || + obj.ParentSheetIndex == ModConfig.Instance.MagicChest.Id) + return new CustomChestContainer(obj, location, tile); + + return null; + } + + /// Get a machine, container, or connector instance for a given terrain feature. + /// The terrain feature. + /// The location to check. + /// The tile position to check. + /// Returns an instance or null. + public IAutomatable GetFor(TerrainFeature feature, GameLocation location, in Vector2 tile) + { + return null; + } + + /// Get a machine, container, or connector instance for a given building. + /// The building. + /// The location to check. + /// The tile position to check. + /// Returns an instance or null. + public IAutomatable GetFor(Building building, BuildableGameLocation location, in Vector2 tile) + { + return null; + } + + /// Get a machine, container, or connector instance for a given tile position. + /// The location to check. + /// The tile position to check. + /// Returns an instance or null. + /// Shipping bin logic from , garbage can logic from . + public IAutomatable GetForTile(GameLocation location, in Vector2 tile) + { + return null; + } + } +} diff --git a/MegaStorage/MegaStorage.csproj b/MegaStorage/MegaStorage.csproj index f12dc23..b424ac4 100644 --- a/MegaStorage/MegaStorage.csproj +++ b/MegaStorage/MegaStorage.csproj @@ -12,13 +12,17 @@ - + + + + + + G:\Steam\steamapps\common\Stardew Valley\Mods\Automate\Automate.dll + false + - - Always - Always diff --git a/MegaStorage/MegaStorageMod.cs b/MegaStorage/MegaStorageMod.cs index b144990..472892f 100644 --- a/MegaStorage/MegaStorageMod.cs +++ b/MegaStorage/MegaStorageMod.cs @@ -1,5 +1,7 @@ -using MegaStorage.Models; +using MegaStorage.Mapping; +using MegaStorage.Models; using MegaStorage.Persistence; +using Pathoschild.Stardew.Automate; using StardewModdingAPI; using StardewModdingAPI.Events; @@ -19,6 +21,7 @@ public override void Entry(IModHelper modHelper) private void OnGameLaunched(object sender, GameLaunchedEventArgs e) { var convenientChestsApi = Helper.ModRegistry.GetApi("aEnigma.ConvenientChests"); + var automateApi = Helper.ModRegistry.GetApi("Pathoschild.Automate"); var spritePatcher = new SpritePatcher(Helper, Monitor); var itemPatcher = new ItemPatcher(Helper, Monitor); @@ -35,7 +38,12 @@ private void OnGameLaunched(object sender, GameLaunchedEventArgs e) itemPatcher.Start(); saveManager.Start(); menuChanger.Start(); + + if (!(convenientChestsApi is null)) + ModConfig.Instance.EnableCategories = false; + + automateApi?.AddFactory(new AutomationFactory()); } } -} +} \ No newline at end of file diff --git a/MegaStorage/MenuChanger.cs b/MegaStorage/MenuChanger.cs index f2368e4..e7bff42 100644 --- a/MegaStorage/MenuChanger.cs +++ b/MegaStorage/MenuChanger.cs @@ -32,6 +32,5 @@ private void OnMenuChanged(object sender, MenuChangedEventArgs e) return; Game1.activeClickableMenu = customChest.GetItemGrabMenu(); } - } -} +} \ No newline at end of file diff --git a/MegaStorage/Models/CustomChestContainer.cs b/MegaStorage/Models/CustomChestContainer.cs new file mode 100644 index 0000000..1e26f9e --- /dev/null +++ b/MegaStorage/Models/CustomChestContainer.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using MegaStorage.Mapping; +using Microsoft.Xna.Framework; +using Pathoschild.Stardew.Automate; +using StardewValley; +using SObject = StardewValley.Object; + +namespace MegaStorage.Models +{ + /// A in-game chest which can provide or store items. + internal class CustomChestContainer : IContainer + { + /********* + ** Fields + *********/ + /// The underlying chest. + private readonly CustomChest _chest; + + + /********* + ** Accessors + *********/ + /// The container name (if any). + public string Name + { + get => this._chest.Name; + private set => this._chest.Name = value; + } + + /// The location which contains the container. + public GameLocation Location { get; } + + /// The tile area covered by the container. + public Rectangle TileArea { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying chest. + /// The location which contains the container. + /// The tile area covered by the container. + public CustomChestContainer(SObject chest, GameLocation location, Vector2 tile) + { + this._chest = chest.ToCustomChest(); + this.Location = location; + this.TileArea = new Rectangle((int)tile.X, (int)tile.Y, 1, 1); + + this.Name = this.MigrateLegacyOptions(this.Name); + } + + /// Store an item stack. + /// The item stack to store. + /// If the storage can't hold the entire stack, it should reduce the tracked stack accordingly. + public void Store(ITrackedStack stack) + { + if (stack.Count <= 0) + return; + + IList inventory = this._chest.items; + + // try stack into existing slot + foreach (var slot in inventory) + { + if (slot == null || !stack.Sample.canStackWith(slot)) continue; + var sample = stack.Sample.getOne(); + sample.Stack = stack.Count; + var added = stack.Count - slot.addToStack(sample); + stack.Reduce(added); + if (stack.Count <= 0) + return; + } + + // try add to empty slot + for (var i = 0; i < _chest.Capacity && i < inventory.Count; i++) + { + if (inventory[i] != null) continue; + inventory[i] = stack.Take(stack.Count); + return; + + } + + // try add new slot + if (inventory.Count < _chest.Capacity) + inventory.Add(stack.Take(stack.Count)); + } + + /// Find items in the pipe matching a predicate. + /// Matches items that should be returned. + /// The number of items to find. + /// If the pipe has no matching item, returns null. Otherwise returns a tracked item stack, which may have less items than requested if no more were found. + public ITrackedStack Get(Func predicate, int count) + { + var stacks = this.GetImpl(predicate, count).ToArray(); + return !stacks.Any() ? null : new TrackedItemCollection(stacks); + } + + /// Returns an enumerator that iterates through the collection. + /// An enumerator that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + foreach (var item in this._chest.items.ToArray()) + { + if (item == null) continue; + ITrackedStack stack; + try + { + stack = this.GetTrackedItem(item); + } + catch (Exception ex) + { + var error = $"Failed to retrieve item #{item.ParentSheetIndex} ('{item.Name}'"; + if (item is SObject obj && obj.preservedParentSheetIndex.Value >= 0) + error += $", preserved item #{obj.preservedParentSheetIndex.Value}"; + error += $") from container '{this._chest.Name}' at {this.Location.Name} (tile: {this.TileArea.X}, {this.TileArea.Y})."; + + throw new InvalidOperationException(error, ex); + } + + if (stack != null) + yield return stack; + } + } + + /// Returns an enumerator that iterates through a collection. + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + + /********* + ** Private methods + *********/ + /// Find items in the pipe matching a predicate. + /// Matches items that should be returned. + /// The number of items to find. + /// If there aren't enough items in the pipe, it should return those it has. + private IEnumerable GetImpl(Func predicate, int count) + { + var countFound = 0; + foreach (var item in this._chest.items.Where(item => item != null && predicate(item))) + { + countFound += item.Stack; + yield return this.GetTrackedItem(item); + if (countFound >= count) + yield break; + } + } + + /// Get a tracked item synced with the chest inventory. + /// The item to track. + private ITrackedStack GetTrackedItem(Item item) + { + return new TrackedItem(item, onEmpty: i => this._chest.items.Remove(i)); + } + + /// Migrate legacy options stored in a chest name. + /// The chest name to migrate. + private string MigrateLegacyOptions(string name) + { + if (string.IsNullOrWhiteSpace(name) || !Regex.IsMatch(name, @"\|automate:(?:ignore|input|output|noinput|nooutput)\|")) + return name; + + // migrate renamed tags + name = name + .Replace("|automate:noinput|", "|automate:no-store|") + .Replace("|automate:output|", "|automate:prefer-store|") + .Replace("|automate:nooutput|", "|automate:no-take|") + .Replace("|automate:input|", "|automate:prefer-take|"); + + // migrate removed tags + if (name.Contains("|automate:ignore|")) + { + var newTag = new[] {"|automate:no-store|", "|automate:no-take|"}.Where(tag => !name.Contains(tag)).Aggregate("", (current, tag) => $"{current} {tag}".Trim()); + name = name.Replace("|automate:ignore|", newTag); + } + + // normalize + name = Regex.Replace(name, @"\| +\|", "| |"); + return name.Trim(); + } + } +} diff --git a/MegaStorage/Models/ModConfig.cs b/MegaStorage/Models/ModConfig.cs index a3305f4..f7eb4e7 100644 --- a/MegaStorage/Models/ModConfig.cs +++ b/MegaStorage/Models/ModConfig.cs @@ -5,11 +5,37 @@ public class ModConfig public CustomChestConfig LargeChest { get; set; } public CustomChestConfig MagicChest { get; set; } + public bool EnableCategories { get; set; } + public ModConfig() { Instance = this; + + LargeChest = new CustomChestConfig() + { + Id = 816, + Name = "Large Chest", + Description = "A large place to store your items.", + Recipe = "388 100 334 5 335 5", + SpritePath = "Sprites/LargeChest.png", + SpriteBWPath = "Sprites/LargeChestBW.png", + SpriteBracesPath = "Sprites/LargeChestBraces.png" + }; + + MagicChest = new CustomChestConfig() + { + Id = 817, + Name = "Magic Chest", + Description = "A magical place to store your items.", + Recipe = "709 100 336 5 337 5 768 50 769 50", + SpritePath = "Sprites/MagicChest.png", + SpriteBWPath = "Sprites/MagicChestBW.png", + SpriteBracesPath = "Sprites/MagicChestBraces.png" + }; + + EnableCategories = true; } public static ModConfig Instance { get; private set; } } -} +} \ No newline at end of file diff --git a/MegaStorage/Persistence/SaveManager.cs b/MegaStorage/Persistence/SaveManager.cs index 62e1dbd..a65406c 100644 --- a/MegaStorage/Persistence/SaveManager.cs +++ b/MegaStorage/Persistence/SaveManager.cs @@ -19,6 +19,15 @@ public SaveManager(IModHelper modHelper, IMonitor monitor, FarmhandMonitor farmh public void Start() { + var saveAnywhereApi = _modHelper.ModRegistry.GetApi("Omegasis.SaveAnywhere"); + + if (!(saveAnywhereApi is null)) + { + saveAnywhereApi.BeforeSave += (sender, args) => HideAndSaveCustomChests(); + saveAnywhereApi.AfterSave += (sender, args) => ReAddCustomChests(); + saveAnywhereApi.AfterLoad += (sender, args) => LoadCustomChests(); + } + _modHelper.Events.GameLoop.SaveLoaded += (sender, args) => LoadCustomChests(); _modHelper.Events.GameLoop.Saving += (sender, args) => HideAndSaveCustomChests(); _modHelper.Events.GameLoop.Saved += (sender, args) => ReAddCustomChests(); diff --git a/MegaStorage/SpritePatcher.cs b/MegaStorage/SpritePatcher.cs index fa32a6f..744a254 100644 --- a/MegaStorage/SpritePatcher.cs +++ b/MegaStorage/SpritePatcher.cs @@ -21,11 +21,15 @@ public SpritePatcher(IModHelper modHelper, IMonitor monitor) _monitor = monitor; } + /// Get whether this instance can edit the given asset. + /// Basic metadata about the asset being loaded. public bool CanEdit(IAssetInfo asset) { return asset.AssetNameEquals("TileSheets/Craftables"); } + /// Edit a matched asset. + /// A helper which encapsulates metadata about an asset and enables changes to it. public void Edit(IAssetData asset) { _monitor.VerboseLog("Type of asset: " + typeof(T)); diff --git a/MegaStorage/UI/AllCategory.cs b/MegaStorage/UI/AllCategory.cs index 86f015a..a6e2ec9 100644 --- a/MegaStorage/UI/AllCategory.cs +++ b/MegaStorage/UI/AllCategory.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using MegaStorage.Models; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -15,7 +16,8 @@ public AllCategory(int index, string name, int x, int y) : base(index, name, new public override void Draw(SpriteBatch b, int x, int y) { - b.Draw(_sprite, new Vector2(x - 72, y + StartY + Index * Height), new Rectangle(0, 0, 16, 16), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1f); + if (ModConfig.Instance.EnableCategories) + b.Draw(_sprite, new Vector2(x - 72, y + StartY + Index * Height), new Rectangle(0, 0, 16, 16), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1f); } protected override bool BelongsToCategory(Item i) diff --git a/MegaStorage/UI/ChestCategory.cs b/MegaStorage/UI/ChestCategory.cs index fe5e79e..760db18 100644 --- a/MegaStorage/UI/ChestCategory.cs +++ b/MegaStorage/UI/ChestCategory.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using MegaStorage.Models; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -28,7 +29,8 @@ public ChestCategory(int index, string name, Vector2 spritePos, int[] categoryId public virtual void Draw(SpriteBatch b, int x, int y) { - b.Draw(Game1.mouseCursors, new Vector2(x - 72, y + StartY + Index * Height), new Rectangle((int)_spritePos.X, (int)_spritePos.Y, 16, 16), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1f); + if (ModConfig.Instance.EnableCategories) + b.Draw(Game1.mouseCursors, new Vector2(x - 72, y + StartY + Index * Height), new Rectangle((int)_spritePos.X, (int)_spritePos.Y, 16, 16), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1f); } public void DrawTooltip(SpriteBatch b) diff --git a/MegaStorage/UI/LargeItemGrabMenu.cs b/MegaStorage/UI/LargeItemGrabMenu.cs index 959bef6..c9d57ce 100644 --- a/MegaStorage/UI/LargeItemGrabMenu.cs +++ b/MegaStorage/UI/LargeItemGrabMenu.cs @@ -475,7 +475,8 @@ public override void draw(SpriteBatch b) public override void performHoverAction(int x, int y) { base.performHoverAction(x, y); - _hoverCategory = _chestCategories.FirstOrDefault(c => c.containsPoint(x, y)); + if (ModConfig.Instance.EnableCategories) + _hoverCategory = _chestCategories.FirstOrDefault(c => c.containsPoint(x, y)); } protected void Draw(SpriteBatch b) diff --git a/MegaStorage/config.json b/MegaStorage/config.json deleted file mode 100644 index ac19e15..0000000 --- a/MegaStorage/config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "LargeChest": { - "Id": 816, - "Name": "Large Chest", - "Description": "A large place to store your items.", - "Recipe": "388 100 334 5 335 5", - "SpritePath": "Sprites/LargeChest.png", - "SpriteBWPath": "Sprites/LargeChestBW.png", - "SpriteBracesPath": "Sprites/LargeChestBraces.png" - }, - "MagicChest": { - "Id": 817, - "Name": "Magic Chest", - "Description": "A magical place to store your items.", - "Recipe": "709 100 336 5 337 5 768 50 769 50", - "SpritePath": "Sprites/MagicChest.png", - "SpriteBWPath": "Sprites/MagicChestBW.png", - "SpriteBracesPath": "Sprites/MagicChestBraces.png" - } -} \ No newline at end of file diff --git a/MegaStorage/manifest.json b/MegaStorage/manifest.json index c903a84..9bcf799 100644 --- a/MegaStorage/manifest.json +++ b/MegaStorage/manifest.json @@ -1,17 +1,27 @@ { - "Name": "Mega Storage", - "Author": "Alek", - "Version": "1.3", - "Description": "Adds Large Chests and Magic Chests to the game.", - "UniqueID": "Alek.MegaStorage", - "EntryDll": "MegaStorage.dll", - "MinimumApiVersion": "3.0.0", - "UpdateKeys": [ "Nexus:4089" ], - "Dependencies": [ - { - "UniqueID": "aEnigma.ConvenientChests", - "MinimumVersion": "1.1.1", - "IsRequired": false - } - ] + "Name": "Mega Storage", + "Author": "Alek and furyx639", + "Version": "1.3.1", + "Description": "Adds Large Chests and Magic Chests to the game.", + "UniqueID": "Alek.MegaStorage", + "EntryDll": "MegaStorage.dll", + "MinimumApiVersion": "3.2.0", + "UpdateKeys": [ "Nexus:4089" ], + "Dependencies": [ + { + "UniqueID": "aEnigma.ConvenientChests", + "MinimumVersion": "1.5.0", + "IsRequired": false + }, + { + "UniqueID": "Pathoschild.Automate", + "MinimumVersion": "1.15.1", + "IsRequired": false + }, + { + "UniqueID": "Omegasis.SaveAnywhere", + "MinimumVersion": "2.12.3", + "IsRequired": false + } + ] } \ No newline at end of file diff --git a/README.md b/README.md index cdf8e10..b946a8b 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Names, descriptions, IDs and recipes for Large Chest and Magic Chest are configu * Requires [SMAPI](https://smapi.io/). * Supports multiplayers and controllers. * Compatible with Chests Anywhere, Stack Everything, Automate, Carry Chest and content packs for Content Patcher and Json Assets. -* Minor incompatibility with Convenient Chests: overlapping UI. -* NOT compatible with Save Anywhere. Using these mods together may result in crashes and loss of items. +* Compatible with Convenient Chests: disables categories when being used with Convenient Chests. +* Minor compatibility with Save Anywhere: after saving you have to return to title and load for chests to appear correctly. # Is this safe? Before saving, all Large Chests and Magic Chests are converted to normal chests. After saving, they are converted back. This makes sure your items aren't lost, even if uninstalling this mod. Normal chests actually have infinite capacity, it's only when adding items one at a time they are limited to 36 capacity. @@ -52,5 +52,6 @@ If you lose any items or custom chests don't convert back from normal chests, pl * Custom sprites by [Revanius](https://www.nexusmods.com/users/40079). * Convenient Chests save fix by [MaienM](https://www.nexusmods.com/stardewvalley/users/6392240). * SDV 1.4 compatibility by Mizzion. +* Updated by [furyx639](https://www.nexusmods.com/stardewvalley/users/1643034). [Nexus page](https://www.nexusmods.com/stardewvalley/mods/4089)