diff --git a/Automate/Framework/Commands/Summary/GroupContainerStats.cs b/Automate/Framework/Commands/Summary/GroupContainerStats.cs index b41fc5b2f..fbe787766 100644 --- a/Automate/Framework/Commands/Summary/GroupContainerStats.cs +++ b/Automate/Framework/Commands/Summary/GroupContainerStats.cs @@ -28,7 +28,7 @@ internal class GroupContainerStats public int TotalSlots { get; } /// Whether the container is a Junimo chest. - public bool IsJunimoChest { get; } + public HashSet GlobalInventoryChests { get; } = new(StringComparer.OrdinalIgnoreCase); /********* @@ -47,12 +47,11 @@ public GroupContainerStats(string name, AutomateContainerPreference storagePrefe foreach (IContainer container in containers) { - // only track Junimo chests once - if (container.IsJunimoChest) + // only track same global inventory chest once + if (container.IsGlobalChest) { - if (this.IsJunimoChest) + if (this.GlobalInventoryChests.Add(container.GlobalInventoryId)) continue; - this.IsJunimoChest = true; } // track stats diff --git a/Automate/Framework/Commands/Summary/GroupStats.cs b/Automate/Framework/Commands/Summary/GroupStats.cs index 3f11e391c..c648aceee 100644 --- a/Automate/Framework/Commands/Summary/GroupStats.cs +++ b/Automate/Framework/Commands/Summary/GroupStats.cs @@ -35,7 +35,7 @@ public GroupStats(IMachineGroup machineGroup) { this.MachineGroup = machineGroup; - if (machineGroup.IsJunimoGroup) + if (machineGroup.IsGlobalGroup) this.Name = "Distributed group"; else { @@ -43,7 +43,7 @@ public GroupStats(IMachineGroup machineGroup) this.Name = $"Group at ({tile.X}, {tile.Y})"; } - this.IsJunimoGroup = machineGroup.IsJunimoGroup; + this.IsJunimoGroup = machineGroup.IsGlobalGroup; this.Machines = machineGroup.Machines .GroupBy(p => p.MachineTypeID) diff --git a/Automate/Framework/Commands/SummaryCommand.cs b/Automate/Framework/Commands/SummaryCommand.cs index 8e1e7cd4e..941d41d95 100644 --- a/Automate/Framework/Commands/SummaryCommand.cs +++ b/Automate/Framework/Commands/SummaryCommand.cs @@ -189,7 +189,7 @@ public override void Handle(string[] args) .ThenBy(p => p.TileArea.Y) .ToArray(); - IContainer[] junimoChests = chests.Where(p => p.IsJunimoChest).ToArray(); + IContainer[] junimoChests = chests.Where(p => p.IsGlobalChest).ToArray(); return junimoChests.Any() ? junimoChests : chests; // special case: no Junimo chests in this location, but we're still connected somehow. This is most likely a custom connected chest from another mod, so just list all of them. diff --git a/Automate/Framework/JunimoMachineGroup.cs b/Automate/Framework/GlobalMachineGroup.cs similarity index 93% rename from Automate/Framework/JunimoMachineGroup.cs rename to Automate/Framework/GlobalMachineGroup.cs index 7bc9b5909..dd3426264 100644 --- a/Automate/Framework/JunimoMachineGroup.cs +++ b/Automate/Framework/GlobalMachineGroup.cs @@ -9,7 +9,7 @@ namespace Pathoschild.Stardew.Automate.Framework; /// An aggregate collection of machine groups linked by Junimo chests. -internal class JunimoMachineGroup : MachineGroup +internal class GlobalMachineGroup : MachineGroup { /********* ** Fields @@ -38,7 +38,7 @@ internal class JunimoMachineGroup : MachineGroup /// Sort machines by priority. /// Build a storage manager for the given containers. /// Encapsulates monitoring and logging. - public JunimoMachineGroup(Func, IEnumerable> sortMachines, Func buildStorage, IMonitor monitor) + public GlobalMachineGroup(Func, IEnumerable> sortMachines, Func buildStorage, IMonitor monitor) : base( locationKey: null, machines: [], @@ -48,7 +48,7 @@ public JunimoMachineGroup(Func, IEnumerable> sor monitor: monitor ) { - this.IsJunimoGroup = true; + this.IsGlobalGroup = true; this.SortMachines = sortMachines; } @@ -64,12 +64,14 @@ public IEnumerable GetAll() public void Add(IList groups) { this.MachineGroups.AddRange(groups); + this.GlobalContainerKeys.UnionWith(groups.SelectMany(p => p.GlobalContainerKeys)); } /// Remove all machine groups in the collection. public void Clear() { this.MachineGroups.Clear(); + this.GlobalContainerKeys.Clear(); this.StorageManager.SetContainers([]); @@ -94,6 +96,8 @@ public void Rebuild() this.Machines = this.SortMachines(this.MachineGroups.SelectMany(p => p.Machines)).ToArray(); this.Tiles = null; + this.GlobalContainerKeys.Clear(); + this.GlobalContainerKeys.UnionWith(this.MachineGroups.SelectMany(p => p.GlobalContainerKeys)); this.StorageManager.SetContainers(this.Containers); } diff --git a/Automate/Framework/IMachineGroup.cs b/Automate/Framework/IMachineGroup.cs index 187e5492a..05db6ac39 100644 --- a/Automate/Framework/IMachineGroup.cs +++ b/Automate/Framework/IMachineGroup.cs @@ -8,20 +8,23 @@ namespace Pathoschild.Stardew.Automate.Framework; internal interface IMachineGroup { /********* - ** Accessors - *********/ + ** Accessors + *********/ /// The main location containing the group (as formatted by ), unless this is an aggregate machine group. string? LocationKey { get; } - /// The machines in the group. + /// The keys for all containers which are linked to global inventories. + HashSet GlobalContainerKeys { get; } + +/// The machines in the group. IMachine[] Machines { get; } /// The containers in the group. IContainer[] Containers { get; } - /// Whether the machine group is linked to a Junimo chest. + /// Whether the machine group is linked to a global inventory. [MemberNotNullWhen(false, nameof(IMachineGroup.LocationKey))] - bool IsJunimoGroup { get; } + bool IsGlobalGroup { get; } /// Whether the group has the minimum requirements to enable internal automation (i.e., at least one chest and one machine). bool HasInternalAutomation { get; } diff --git a/Automate/Framework/MachineDataForLocation.cs b/Automate/Framework/MachineDataForLocation.cs index 47d5eb806..618d9a054 100644 --- a/Automate/Framework/MachineDataForLocation.cs +++ b/Automate/Framework/MachineDataForLocation.cs @@ -43,7 +43,7 @@ internal record MachineDataForLocation(string LocationKey, IReadOnlyCollectionGet whether the tile area intersects a machine group which meets the minimum requirements for automation (regardless of whether the machines are currently running). /// The tile area to check. - /// This is the normal-chest equivalent of . + /// This is the normal-chest equivalent of . public bool IntersectsAutomatedGroup(Rectangle tileArea) { var activeTiles = this.ActiveTiles; @@ -59,7 +59,7 @@ public bool IntersectsAutomatedGroup(Rectangle tileArea) /// Get whether a tile area contains or is adjacent to a tracked automateable. /// The tile area to check. - /// This is the normal-chest equivalent of . + /// This is the normal-chest equivalent of . public bool ContainsOrAdjacent(Rectangle tileArea) { var activeTiles = this.ActiveTiles; diff --git a/Automate/Framework/MachineGroup.cs b/Automate/Framework/MachineGroup.cs index 7e26f416e..33c45eba4 100644 --- a/Automate/Framework/MachineGroup.cs +++ b/Automate/Framework/MachineGroup.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Microsoft.Xna.Framework; using Pathoschild.Stardew.Common; @@ -59,6 +60,9 @@ internal class MachineGroup : IMachineGroup /// public string? LocationKey { get; } + /// + public HashSet GlobalContainerKeys { get; } = new(StringComparer.OrdinalIgnoreCase); + /// public IMachine[] Machines { get; protected set; } @@ -67,10 +71,10 @@ internal class MachineGroup : IMachineGroup /// [MemberNotNullWhen(false, nameof(IMachineGroup.LocationKey))] - public bool IsJunimoGroup { get; protected set; } + public bool IsGlobalGroup { get; protected set; } /// - public virtual bool HasInternalAutomation => this.IsJunimoGroup || (this.Machines.Length > 0 && this.Containers.Any(p => !p.IsJunimoChest)); + public virtual bool HasInternalAutomation => this.IsGlobalGroup || (this.Machines.Length > 0 && this.Containers.Any(p => !p.IsGlobalChest)); /********* @@ -91,7 +95,15 @@ public MachineGroup(string? locationKey, IEnumerable machines, IEnumer this.Tiles = [.. tiles]; this.Monitor = monitor; - this.IsJunimoGroup = this.Containers.Any(p => p.IsJunimoChest); + foreach (IContainer container in this.Containers) + { + if (container.IsGlobalChest) + { + this.GlobalContainerKeys.Add(container.GlobalInventoryId); + } + } + + this.IsGlobalGroup = this.GlobalContainerKeys.Count > 0; this.StorageManager = buildStorage(this.GetUniqueContainers(this.Containers)); } diff --git a/Automate/Framework/MachineManager.cs b/Automate/Framework/MachineManager.cs index 461742e0f..b942714ca 100644 --- a/Automate/Framework/MachineManager.cs +++ b/Automate/Framework/MachineManager.cs @@ -47,7 +47,7 @@ internal class MachineManager public MachineGroupFactory Factory { get; } /// An aggregate collection of machine groups linked by Junimo chests. - public JunimoMachineGroup JunimoMachineGroup { get; } + public List GlobalMachineGroups { get; } = new(); /********* @@ -67,7 +67,7 @@ public MachineManager(Func config, DataModel data, IAutomationFactory this.Factory = new(this.GetMachineOverride, this.BuildStorage, monitor); this.Factory.Add(defaultFactory); - this.JunimoMachineGroup = new(this.Factory.SortMachines, this.BuildStorage, this.Monitor); + //this.GlobalMachineGroups = new(this.Factory.SortMachines, this.BuildStorage, this.Monitor); } /**** @@ -76,8 +76,9 @@ public MachineManager(Func config, DataModel data, IAutomationFactory /// Get the machine groups in every location. public IEnumerable GetActiveMachineGroups() { - if (this.JunimoMachineGroup.HasInternalAutomation) - yield return this.JunimoMachineGroup; + foreach (IMachineGroup group in this.GlobalMachineGroups) + if (group.HasInternalAutomation) + yield return group; foreach (IMachineGroup group in this.ActiveMachineGroups) yield return group; @@ -92,7 +93,7 @@ public IEnumerable GetForApi(GameLocation location) return this .ActiveMachineGroups .Concat(this.DisabledMachineGroups) - .Concat(this.JunimoMachineGroup.GetAll()) + .Concat(this.GlobalMachineGroups.SelectMany(group => group.GetAll())) .Where(p => p.LocationKey == locationKey); } @@ -141,7 +142,7 @@ public void Clear() this.MachineData.Clear(); this.ActiveMachineGroups = []; this.DisabledMachineGroups = []; - this.JunimoMachineGroup.Clear(); + this.GlobalMachineGroups.Clear(); } /// Clear all registered machines and add all locations to the reload queue. @@ -149,7 +150,8 @@ public void Reset() { this.Clear(); - this.JunimoMachineGroup.Rebuild(); + foreach (GlobalMachineGroup group in this.GlobalMachineGroups) + group.Rebuild(); this.ReloadQueue.AddRange(CommonHelper.GetLocations()); } @@ -213,7 +215,8 @@ private StorageManager BuildStorage(IContainer[] containers) /// The locations which have been removed, and whose machines should be reloaded if they still exist. private void ReloadMachinesIn(ISet locations, ISet removedLocations) { - bool junimoGroupChanged = false; + List globalAdded = []; + HashSet globalChanged = []; bool anyChanged = false; // remove old groups @@ -225,10 +228,13 @@ private void ReloadMachinesIn(ISet locations, ISet r foreach (string locationKey in locationKeys) anyChanged |= this.MachineData.Remove(locationKey); - if (this.JunimoMachineGroup.RemoveLocations(locationKeys)) + foreach (GlobalMachineGroup globalGroup in this.GlobalMachineGroups) { - anyChanged = true; - junimoGroupChanged = true; + if (globalGroup.RemoveLocations(locationKeys)) + { + anyChanged = true; + globalChanged.Add(globalGroup); + } } } @@ -240,31 +246,26 @@ private void ReloadMachinesIn(ISet locations, ISet r // collect new groups List active = []; List disabled = []; - List junimo = []; foreach (IMachineGroup group in this.Factory.GetMachineGroups(location, this.Monitor)) { if (!group.HasInternalAutomation) disabled.Add(group); - else if (group.IsJunimoGroup) - junimo.Add(group); + else if (!group.IsGlobalGroup) + active.Add(group); else - active.Add(group); + { + globalAdded.Add(group); + globalChanged.Add(group); + } } // add groups this.MachineData[locationKey] = new MachineDataForLocation(locationKey, active, disabled); // track change - if (junimo.Any()) - { - this.JunimoMachineGroup.Add(junimo); - junimoGroupChanged = true; - anyChanged = true; - } - else if (active.Any()) - anyChanged = true; + anyChanged |= active.Any(); } // rebuild caches @@ -283,7 +284,60 @@ private void ReloadMachinesIn(ISet locations, ISet r this.DisabledMachineGroups = disabled.ToArray(); } - if (junimoGroupChanged) - this.JunimoMachineGroup.Rebuild(); + if (!globalChanged.Any()) + return; + + // determine distinct groups + List> distinctGlobalGroups = []; + foreach (HashSet groupKeys in globalChanged.Select(p => p.GlobalContainerKeys)) + { + HashSet? existing = distinctGlobalGroups.FirstOrDefault(p => p.Overlaps(groupKeys)); + if (existing != null) + existing.UnionWith(groupKeys); + else + distinctGlobalGroups.Add(groupKeys); + } + + foreach (HashSet groupKeys in distinctGlobalGroups) + { + GlobalMachineGroup? selectedGroup = null; + int total = this.GlobalMachineGroups.Count; + + for (int i = 0; i < total; i++) + { + GlobalMachineGroup globalGroup = this.GlobalMachineGroups[i]; + if (!globalGroup.GlobalContainerKeys.Overlaps(groupKeys)) + continue; + + selectedGroup ??= globalGroup; + if (selectedGroup == globalGroup) + globalChanged.Add(selectedGroup); + + else + { + selectedGroup.Add([.. globalGroup.GetAll()]); + this.GlobalMachineGroups.Remove(globalGroup); + total--; + } + } + + // create new group + if (selectedGroup == null) + { + selectedGroup = new GlobalMachineGroup(this.Factory.SortMachines, this.BuildStorage, this.Monitor); + this.GlobalMachineGroups.Add(selectedGroup); + } + + // add groups to selected + IList groups = [.. globalAdded.Where(p => p.GlobalContainerKeys.Overlaps(groupKeys))]; + if (groups.Any()) + selectedGroup.Add(groups); + } + + // rebuild groups + foreach (GlobalMachineGroup globalGroup in globalChanged.OfType()) + { + globalGroup.Rebuild(); + } } } diff --git a/Automate/Framework/OverlayMenu.cs b/Automate/Framework/OverlayMenu.cs index 7a4d8deca..03b87337f 100644 --- a/Automate/Framework/OverlayMenu.cs +++ b/Automate/Framework/OverlayMenu.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Pathoschild.Stardew.Common; @@ -25,8 +26,8 @@ internal class OverlayMenu : BaseOverlay /// The machine data for the current location. private readonly MachineDataForLocation? MachineData; - /// The machine group for machines connected to Junimo chests. - private readonly JunimoMachineGroup JunimoGroup; + /// The machine groups for machines connected to global inventories. + private readonly List GlobalGroups; /********* @@ -38,13 +39,13 @@ internal class OverlayMenu : BaseOverlay /// Simplifies access to private code. /// The unique key for the current location. /// The machine groups to display. - /// The machine group for machines connected to Junimo chests. - public OverlayMenu(IModEvents events, IInputHelper inputHelper, IReflectionHelper reflection, string locationKey, MachineDataForLocation? machineData, JunimoMachineGroup junimoGroup) + /// The machine group for machines connected to global inventories. + public OverlayMenu(IModEvents events, IInputHelper inputHelper, IReflectionHelper reflection, string locationKey, MachineDataForLocation? machineData, List globalGroups) : base(events, inputHelper, reflection) { this.LocationKey = locationKey; this.MachineData = machineData; - this.JunimoGroup = junimoGroup; + this.GlobalGroups = globalGroups; } @@ -59,7 +60,7 @@ protected override void DrawWorld(SpriteBatch spriteBatch) return; // draw each tile - IReadOnlySet junimoChestTiles = this.JunimoGroup.GetTiles(this.LocationKey); + IReadOnlySet globalInventoryTiles = new HashSet(this.GlobalGroups.SelectMany(p => p.GetTiles(this.LocationKey))); foreach (Vector2 tile in TileHelper.GetVisibleTiles(expand: 1)) { // get tile's screen coordinates @@ -70,12 +71,12 @@ protected override void DrawWorld(SpriteBatch spriteBatch) // get machine group IMachineGroup? group = null; Color? color = null; - if (junimoChestTiles.Contains(tile)) + if (globalInventoryTiles.Contains(tile)) { - color = this.JunimoGroup.HasInternalAutomation + group = this.GlobalGroups.FirstOrDefault(p => p.GetTiles(this.LocationKey).Contains(tile)); + color = group?.HasInternalAutomation == true ? Color.Green * 0.2f : Color.Red * 0.2f; - group = this.JunimoGroup; } else if (this.MachineData is not null) { diff --git a/Automate/Framework/Storage/ChestContainer.cs b/Automate/Framework/Storage/ChestContainer.cs index ab646cbe1..9280f30d3 100644 --- a/Automate/Framework/Storage/ChestContainer.cs +++ b/Automate/Framework/Storage/ChestContainer.cs @@ -33,11 +33,14 @@ internal class ChestContainer : IContainer /// public string Name => this.Chest.Name; + /// + public string? GlobalInventoryId => this.Chest.GlobalInventoryId ?? (this.Chest.SpecialChestType == Chest.SpecialChestTypes.JunimoChest ? "JunimoChests" : null); + /// public ModDataDictionary ModData => this.Chest.modData; /// - public bool IsJunimoChest => this.Chest.SpecialChestType == Chest.SpecialChestTypes.JunimoChest; + public bool IsGlobalChest => this.Chest.SpecialChestType == Chest.SpecialChestTypes.JunimoChest || this.Chest.GlobalInventoryId != null; /// public bool IsLocked => this.Chest.GetMutex().IsLocked(); diff --git a/Automate/Framework/StorageManager.cs b/Automate/Framework/StorageManager.cs index 8be655787..f91b8c690 100644 --- a/Automate/Framework/StorageManager.cs +++ b/Automate/Framework/StorageManager.cs @@ -38,13 +38,13 @@ public void SetContainers(IEnumerable containers) this.InputContainers = containerCollection .Where(p => p.StorageAllowed()) - .OrderBy(p => p.IsJunimoChest) // push items into Junimo chests last + .OrderBy(p => p.IsGlobalChest) // push items into Junimo chests last .ThenByDescending(p => p.StoragePreferred()) .ToArray(); this.OutputContainers = containerCollection .Where(p => p.TakingItemsAllowed()) - .OrderByDescending(p => p.IsJunimoChest) // take items from Junimo chests first + .OrderByDescending(p => p.IsGlobalChest) // take items from Junimo chests first .ThenByDescending(p => p.TakingItemsPreferred()) .ToArray(); } diff --git a/Automate/IContainer.cs b/Automate/IContainer.cs index 8ee581e08..c319d53fb 100644 --- a/Automate/IContainer.cs +++ b/Automate/IContainer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using StardewValley; using StardewValley.Inventories; using StardewValley.Mods; @@ -15,11 +16,15 @@ public interface IContainer : IAutomatable, IEnumerable /// The container name (if any). string Name { get; } + /// The global inventory id (if any). + string? GlobalInventoryId { get; } + /// The raw mod data for the container. ModDataDictionary ModData { get; } - /// Whether this is a Junimo chest, which shares a global inventory with all other Junimo chests. - bool IsJunimoChest { get; } + /// Whether this is a global inventory chest, which shares an inventory with all other chests with the same Global Inventory Id. + [MemberNotNullWhen(true, nameof(IContainer.GlobalInventoryId))] + bool IsGlobalChest { get; } /// Whether this chest is locked (e.g. because a player has it open). bool IsLocked { get; } diff --git a/Automate/ModEntry.cs b/Automate/ModEntry.cs index 9bfb27ade..2f0477a8d 100644 --- a/Automate/ModEntry.cs +++ b/Automate/ModEntry.cs @@ -430,7 +430,7 @@ private void EnableOverlay() reflection: this.Helper.Reflection, locationKey: this.MachineManager.Factory.GetLocationKey(Game1.currentLocation), machineData: this.MachineManager.GetMachineDataFor(Game1.currentLocation), - junimoGroup: this.MachineManager.JunimoMachineGroup + globalGroups: this.MachineManager.GlobalMachineGroups ); } @@ -453,7 +453,7 @@ private bool ReloadIfNeeded(GameLocation location, IEnumerable globalData = this.MachineManager.GlobalMachineGroups; bool shouldReload = false; foreach ((Rectangle tileArea, TEntity entity, bool isAdded) in entities) @@ -479,7 +479,7 @@ private bool ReloadIfNeeded(GameLocation location, IEnumerable p.ContainsOrAdjacent(locationKey, tileArea)) || (automateable is IContainer ? data.ContainsOrAdjacent(tileArea) : data.IsConnectedToChest(tileArea)); if (shouldReload) @@ -487,7 +487,7 @@ private bool ReloadIfNeeded(GameLocation location, IEnumerable p.IntersectsAutomatedGroup(locationKey, tileArea))) { shouldReload = true; break;