From 6a8460da1b65e8c4b547b1bdcead1c44082bec76 Mon Sep 17 00:00:00 2001 From: geromet Date: Sat, 4 Jul 2026 01:09:52 +0200 Subject: [PATCH] RemoteCrafting optimisation --- .../Harmony/DropBoxToContainers.cs | 14 +- .../Scripts/RemoteCraftingUtils.cs | 362 +++++++++++++++--- 2 files changed, 306 insertions(+), 70 deletions(-) diff --git a/Mods/0-SCore/Features/RemoteCrafting/Harmony/DropBoxToContainers.cs b/Mods/0-SCore/Features/RemoteCrafting/Harmony/DropBoxToContainers.cs index 2e01a50f..e50f6106 100644 --- a/Mods/0-SCore/Features/RemoteCrafting/Harmony/DropBoxToContainers.cs +++ b/Mods/0-SCore/Features/RemoteCrafting/Harmony/DropBoxToContainers.cs @@ -17,13 +17,11 @@ public static bool Prefix(XUiC_LootContainer __instance) { if (__instance.localTileEntity == null) { - Log.Out("[DropBox] OnClose: localTileEntity is null, skipping."); return true; } var composite = __instance.localTileEntity as TEFeatureStorage; if (composite == null) { - Log.Out("[DropBox] OnClose: localTileEntity is not TEFeatureStorage, skipping."); return true; } @@ -34,7 +32,6 @@ public static bool Prefix(XUiC_LootContainer __instance) { StringParsers.TryParseBool(dropBoxStr, out var dropBoxBool) && dropBoxBool; if (block is not BlockDropBoxContainer && !hasDropBoxProperty) { - Log.Out($"[DropBox] OnClose: block at {pos} ({block.GetBlockName()}) is not a drop box, skipping."); return true; } @@ -44,29 +41,26 @@ public static bool Prefix(XUiC_LootContainer __instance) { distance = StringParsers.ParseFloat(strDistance); var primaryPlayer = __instance.xui.playerUI.entityPlayer; - Log.Out($"[DropBox] OnClose: distributing from {block.GetBlockName()} at {pos}, player={primaryPlayer?.EntityName ?? "null"}, distance={distance}, IsServer={SingletonMonoBehaviour.Instance.IsServer}"); + if (primaryPlayer == null) + return true; if (__instance.localTileEntity.items == null) { - Log.Out("[DropBox] OnClose: items array is null, skipping."); return true; } var items = __instance.localTileEntity.items; + var tileEntities = RemoteCraftingUtils.GetTileEntities(primaryPlayer, distance, false); for (var i = 0; i < items.Length; i++) { if (items[i].IsEmpty()) continue; - var itemName = items[i].itemValue?.ItemClass?.GetItemName() ?? "unknown"; - Log.Out($"[DropBox] OnClose: trying to distribute slot {i}: {itemName} x{items[i].count}"); - if (RemoteCraftingUtils.AddToNearbyContainer(primaryPlayer, items[i], distance)) + if (RemoteCraftingUtils.AddToNearbyContainer(primaryPlayer, items[i], tileEntities)) { - Log.Out($"[DropBox] OnClose: distributed {itemName} successfully, clearing slot {i}."); items[i] = ItemStack.Empty.Clone(); continue; } - Log.Out($"[DropBox] OnClose: no container accepted {itemName}, leaving in slot {i}."); __instance.localTileEntity.UpdateSlot(i, items[i]); } diff --git a/Mods/0-SCore/Features/RemoteCrafting/Scripts/RemoteCraftingUtils.cs b/Mods/0-SCore/Features/RemoteCrafting/Scripts/RemoteCraftingUtils.cs index efe300bc..ff999f56 100644 --- a/Mods/0-SCore/Features/RemoteCrafting/Scripts/RemoteCraftingUtils.cs +++ b/Mods/0-SCore/Features/RemoteCrafting/Scripts/RemoteCraftingUtils.cs @@ -12,6 +12,147 @@ namespace SCore.Features.RemoteCrafting.Scripts public class RemoteCraftingUtils { private const string AdvFeatureClass = "AdvancedRecipes"; + private const int NearbyContainerCacheTtlMs = 350; + private const float NearbyContainerCachePositionBucketSize = 1f; + + private static NearbyContainerCacheEntry _nearbyContainerCache; + + private sealed class NearbyContainerCacheEntry + { + public NearbyContainerCacheKey Key { get; set; } + public DateTime ExpiresAtUtc { get; set; } + public List TileEntities { get; set; } + } + + private readonly struct NearbyContainerCacheKey : IEquatable + { + private readonly int _playerId; + private readonly float _distance; + private readonly bool _forRepairs; + private readonly Vector3i _positionBucket; + private readonly string _context; + + public NearbyContainerCacheKey(int playerId, float distance, bool forRepairs, Vector3i positionBucket, string context) + { + _playerId = playerId; + _distance = distance; + _forRepairs = forRepairs; + _positionBucket = positionBucket; + _context = context; + } + + public bool Equals(NearbyContainerCacheKey other) + { + return _playerId == other._playerId && + Math.Abs(_distance - other._distance) < 0.001f && + _forRepairs == other._forRepairs && + _positionBucket.Equals(other._positionBucket) && + _context == other._context; + } + + public override bool Equals(object obj) + { + return obj is NearbyContainerCacheKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = _playerId; + hashCode = (hashCode * 397) ^ _distance.GetHashCode(); + hashCode = (hashCode * 397) ^ _forRepairs.GetHashCode(); + hashCode = (hashCode * 397) ^ _positionBucket.GetHashCode(); + hashCode = (hashCode * 397) ^ (_context != null ? _context.GetHashCode() : 0); + return hashCode; + } + } + } + + private sealed class NearbyContainerScanContext + { + public bool LandClaimContainersOnly { get; set; } + public bool LandClaimPlayerOnly { get; set; } + public string DisabledSenderRaw { get; set; } + public string NotToWorkstationRaw { get; set; } + public string BindToWorkstationRaw { get; set; } + public string CurrentWorkstation { get; set; } + public bool HasLocalWorkstationContext { get; set; } + public bool CanUseNearbyContainerCache { get; set; } + public bool InvertDisable { get; set; } + public bool EnforceBindToWorkstation { get; set; } + public string[] DisabledSenderValues { get; set; } + public IReadOnlyList NotToWorkstationBindings { get; set; } + public IReadOnlyList BindToWorkstationBindings { get; set; } + } + + private readonly struct WorkstationLootBinding + { + public WorkstationLootBinding(string[] workstations, string[] lootLists) + { + Workstations = workstations; + LootLists = lootLists; + } + + public string[] Workstations { get; } + public string[] LootLists { get; } + } + + public static void InvalidateNearbyContainerCache() + { + _nearbyContainerCache = null; + } + + private static bool TryGetNearbyContainerCache(NearbyContainerCacheKey key, out List tileEntities) + { + var cache = _nearbyContainerCache; + if (cache != null && DateTime.UtcNow <= cache.ExpiresAtUtc && cache.Key.Equals(key)) + { + tileEntities = new List(cache.TileEntities); + return true; + } + + tileEntities = null; + return false; + } + + private static void StoreNearbyContainerCache(NearbyContainerCacheKey key, List tileEntities) + { + _nearbyContainerCache = new NearbyContainerCacheEntry + { + Key = key, + ExpiresAtUtc = DateTime.UtcNow.AddMilliseconds(NearbyContainerCacheTtlMs), + TileEntities = new List(tileEntities) + }; + } + + private static bool TryBuildNearbyContainerCacheKey(EntityAlive player, float distance, bool forRepairs, + NearbyContainerScanContext scanContext, out NearbyContainerCacheKey key) + { + key = default; + if (player == null || player.world == null || GameManager.Instance?.World == null || scanContext == null || + !scanContext.CanUseNearbyContainerCache) return false; + + var position = player.GetPosition(); + var positionBucket = new Vector3i( + Mathf.FloorToInt(position.x / NearbyContainerCachePositionBucketSize), + Mathf.FloorToInt(position.y / NearbyContainerCachePositionBucketSize), + Mathf.FloorToInt(position.z / NearbyContainerCachePositionBucketSize)); + + var landClaimContainersOnly = scanContext.LandClaimContainersOnly; + var landClaimPlayerOnly = scanContext.LandClaimPlayerOnly; + var disabledsender = scanContext.DisabledSenderRaw; + var nottoWorkstation = scanContext.NotToWorkstationRaw; + var bindtoWorkstation = scanContext.BindToWorkstationRaw; + var invertDisable = scanContext.InvertDisable; // Invertdisable + var enforceBindToWorkstation = scanContext.EnforceBindToWorkstation; // enforcebindtoWorkstation + var workstation = scanContext.CurrentWorkstation; // GetCurrentWorkstation is resolved once during context creation. + + var context = string.Join("|", landClaimContainersOnly, landClaimPlayerOnly, disabledsender, nottoWorkstation, + bindtoWorkstation, invertDisable, enforceBindToWorkstation, workstation); + key = new NearbyContainerCacheKey(player.entityId, distance, forRepairs, positionBucket, context); + return true; + } private static string GetCurrentWorkstation(EntityPlayerLocal player) { @@ -84,9 +225,14 @@ public static List GetTileEntities(EntityAlive player, float distanc } } - var disabledsender = Configuration.GetPropertyValue(AdvFeatureClass, "disablesender").Split(','); - var nottoWorkstation = Configuration.GetPropertyValue(AdvFeatureClass, "nottoWorkstation"); - var bindtoWorkstation = Configuration.GetPropertyValue(AdvFeatureClass, "bindtoWorkstation"); + var scanContext = BuildNearbyContainerScanContext(player, landClaimContainersOnly, landClaimPlayerOnly); + var canUseNearbyContainerCache = TryBuildNearbyContainerCacheKey(player, distance, forRepairs, scanContext, + out var nearbyContainerCacheKey); + if (canUseNearbyContainerCache && TryGetNearbyContainerCache(nearbyContainerCacheKey, out var cachedTileEntities)) + { + return cachedTileEntities; + } + var tileEntities = new List(); const string targetTypes = "Loot, SecureLoot, SecureLootSigned, Composite"; var paths = SCoreUtils.ScanForTileEntities(player, targetTypes, true); @@ -124,47 +270,137 @@ public static List GetTileEntities(EntityAlive player, float distanc { continue; } - if (!tileEntity.TryGetSelfOrFeature(out var lootTileEntity)) + if (!ShouldIncludeTileEntity(scanContext, tileEntity)) { continue; } - if (disabledsender[0] != null) - { - if (DisableSender(disabledsender, tileEntity)) - { - continue; - } - } - if (!string.IsNullOrEmpty(nottoWorkstation)) - { - if (NotToWorkstation(nottoWorkstation, player, tileEntity)) - { - continue; - } - } + tileEntities.Add(tileEntity); + } - if (!string.IsNullOrEmpty(bindtoWorkstation)) + if (canUseNearbyContainerCache) + { + StoreNearbyContainerCache(nearbyContainerCacheKey, tileEntities); + } + + return tileEntities; + } + + private static NearbyContainerScanContext BuildNearbyContainerScanContext(EntityAlive player, + bool landClaimContainersOnly, bool landClaimPlayerOnly) + { + var disabledSenderRaw = Configuration.GetPropertyValue(AdvFeatureClass, "disablesender"); + var notToWorkstationRaw = Configuration.GetPropertyValue(AdvFeatureClass, "nottoWorkstation"); + var bindToWorkstationRaw = Configuration.GetPropertyValue(AdvFeatureClass, "bindtoWorkstation"); + var invertDisable = bool.Parse(Configuration.GetPropertyValue(AdvFeatureClass, "Invertdisable")); + var enforceBindToWorkstation = bool.Parse(Configuration.GetPropertyValue(AdvFeatureClass, "enforcebindtoWorkstation")); + + var hasLocalWorkstationContext = false; + var canUseNearbyContainerCache = true; + var currentWorkstation = string.Empty; + if (player is EntityPlayerLocal playerLocal) + { + hasLocalWorkstationContext = true; + try { - if (BindToWorkstation(bindtoWorkstation, player, tileEntity)) - { - tileEntities.Add(tileEntity); - } + currentWorkstation = GetCurrentWorkstation(playerLocal); } - else + catch { - tileEntities.Add(tileEntity); + canUseNearbyContainerCache = false; } } - return tileEntities; + return new NearbyContainerScanContext + { + LandClaimContainersOnly = landClaimContainersOnly, + LandClaimPlayerOnly = landClaimPlayerOnly, + DisabledSenderRaw = disabledSenderRaw, + NotToWorkstationRaw = notToWorkstationRaw, + BindToWorkstationRaw = bindToWorkstationRaw, + CurrentWorkstation = currentWorkstation, + HasLocalWorkstationContext = hasLocalWorkstationContext, + CanUseNearbyContainerCache = canUseNearbyContainerCache, + InvertDisable = invertDisable, + EnforceBindToWorkstation = enforceBindToWorkstation, + DisabledSenderValues = SplitCsv(disabledSenderRaw), + NotToWorkstationBindings = ParseWorkstationBindings(notToWorkstationRaw, true), + BindToWorkstationBindings = ParseWorkstationBindings(bindToWorkstationRaw, false) + }; + } + + private static string[] SplitCsv(string value) + { + return value?.Split(',') ?? Array.Empty(); + } + + private static List ParseWorkstationBindings(string value, bool trimLootLists) + { + var bindings = new List(); + if (string.IsNullOrEmpty(value)) + { + return bindings; + } + + foreach (var binding in value.Split(';')) + { + var parts = binding.Split(':'); + var workstations = parts.Length > 0 ? SplitAndTrim(parts[0], true) : Array.Empty(); + var lootLists = parts.Length > 1 ? SplitAndTrim(parts[1], trimLootLists) : Array.Empty(); + bindings.Add(new WorkstationLootBinding(workstations, lootLists)); + } + + return bindings; + } + + private static string[] SplitAndTrim(string value, bool trimEntries) + { + var values = value?.Split(',') ?? Array.Empty(); + if (!trimEntries) + { + return values; + } + + for (var i = 0; i < values.Length; i++) + { + values[i] = values[i].Trim(); + } + + return values; + } + + private static bool ShouldIncludeTileEntity(NearbyContainerScanContext scanContext, TileEntity tileEntity) + { + if (!tileEntity.TryGetSelfOrFeature(out _)) + { + return false; + } + + if (scanContext.DisabledSenderValues.Length > 0 && scanContext.DisabledSenderValues[0] != null && + DisableSender(scanContext.DisabledSenderValues, scanContext.InvertDisable, tileEntity)) + { + return false; + } + + if (!string.IsNullOrEmpty(scanContext.NotToWorkstationRaw) && NotToWorkstation(scanContext, tileEntity)) + { + return false; + } + + return string.IsNullOrEmpty(scanContext.BindToWorkstationRaw) || BindToWorkstation(scanContext, tileEntity); } public static bool DisableSender(IEnumerable value, ITileEntity tileEntity) { - if (!tileEntity.TryGetSelfOrFeature(out var lootTileEntity)) return false; + var disableSenderValues = value as string[] ?? value.ToArray(); var invertdisable = bool.Parse(Configuration.GetPropertyValue(AdvFeatureClass, "Invertdisable")); - if (!invertdisable) + return DisableSender(disableSenderValues, invertdisable, tileEntity); + } + + private static bool DisableSender(IReadOnlyCollection value, bool invertDisable, ITileEntity tileEntity) + { + if (!tileEntity.TryGetSelfOrFeature(out var lootTileEntity)) return false; + if (!invertDisable) { if (value.Any(x => x.Trim() == lootTileEntity.lootListName)) { @@ -182,24 +418,22 @@ public static bool DisableSender(IEnumerable value, ITileEntity tileEnti return false; } - private static bool BindToWorkstation(string value, EntityAlive player, TileEntity tileEntity) + private static bool BindToWorkstation(NearbyContainerScanContext scanContext, TileEntity tileEntity) { var result = false; - if (player is not EntityPlayerLocal playerLocal) return false; + if (!scanContext.HasLocalWorkstationContext) return false; if (!tileEntity.TryGetSelfOrFeature(out var lootTileEntity)) return false; - // TODO: we want to refactor this to remove the complex LinQ. - // bind storage to workstation - if (value.Split(';').Where(x => - x.Split(':')[0].Split(',').Any(ws => ws.Trim() == GetCurrentWorkstation(playerLocal))) - .Any(x => x.Split(':')[1].Split(',').Any(y => y == lootTileEntity.lootListName))) result = true; - // bind storage to other workstations if allowed - if (value.Split(';').Any(x => - x.Split(':')[0].Split(',').Any(ws => ws.Trim() == GetCurrentWorkstation(playerLocal))) - || bool.Parse(Configuration.GetPropertyValue(AdvFeatureClass, "enforcebindtoWorkstation"))) + if (scanContext.BindToWorkstationBindings.Where(binding => + binding.Workstations.Any(ws => ws == scanContext.CurrentWorkstation)) + .Any(binding => binding.LootLists.Any(lootList => lootList == lootTileEntity.lootListName))) result = true; + if (scanContext.BindToWorkstationBindings.Any(binding => + binding.Workstations.Any(ws => ws == scanContext.CurrentWorkstation)) + || scanContext.EnforceBindToWorkstation) return result; { - if (value.Split(';').Any(x => x.Split(':')[1].Split(',').Any(y => y == lootTileEntity.lootListName))) + if (scanContext.BindToWorkstationBindings.Any(binding => + binding.LootLists.Any(lootList => lootList == lootTileEntity.lootListName))) { result = false; } @@ -212,18 +446,16 @@ private static bool BindToWorkstation(string value, EntityAlive player, TileEnti return result; } - private static bool NotToWorkstation(string value, EntityAlive player, TileEntity tileEntity) + private static bool NotToWorkstation(NearbyContainerScanContext scanContext, TileEntity tileEntity) { var result = false; - if (player is not EntityPlayerLocal playerLocal) return false; + if (!scanContext.HasLocalWorkstationContext) return false; if (!tileEntity.TryGetSelfOrFeature(out var lootTileEntity)) return false; - foreach (var bind in value.Split(';')) + foreach (var binding in scanContext.NotToWorkstationBindings) { - var workstation = bind.Split(':')[0].Split(','); - var disablebinding = bind.Split(':')[1].Split(','); - if ((workstation.Any(ws => ws.Trim() == GetCurrentWorkstation(playerLocal))) && - (disablebinding.Any(x => x.Trim() == lootTileEntity.lootListName))) result = true; + if ((binding.Workstations.Any(ws => ws == scanContext.CurrentWorkstation)) && + (binding.LootLists.Any(lootList => lootList == lootTileEntity.lootListName))) result = true; } return result; @@ -310,7 +542,13 @@ public static List SearchNearbyContainers(EntityAlive player, ItemVal public static bool AddToNearbyContainer(EntityAlive player, ItemStack itemStack, float distance) { var tileEntities = GetTileEntities(player, distance, false); - var itemName = itemStack.itemValue?.ItemClass?.GetItemName() ?? "unknown"; + return AddToNearbyContainer(player, itemStack, tileEntities); + } + + public static bool AddToNearbyContainer(EntityAlive player, ItemStack itemStack, IEnumerable tileEntities) + { + if (tileEntities == null) return false; + foreach (var tileEntity in tileEntities) { if (!tileEntity.TryGetSelfOrFeature(out var lootTileEntity)) continue; @@ -368,7 +606,10 @@ private static bool CheckTileEntity(ItemStack itemStack, ITileEntityLootable loo finally { if (changed) + { lootTileEntity.SetModified(); + InvalidateNearbyContainerCache(); + } lootTileEntity.SetUserAccessing(false); } @@ -414,6 +655,7 @@ public static void ConsumeItem(IEnumerable itemStacks, EntityPlayerLo if (!lootTileEntity.HasItem(enumerable[i].itemValue)) continue; + var containerChanged = false; for (var y = 0; y < lootTileEntity.items.Length; y++) { var item = lootTileEntity.items[y]; @@ -431,17 +673,11 @@ public static void ConsumeItem(IEnumerable itemStacks, EntityPlayerLo } else { - // Otherwise, let's just count down until we meet the requirement. - while (num >= 0) - { - item.count--; - num--; - if (item.count <= 0) - { - _removedItems.Add(new ItemStack(item.itemValue.Clone(), num)); - break; - } - } + // Remove only the amount this container can actually satisfy, then continue to the next source. + var removedCount = Math.Min(item.count, num); + item.count -= removedCount; + num -= removedCount; + _removedItems.Add(new ItemStack(item.itemValue.Clone(), removedCount)); } //Update the slot on the container, and do the Setmodified(), so that the dedis can get updated. @@ -450,14 +686,20 @@ public static void ConsumeItem(IEnumerable itemStacks, EntityPlayerLo // Add it to the removed list. _removedItems.Add(item.Clone()); lootTileEntity.UpdateSlot(y, ItemStack.Empty.Clone()); + containerChanged = true; } else { lootTileEntity.UpdateSlot(y, item); + containerChanged = true; } } - lootTileEntity.SetModified(); + if (containerChanged) + { + lootTileEntity.SetModified(); + InvalidateNearbyContainerCache(); + } } } }