using System.Collections.Generic; using UnityEngine; namespace UAI { /// /// V4 container and tile-entity utilities: world scanning, looting, and container validation. /// /// Differences from : /// /// caches results /// per entity + target-type combination for /// seconds, avoiding redundant chunk iteration every consideration tick. /// The inner NearestPathSorter class is replaced with a /// sqrMagnitude-based lambda comparison. /// All magic numbers replaced with constants. /// /// /// public static class ContainerUtils { // ── Scan Cache ──────────────────────────────────────────────────────────── // Key: (entityId, targetTypesHashCode) Value: (paths, timestamp) private static readonly Dictionary<(int, int), (List paths, float time)> _scanCache = new Dictionary<(int, int), (List, float)>(); // ── ScanForTileEntities ──────────────────────────────────────────────────── /// public static List ScanForTileEntities(Context context, string targetTypes = "") { return ScanForTileEntities(context.Self, targetTypes); } /// /// Returns a distance-sorted list of world positions for tile entities near /// that match the filter. /// Results are cached per entity + filter string for /// seconds. /// /// The entity performing the scan. /// /// Comma-separated tile-entity type names, optionally qualified with a block name /// after a colon (e.g. "Loot:crateWooden,Workstation"). /// Passing an empty string or "basic" uses a default set of common types. /// /// When true, already-touched loot containers are included. public static List ScanForTileEntities(EntityAlive self, string targetTypes = "", bool ignoreTouch = false) { // Normalise default filter. if (string.IsNullOrEmpty(targetTypes) || targetTypes.ToLower().Contains("basic")) targetTypes = "LandClaim,Loot,VendingMachine,Forge,Campfire,Workstation,PowerSource,Composite"; var cacheKey = (self.entityId, targetTypes.GetHashCode()); var now = Time.time; if (_scanCache.TryGetValue(cacheKey, out var cached) && now - cached.time < AIConstants.TileEntityCacheTtl) return cached.paths; // Cache miss — perform the scan. var paths = new List(); var blockPos = self.GetBlockPosition(); var chunkX = World.toChunkXZ(blockPos.x); var chunkZ = World.toChunkXZ(blockPos.z); var radius = AIConstants.TileEntityScanChunkRadius; // Pre-split the filter string once rather than inside the innermost loop. var filterEntries = targetTypes.Split(','); for (var i = -radius; i <= radius; i++) { for (var j = -radius; j <= radius; j++) { var chunk = (Chunk) self.world.GetChunkSync(chunkX + j, chunkZ + i); if (chunk == null) continue; foreach (var tileEntity in chunk.GetTileEntities().list) { foreach (var filterEntryRaw in filterEntries) { var filterEntry = filterEntryRaw; var blockNames = ""; // Optional block-name qualifier after ":". if (filterEntry.Contains(":")) { var parts = filterEntry.Split(':'); filterEntry = parts[0]; blockNames = parts[1]; } var targetType = EnumUtils.Parse(filterEntry.Trim(), true); if (tileEntity.GetTileEntityType() != targetType) continue; if (tileEntity.GetTileEntityType() == TileEntityType.None) continue; if (!ignoreTouch && tileEntity is TileEntityComposite tec && tec.GetFeature()?.bTouched == true) continue; if (!string.IsNullOrEmpty(blockNames) && !blockNames.Contains(tileEntity.blockValue.Block.GetBlockName())) continue; paths.Add(tileEntity.ToWorldPos().ToVector3()); } } } } // Sort by squared distance — no allocating IComparer object needed. paths.Sort((a, b) => self.GetDistanceSq(a).CompareTo(self.GetDistanceSq(b))); _scanCache[cacheKey] = (paths, now); return paths; } // ── GetItemFromContainer ────────────────────────────────────────────────── /// /// Loots items from into the entity's own loot inventory, /// scaling loot game-stage from the leader player if one exists. /// public static void GetItemFromContainer(Context context, TileEntityComposite tileContainer) { var storage = tileContainer.GetFeature(); if (storage == null) return; var blockPos = tileContainer.ToWorldPos(); if (string.IsNullOrEmpty(storage.lootListName)) return; if (storage.bTouched) return; storage.bTouched = true; storage.bWasTouched = true; if (storage.items == null) return; context.Self.SetLookPosition(blockPos); context.Self.MinEventContext.TileEntity = tileContainer; context.Self.FireEvent(MinEventTypes.onSelfOpenLootContainer); var lootContainer = LootContainer.GetLootContainer(storage.lootListName); if (lootContainer == null) { context.Self.SetLookPosition(Vector3.zero); return; } var leader = EntityUtilities.GetLeaderOrOwner(context.Self.entityId) as EntityPlayer; var lootGameStage = leader != null ? leader.unModifiedGameStage : 1f; var items = lootContainer.Spawn( context.Self.rand, storage.items.Length, lootGameStage, 0f, leader, new FastTags(), lootContainer.UniqueItems, true, false); for (var i = 0; i < items.Count; i++) context.Self.bag?.AddItem(items[i].Clone()); context.Self.FireEvent(MinEventTypes.onSelfLootContainer); context.Self.SetLookPosition(Vector3.zero); } // ── CheckContainer ──────────────────────────────────────────────────────── /// /// Validates that the entity is on the ground, facing , /// within reach, and that a valid tile entity exists there; then loots it. /// Returns true when the container was successfully interacted with. /// public static bool CheckContainer(Context context, Vector3 position) { if (!context.Self.onGround) return false; context.Self.SetLookPosition(position); var lookRay = new Ray(context.Self.position, context.Self.GetLookVector()); context.Self.SetLookPosition(Vector3.zero); if (!Voxel.Raycast(context.Self.world, lookRay, AIConstants.ContainerRaycastLength, false, false)) return false; if (!Voxel.voxelRayHitInfo.bHitValid) return false; var distSq = (position - context.Self.position).sqrMagnitude; if (distSq > AIConstants.ContainerReachDistSq) return false; var tileEntity = context.Self.world.GetTileEntity(new Vector3i(position)); if (tileEntity is TileEntityComposite lootContainer) GetItemFromContainer(context, lootContainer); EntityUtilities.Stop(context.Self.entityId); return true; } } }