feat: Adds AdvancedSearch (XmlFind) (#1649)
### Summary Adds Advanced Search (XmlFind) using `[AS` command. Notable differences from XmlFind: * No save to file. * No "Display from" option (might add that later). * No advanced XmlSpawner property test syntax. Only basic property operator comparisons. * This version will freeze your shard, but fans out to all processors just like world saves. (So it should be stupid fast, especially if you have lots of cores) * Does not support spawn entry/type/error searching. We have a spawn search command to do that already. * I haven't set up correct "defaults" for the options when you first open it. I'll do that later. ### Screenshots <img width="575" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/2b08e586-0ea6-41c4-b7cd-f70e2ee62b86"> <img width="575" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/da3f16b9-6a9e-4d2c-9d93-6cdfa67b4ddc"> <img width="572" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/ae1d0a3f-a30d-497c-99ea-2cc988147f8f">
This commit is contained in:
parent
fe6e9dfe5c
commit
fbbc407954
10 changed files with 2129 additions and 0 deletions
|
|
@ -0,0 +1,363 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
|
||||
namespace Server.Engines.AdvancedSearch;
|
||||
|
||||
public class AdvancedSearchThreadWorker
|
||||
{
|
||||
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<IEntity> _entities;
|
||||
private ConcurrentQueue<AdvancedSearchResult> _results;
|
||||
private ConcurrentQueue<IEntity> _ignoreQueue;
|
||||
private WorldLocation _worldLocation;
|
||||
private AdvancedSearchFilter _filter;
|
||||
|
||||
public AdvancedSearchThreadWorker()
|
||||
{
|
||||
_startEvent = new AutoResetEvent(false);
|
||||
_stopEvent = new AutoResetEvent(false);
|
||||
_entities = new ConcurrentQueue<IEntity>();
|
||||
_thread = new Thread(Execute);
|
||||
_thread.Start(this);
|
||||
}
|
||||
|
||||
public void Wake(
|
||||
WorldLocation worldLocation,
|
||||
AdvancedSearchFilter filter,
|
||||
ConcurrentQueue<AdvancedSearchResult> results,
|
||||
ConcurrentQueue<IEntity> ignoreQueue
|
||||
)
|
||||
{
|
||||
_worldLocation = worldLocation;
|
||||
_filter = filter;
|
||||
_ignoreQueue = ignoreQueue;
|
||||
_results = results;
|
||||
_startEvent.Set();
|
||||
}
|
||||
|
||||
public void Sleep()
|
||||
{
|
||||
Volatile.Write(ref _pause, true);
|
||||
_stopEvent.WaitOne();
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
_exit = true;
|
||||
|
||||
Wake(WorldLocation.Zero, null, null, null);
|
||||
Sleep();
|
||||
}
|
||||
|
||||
public void Push(IEntity entity)
|
||||
{
|
||||
_entities.Enqueue(entity);
|
||||
}
|
||||
|
||||
private static void Execute(object obj)
|
||||
{
|
||||
AdvancedSearchThreadWorker worker = (AdvancedSearchThreadWorker)obj;
|
||||
|
||||
var reader = worker._entities;
|
||||
|
||||
while (worker._startEvent.WaitOne())
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
bool 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;
|
||||
}
|
||||
}
|
||||
|
||||
worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished
|
||||
worker._pause = false;
|
||||
|
||||
if (Core.Closing || worker._exit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AdvancedSearchResult DoEntitySearch(IEntity entity)
|
||||
{
|
||||
if (_filter.HideValidInternalMap)
|
||||
{
|
||||
HandleValidInternal(entity);
|
||||
}
|
||||
|
||||
// Check for valid map
|
||||
if (_filter.FilterFelucca && entity.Map != Map.Felucca ||
|
||||
_filter.FilterTrammel && entity.Map != Map.Trammel ||
|
||||
_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;
|
||||
}
|
||||
|
||||
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.InsensitiveEquals(_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.InsensitiveEquals(_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 (Key k in keyring.Keys)
|
||||
{
|
||||
_ignoreQueue.Enqueue(k);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity is BaseHouse house)
|
||||
{
|
||||
foreach (RelocatedEntity relEntity in house.RelocatedEntities)
|
||||
{
|
||||
if (relEntity.Entity is Item)
|
||||
{
|
||||
_ignoreQueue.Enqueue(relEntity.Entity);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (VendorInventory inventory in house.VendorInventories)
|
||||
{
|
||||
foreach (Item subItem in inventory.Items)
|
||||
{
|
||||
_ignoreQueue.Enqueue(subItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EvaluateRecursive(IEntity entity, ReadOnlySpan<char> span)
|
||||
{
|
||||
int atIndex = span.IndexOf('@');
|
||||
int orIndex = span.IndexOf('|');
|
||||
|
||||
if (atIndex == -1 && orIndex == -1)
|
||||
{
|
||||
return EvaluateSingleExpression(entity, span);
|
||||
}
|
||||
|
||||
bool result = atIndex != -1;
|
||||
int splitIndex = result ? atIndex : orIndex;
|
||||
|
||||
var left = EvaluateRecursive(entity, span.Slice(0, splitIndex));
|
||||
var right = EvaluateRecursive(entity, span.Slice(splitIndex + 1));
|
||||
|
||||
return result ? left && right : left || right;
|
||||
}
|
||||
|
||||
private static bool EvaluateSingleExpression(IEntity entity, ReadOnlySpan<char> expression)
|
||||
{
|
||||
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 = entity.GetType().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);
|
||||
bool result = AdvancedSearchUtilities.CompareValues(property.PropertyType, propertyValue, valuePart, operatorSpan);
|
||||
|
||||
return negate ? !result : result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue