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;
}
}
}