using System; using System.Collections.Concurrent; using System.Reflection; using System.Threading; using Server.Items; using Server.Logging; using Server.Mobiles; using Server.Multis; namespace Server.Engines.AdvancedSearch; /// /// Filters entities on a background thread while the main loop keeps mutating them — an /// intentional, bounded race. Reads of live / state are /// unsynchronized, so a torn read may report stale coordinates, and any /// getter that throws mid-read is caught per-entity in and skipped. /// Results are best-effort and may omit a concurrently modified entity, but never fault or corrupt /// server state. Eliminating the race would require snapshotting each read field onto the main /// thread before handing entities off; that is deferred. /// public class AdvancedSearchThreadWorker { private static readonly ILogger _logger = LogFactory.GetLogger(typeof(AdvancedSearchThreadWorker)); private static readonly ConcurrentDictionary _propCache = new(); private readonly Thread _thread; private readonly AutoResetEvent _startEvent; // Main thread tells the thread to start working private readonly AutoResetEvent _stopEvent; // Main thread waits for the worker finish draining private bool _pause; private bool _exit; private readonly ConcurrentQueue _entities; private ConcurrentQueue _results; private ConcurrentQueue _ignoreQueue; private WorldLocation _worldLocation; private AdvancedSearchFilter _filter; public AdvancedSearchThreadWorker() { _startEvent = new AutoResetEvent(false); _stopEvent = new AutoResetEvent(false); _entities = new ConcurrentQueue(); _thread = new Thread(Execute) { IsBackground = true }; _thread.Start(this); } public void Wake( WorldLocation worldLocation, AdvancedSearchFilter filter, ConcurrentQueue results, ConcurrentQueue ignoreQueue ) { _worldLocation = worldLocation; _filter = filter; _ignoreQueue = ignoreQueue; _results = results; _startEvent.Set(); } public void Sleep() { Volatile.Write(ref _pause, true); _stopEvent.WaitOne(); } public void Exit() { Volatile.Write(ref _exit, true); Wake(WorldLocation.Zero, null, null, null); // Tolerate a worker that has already terminated (e.g. Core.Closing raced us) so // shutdown can't deadlock waiting on a stopEvent that will never be set. if (_thread.IsAlive) { Sleep(); } } public void Push(IEntity entity) { _entities.Enqueue(entity); } private static void Execute(object obj) { var worker = (AdvancedSearchThreadWorker)obj; var reader = worker._entities; while (worker._startEvent.WaitOne()) { while (true) { var pauseRequested = Volatile.Read(ref worker._pause); if (reader.TryDequeue(out var entity)) { var result = worker.DoEntitySearch(entity); if (result != null) { worker._results?.Enqueue(result); } } else if (pauseRequested) // Break when finished { worker._results = null; worker._filter = null; break; } else { // Transiently empty but not yet paused: yield rather than busy-spin. Thread.Yield(); } } // The owning thread may start another cycle the moment _stopEvent is set (Exit does exactly // that). Clear _pause and sample the exit condition before signaling, or the new cycle's // pause request is clobbered / its Sleep orphaned. Matches SerializationThreadWorker. var exiting = Core.Closing || Volatile.Read(ref worker._exit); Volatile.Write(ref worker._pause, false); worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished if (exiting) { return; } } } private AdvancedSearchResult DoEntitySearch(IEntity entity) { if (entity == null || entity.Deleted) { return null; } try { return DoEntitySearchCore(entity); } catch (Exception ex) { _logger.Warning(ex, "AdvancedSearch: filter threw for {Entity}; skipping", entity); return null; } } private AdvancedSearchResult DoEntitySearchCore(IEntity entity) { if (_filter == null) { // Exit() clears the filter; a straggler entity dequeued after teardown bails here. return null; } // Check for valid map if (_filter.FilterVeridia && entity.Map != Map.Veridia || _filter.FilterUnderworld && entity.Map != Map.Underworld || _filter.FilterIlshenar && entity.Map != Map.Ilshenar || _filter.FilterMalas && entity.Map != Map.Malas || _filter.FilterTokuno && entity.Map != Map.Tokuno || _filter.FilterTerMur && entity.Map != Map.TerMur || _filter.FilterInternalMap && entity.Map != Map.Internal || _filter.FilterNullMap && entity.Map != null) { return null; } var location = (entity as Item)?.GetWorldLocation() ?? entity.Location; if (_filter.FilterRange && (_filter.Range == null || _filter.Range < 0 || entity.Map != _worldLocation.Map || !Utility.InRange(_worldLocation.Location, location, _filter.Range.Value))) { return null; } if (_filter.FilterRegion && (string.IsNullOrWhiteSpace(_filter.RegionName) || !Region.Find(location, entity.Map).IsPartOf(_filter.RegionName))) { return null; } // After the cheap filters, so non-qualifying entities skip the house/keyring enumeration. if (_filter.HideValidInternalMap) { HandleValidInternal(entity); } if (entity is Mobile mobile) { return DoMobileSearch(mobile); } if (entity is Item item) { return DoItemSearch(item); } return null; } private static bool IsValidInternal(Item item) { if (item.Parent != null || item.HeldBy != null) { return true; } if (item is Fists or MountItem or EffectItem or MovingCrate or BaseDockedBoat or BaseBoat or Plank or TillerMan or Hold) { return true; } // DisplayCache container if (item.GetType().DeclaringType == typeof(GenericBuyInfo)) { return true; } return false; } private AdvancedSearchResult DoItemSearch(Item item) { if (_filter.FilterName && !string.IsNullOrWhiteSpace(_filter.Name) && !(item.Name ?? item.ItemData.Name).InsensitiveContains(_filter.Name)) { return null; } if (_filter.HideValidInternalMap && item.Map == Map.Internal && !IsValidInternal(item)) { return null; } if (_filter.FilterPropertyTest && (string.IsNullOrWhiteSpace(_filter.PropertyTest) || !EvaluateRecursive(item, _filter.PropertyTest))) { return null; } return new AdvancedSearchResult(item.Name ?? item.ItemData.Name, item.GetType(), item.Location, item.Map, item.RootParent) { Entity = item, }; } private AdvancedSearchResult DoMobileSearch(Mobile mobile) { if (_filter.FilterName && !string.IsNullOrWhiteSpace(_filter.Name) && !mobile.Name.InsensitiveContains(_filter.Name)) { return null; } if (_filter.HideValidInternalMap && mobile.Map == Map.Internal && !IsValidInternal(mobile)) { return null; } if (_filter.FilterPropertyTest && (string.IsNullOrWhiteSpace(_filter.PropertyTest) || !EvaluateRecursive(mobile, _filter.PropertyTest))) { return null; } return new AdvancedSearchResult(mobile.Name, mobile.GetType(), mobile.Location, mobile.Map, null) { Entity = mobile }; } private static bool IsValidInternal(Mobile m) { // Logged out players if (m.Account != null) { return true; } // Stabled pets if (m is BaseCreature creature && creature.IsStabled) { return true; } // Internalized vendors if (m is PlayerVendor playerVendor && playerVendor.House != null) { return true; } // Currently mounted creatures if (m is IMount mount && mount.Rider != null) { return true; } return false; } public void HandleValidInternal(IEntity entity) { if (entity is CommodityDeed deed && deed.Commodity != null && deed.Commodity.Map == Map.Internal) { _ignoreQueue.Enqueue(entity); return; } // Keys don't have a backreference, so we just ignore them for now if (entity is KeyRing keyring && keyring.Keys?.Count > 0) { foreach (var k in keyring.Keys) { _ignoreQueue.Enqueue(k); } return; } if (entity is BaseHouse house) { foreach (var relEntity in house.RelocatedEntities) { if (relEntity.Entity is Item) { _ignoreQueue.Enqueue(relEntity.Entity); } } foreach (var inventory in house.VendorInventories) { foreach (var subItem in inventory.Items) { _ignoreQueue.Enqueue(subItem); } } } } private static bool EvaluateRecursive(IEntity entity, ReadOnlySpan span) => AdvancedSearchUtilities.EvaluateBoolean(span, entity, static (e, leaf) => EvaluateSingleExpression(e, leaf)); private static bool EvaluateSingleExpression(IEntity entity, ReadOnlySpan expression) { expression = expression.Trim(); if (expression.Length == 0) { return false; } var negate = false; if (expression[0] == '~') { negate = true; expression = expression[1..]; } var operatorSpan = AdvancedSearchUtilities.FindOperatorIndex(expression, out var operatorIndex); if (operatorSpan.Length == 0) { return false; } var propertyName = expression[..operatorIndex].Trim(); var valuePart = expression[(operatorIndex + operatorSpan.Length)..].Trim(); if (valuePart.Length == 0) { return false; } var properties = _propCache.GetOrAdd(entity.GetType(), static t => t.GetProperties()); PropertyInfo property = null; for (var i = 0; i < properties.Length; ++i) { var p = properties[i]; if (p.CanRead && p.Name.InsensitiveEquals(propertyName)) { property = p; break; } } if (property == null) { return false; } var propertyValue = property.GetValue(entity); var result = AdvancedSearchUtilities.CompareValues(property.PropertyType, propertyValue, valuePart, operatorSpan); return negate ? !result : result; } }