ModernUO/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs
Kamron Batman 1e97ed50f6
fix: Harden Advanced Search: crash-safety, autosave, correct results & worker fixes (#2543)
## Summary

Hardens the **Advanced Search** engine (`Projects/UOContent/Engines/Advanced Search/`) — the GM entity finder that fans searches across background worker threads. A code review surfaced 14 defects (A–N), including a shard-crasher reachable from a single admin typo and a path that silently disables autosave for the rest of the shard's uptime. Each behavioral fix ships with a test.

Full `UOContent.Tests` suite: **530/530 green** (21 new AdvancedSearch tests).

## Fixes

### Crash / data-loss
- **A — Shard crash on a malformed Property Test.** `AdvancedSearchThreadWorker.Execute` had no `try/catch` and the worker `Thread` is foreground, so a parse throw (`Hits>abc`, `Layer=onehanded` — `Enum.Parse` was case-sensitive, `Hits>1@` — empty sub-expression indexing) terminated the process. Now: `ParseValue`/`CompareValues` use `TryParse`/`Enum.TryParse(ignoreCase)` and return no-match instead of throwing; the per-entity filter is wrapped in `try/catch` (logs + skips); empty expressions are guarded.
- **C — Overlapping searches corrupt state + brick autosave.** `_threadWorkers`/`_threadId` were `static` but `DoSearch` is an instance method; a second search (double-click / two admins) stomped shared worker state and could leave a drain waiting forever on the shared `AutoResetEvent`, so `AutoSave.SavesEnabled` was never restored. Now: an `Interlocked` re-entrancy guard rejects concurrent searches.
- **G — Autosave restore not guaranteed.** The restore lived only in the success callback. Now it's in a `finally` (plus an outer `catch` covering the synchronous setup and a `catch` on the drain body), so autosave + the guard are always released.

### Wrong results
- **D — `@`/`|` operator precedence.** `a@b|c` evaluated as `a && (b || c)` instead of `(a && b) || c`. OR now binds looser than AND (`AdvancedSearchUtilities.EvaluateBoolean`, unit-tested).
- **E — Descending sort, partial last page rendered blank** (the index decreased in descending mode and the `break` early-out killed the loop). Now a bounded `VisibleCount`-driven loop renders the last page in both directions.
- **F — Deleted entities** were not skipped (ghost rows). Now `DoEntitySearch` skips `entity.Deleted`.
- **N — Reference-type comparisons** threw (`Comparer<T>.Default.Compare` on non-`IComparable`) and compared references to a string. Now equality is by value and ordering is guarded to `IComparable` (no throw).

### Worker perf / hardening
- **H** busy-spin → `Thread.Yield()` in the drain; **I** `GetProperties()` cached per `Type`; **J** `HandleValidInternal` moved behind the cheap map/range/region filters; **K** worker threads are `IsBackground` + `Exit()` tolerates an already-terminated worker; **L** `_filter == null` guard; **M** consistent `Volatile` access on `_pause`/`_exit`.

### Documented
- **B** — the residual worker/event-loop read race is documented on `AdvancedSearchThreadWorker`: workers read live entity state concurrently with the loop, so value-type reads may be stale-but-safe and getter exceptions are swallowed; fully eliminating it would require snapshotting entity fields on the main thread (deferred).

## Notes
- New test-only seams (`TryBeginSearch`/`EndSearch`/`IsSearchInProgress`/`VisibleCount`/`TryParseValue`/`EvaluateBoolean`) are `internal` via the existing `InternalsVisibleTo("UOContent.Tests")`.
- Dead `public ParseValue<T>` removed.
- `ConcurrentDictionary` for the reflection cache is intentional — these workers are genuinely parallel.
2026-07-21 07:51:06 -07:00

1000 lines
31 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using CommunityToolkit.HighPerformance;
using Server.Commands;
using Server.Commands.Generic;
using Server.Engines.Spawners;
using Server.Gumps;
using Server.Logging;
using Server.Network;
using Server.Saves;
namespace Server.Engines.AdvancedSearch;
[Flags]
public enum AdvancedSearchGumpOptions : long
{
None,
SortDescending = 0x00000001,
SortByType = 0x00000002,
SortByName = 0x00000004,
SortByRange = 0x00000008,
SortByMap = 0x00000010,
SortBySelected = 0x00000020,
AllSelected = 0x00000040
}
public class AdvancedSearchGump : Gump
{
private static readonly ILogger _logger = LogFactory.GetLogger(typeof(AdvancedSearchGump));
private const int MaxEntries = 18;
private static int _threadId;
private static AdvancedSearchThreadWorker[] _threadWorkers;
private static int _searchInProgress;
internal static bool IsSearchInProgress => Volatile.Read(ref _searchInProgress) == 1;
internal static bool TryBeginSearch() => Interlocked.CompareExchange(ref _searchInProgress, 1, 0) == 0;
internal static void EndSearch() => Volatile.Write(ref _searchInProgress, 0);
private static void Configure()
{
EventSink.Shutdown += Shutdown;
EventSink.ServerCrashed += OnCrashed;
}
private static void OnCrashed(ServerCrashedEventArgs obj)
{
Shutdown();
}
private static void Shutdown()
{
if (_threadWorkers == null)
{
return;
}
for (var i = 0; i < _threadWorkers.Length; i++)
{
_threadWorkers[i].Exit();
}
}
public static readonly AdvancedSearchFilter DefaultFilter = new()
{
FilterType = true,
FilterFelucca = true,
HideValidInternalMap = true,
Type = typeof(Spawner),
};
private AdvancedSearchGumpOptions _options;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool GetOptionsFlag(AdvancedSearchGumpOptions option) => (_options & option) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetOptionsFlag(AdvancedSearchGumpOptions option, bool value) =>
_options = value ? _options | option : _options & ~option;
public bool SortDescending
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.SortDescending);
set => SetOptionsFlag(AdvancedSearchGumpOptions.SortDescending, value);
}
public bool SortByType
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.SortByType);
set => SetOptionsFlag(AdvancedSearchGumpOptions.SortByType, value);
}
public bool SortByName
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.SortByName);
set => SetOptionsFlag(AdvancedSearchGumpOptions.SortByName, value);
}
public bool SortByRange
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.SortByRange);
set => SetOptionsFlag(AdvancedSearchGumpOptions.SortByRange, value);
}
public bool SortByMap
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.SortByMap);
set => SetOptionsFlag(AdvancedSearchGumpOptions.SortByMap, value);
}
public bool SortBySelected
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.SortBySelected);
set => SetOptionsFlag(AdvancedSearchGumpOptions.SortBySelected, value);
}
public bool AllSelected
{
get => GetOptionsFlag(AdvancedSearchGumpOptions.AllSelected);
set => SetOptionsFlag(AdvancedSearchGumpOptions.AllSelected, value);
}
public AdvancedSearchFilter Filter { get; set; } = DefaultFilter with {};
public AdvancedSearchResult[] SearchResults { get; set; }
public int DisplayFrom { get; set; }
public string CommandString { get; set; }
public AdvancedSearchGump() : base(50, 50) => Build();
// Entries on the current page — bounds the paging loop so a partial last page can't read past the end.
internal static int VisibleCount(int total, int displayFrom, int maxEntries) =>
Math.Clamp(total - displayFrom, 0, maxEntries);
private void Build()
{
const int height = 500;
var haveResults = SearchResults?.Length > 0;
var width = haveResults ? 755 : 170;
AddBackground(0, 0, width, height, 5054);
AddAlphaRegion(0, 0, width, height);
/// Sorting
var y = 5;
AddButton(5, y, 0xFAB, 0xFAD, 700);
AddLabel(38, y, 0x384, "Sort");
if (SortDescending)
{
AddButton(75, y + 3, 0x15E2, 0x15E6, 701);
AddLabel(95, y, 0x384, "Desc");
}
else
{
AddButton(75, y + 3, 0x15E0, 0x15E4, 701);
AddLabel(95, y, 0x384, "Asc");
}
y += 22;
AddRadio(5, y, 0xD2, 0xD3, SortByType, 0);
AddLabel(28, y, 0x384, "Type");
AddRadio(75, y, 0xD2, 0xD3, SortByName, 1);
AddLabel(98, y, 0x384, "Name");
y += 20;
AddRadio(5, y, 0xD2, 0xD3, SortByRange, 2);
AddLabel(28, y, 0x384, "Range");
AddRadio(75, y, 0xD2, 0xD3, SortByMap, 3);
AddLabel(98, y, 0x384, "Map");
y += 20;
AddRadio(5, y, 0xD2, 0xD3, SortBySelected, 4);
AddLabel(28, y, 0x384, "Select");
/// Searching
y = 85;
AddButton(5, y, 0xFA8, 0xFAA, 3);
AddLabel(38, y, 0x384, "Search");
y += 20;
AddCheck(5, y, 0xD2, 0xD3, Filter!.FilterInternalMap, 312);
AddLabel(28, y, 0x384, "Internal");
AddCheck(75, y, 0xD2, 0xD3, Filter.FilterNullMap, 314);
AddLabel(98, y, 0x384, "Null");
y += 20;
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterFelucca, 308);
AddLabel(28, y, 0x384, "Fel");
AddCheck(75, y, 0xD2, 0xD3, Filter.FilterTrammel, 309);
AddLabel(98, y, 0x384, "Tram");
y += 20;
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterMalas, 310);
AddLabel(28, y, 0x384, "Mal");
AddCheck(75, y, 0xD2, 0xD3, Filter.FilterIlshenar, 311);
AddLabel(98, y, 0x384, "Ilsh");
y += 20;
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterTokuno, 318);
AddLabel(28, y, 0x384, "Tok");
AddCheck(75, y, 0xD2, 0xD3, Filter.FilterTerMur, 320);
AddLabel(98, y, 0x384, "Ter");
y += 20;
AddCheck(5, y, 0xD2, 0xD3, Filter.HideValidInternalMap, 316);
AddLabel(28, y, 0x384, "Hide valid internal");
/// Filter
y = height - 295;
AddLabel(28, y, 0x384, "Region");
AddImageTiled(70, y, 68, 19, 0xBBC);
AddTextEntry(70, y, 250, 19, 0, 106, Filter.RegionName);
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterRegion, 319);
y += 20;
AddLabel(28, y, 0x384, "Age");
AddImageTiled(70, y, 68, 19, 0xBBC);
AddTextEntry(70, y, 250, 19, 0, 105, Filter.Age.ToString());
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterAge, 303);
AddCheck(50, y + 2, 0x1467, 0x1468, Filter.FilterAgeDirection, 302);
y += 20;
AddLabel(28, y, 0x384, "Range");
AddImageTiled(70, y, 45, 19, 0xBBC);
AddTextEntry(70, y, 45, 19, 0, 100, Filter.Range.ToString());
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterRange, 304);
y += 20;
AddLabel(28, y, 0x384, "Type");
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterType, 305);
AddImageTiled(6, y + 20, 132, 19, 0xBBC);
AddTextEntry(6, y + 20, 250, 19, 0, 101, Filter.Type?.Name);
y += 41;
AddLabel(28, y, 0x384, "Property Test");
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterPropertyTest, 315);
AddImageTiled(6, y + 20, 132, 19, 0xBBC);
AddTextEntry(6, y + 20, 500, 19, 0, 104, Filter.PropertyTest);
y += 41;
AddLabel(28, y, 0x384, "Name");
AddCheck(5, y, 0xD2, 0xD3, Filter.FilterName, 306);
AddImageTiled(6, y + 20, 132, 19, 0xBBC);
AddTextEntry(6, y + 20, 250, 19, 0, 102, Filter.Name);
if (!haveResults)
{
return;
}
/// Control Buttons
y = height - 25;
AddButton(150, y, 0xFB1, 0xFB3, 156);
AddLabel(183, y, 0x384, "Delete");
// For spawners in the list
AddButton(230, y, 0xFA2, 0xFA3, 157);
AddLabel(263, y, 0x384, "Reset");
AddButton(310, y, 0xFA8, 0xFAA, 158);
AddLabel(343, y, 0x384, "Respawn");
AddButton(5, y, 0xFAE, 0xFAF, 154);
AddLabel(38, y, 0x384, "Bring");
AddButton(470, y - 25, 0xFA8, 0xFAA, 160);
AddLabel(503, y - 25, 0x384, "Command:");
AddImageTiled(560, y - 25, 180, 19, 0xBBC);
AddTextEntry(560, y - 25, 180, 19, 0, 301, CommandString);
if (DisplayFrom > 0)
{
AddButton(395, height - 25, 0x15E3, 0x15E7, 202); // backward
}
if (DisplayFrom + MaxEntries < SearchResults.Length)
{
AddButton(415 + 25, height - 25, 0x15E1, 0x15E5, 201); // forward
}
if (SearchResults?.Length > 0)
{
var maxEntryIndex = Math.Min(DisplayFrom + MaxEntries, SearchResults.Length - 1);
/// Headers
AddLabel(143, 5, 0x384, "Gump");
AddLabel(178, 5, 0x384, "Prop");
AddLabel(210, 5, 0x384, "Goto");
AddLabel(250, 5, 0x384, "Name");
AddLabel(365, 5, 0x384, "Type");
AddLabel(460, 5, 0x384, "Location");
AddLabel(578, 5, 0x384, "Map");
AddLabel(650, 5, 0x384, "Owner");
AddLabel(180, y - 50, 68, $"Found {SearchResults.Length} items/mobiles");
AddLabel(400, y - 50, 68,
$"Displaying {DisplayFrom + 1}-{maxEntryIndex + 1}"
);
// Count the number of selected objects
var count = 0;
foreach (var e in SearchResults)
{
if (e.Selected)
{
count++;
}
}
AddLabel(600, y - 50, 33, $"Selected {count}");
AddLabel(610, y, 0x384, "Select All");
// display the select-all toggle
AddButton(670, y, AllSelected ? 0xD3 : 0xD2, AllSelected ? 0xD2 : 0xD3, 1);
var allDisplayedSelected = true;
// Bound to this page's real entries so a partial last page still renders in descending mode.
var visibleCount = VisibleCount(SearchResults.Length, DisplayFrom, MaxEntries);
for (var i = 0; i < visibleCount; i++)
{
var offset = SortDescending ? visibleCount - 1 - i : i;
var index = offset + DisplayFrom;
var entry = SearchResults[index];
if (!entry.Selected)
{
allDisplayedSelected = false;
}
AddImageTiled(235, 22 * i + 30, 386, 23, 0x52);
AddImageTiled(236, 22 * i + 31, 384, 21, 0xBBC);
if (entry.Entity is BaseSpawner)
{
AddButton(145, 22 * i + 30, 0xFBD, 0xFBE, 2000 + offset);
}
// Goto button
AddButton(205, 22 * i + 30, 0xFAE, 0xFAF, 1000 + offset);
// Interface button
AddButton(175, 22 * i + 30, 0xFAB, 0xFAD, 3000 + offset);
var textHue = 0;
var parentName = "";
var deleted = entry.Entity?.Deleted != false;
if (deleted)
{
entry.Entity = null; // Release this so we don't have hanging references
}
var loc = entry.GetLocation();
var map = entry.GetMap();
if (entry.Parent is Mobile parentMob)
{
textHue = parentMob.Player ? 44 : 24;
parentName = parentMob.Name;
}
else if (entry.Parent is Item parentItem)
{
textHue = 5;
parentName = parentItem.Name ?? parentItem.ItemData.Name;
}
// Name
AddLabelCropped(248, 22 * i + 31, 110, 21, deleted ? 5 : 0, entry.Name);
// Type
AddImageTiled(360, 22 * i + 31, 90, 21, 0xBBC);
AddLabelCropped(360, 22 * i + 31, 90, 21, 0, entry.Type.Name);
// Location
AddImageTiled(450, 22 * i + 31, 137, 21, 0xBBC);
AddLabel(450, 22 * i + 31, 0, loc.ToString());
// Map
AddImageTiled(571, 22 * i + 31, 70, 21, 0xBBC);
AddLabel(571, 22 * i + 31, 0, map?.Name ?? "(-null-)");
// Parent
AddImageTiled(640, 22 * i + 31, 90, 21, 0xBBC);
AddLabelCropped(640, 22 * i + 31, 90, 21, textHue, parentName);
// display the selection button
AddButton(730, 22 * i + 32, entry.Selected ? 0xD3 : 0xD2, entry.Selected ? 0xD2 : 0xD3, 4000 + offset);
}
AddButton(730, 5, allDisplayedSelected ? 0xD3 : 0xD2, allDisplayedSelected ? 0xD2 : 0xD3, 2); // Select all displayed
}
}
public override void OnResponse(NetState state, in RelayInfo info)
{
var from = state.Mobile;
if (from == null)
{
return;
}
SetSortSwitches(info.Switches.Length > 0 ? info.Switches[0] : -1);
Filter ??= new AdvancedSearchFilter();
Filter.FilterAgeDirection = info.IsSwitched(302);
Filter.FilterAge = info.IsSwitched(303);
Filter.FilterRange = info.IsSwitched(304);
Filter.FilterType = info.IsSwitched(305);
Filter.FilterName = info.IsSwitched(306);
Filter.FilterFelucca = info.IsSwitched(308);
Filter.FilterTrammel = info.IsSwitched(309);
Filter.FilterMalas = info.IsSwitched(310);
Filter.FilterIlshenar = info.IsSwitched(311);
Filter.FilterInternalMap = info.IsSwitched(312);
Filter.FilterNullMap = info.IsSwitched(314);
Filter.FilterPropertyTest = info.IsSwitched(315);
Filter.HideValidInternalMap = info.IsSwitched(316);
Filter.FilterTokuno = info.IsSwitched(318);
Filter.FilterRegion = info.IsSwitched(319);
Filter.FilterTerMur = info.IsSwitched(320);
var rangeText = info.GetTextEntry(100);
Filter.Range = rangeText != null ? Utility.ToInt32(rangeText) : null;
var filterText = info.GetTextEntry(101);
Filter.Type = filterText != null ? AssemblyHandler.FindTypeByName(filterText) : null;
Filter.Name = info.GetTextEntry(102);
Filter.PropertyTest = info.GetTextEntry(104);
var ageText = info.GetTextEntry(105);
Filter.Age = ageText != null ? Utility.ToTimeSpan(ageText) : null;
Filter.RegionName = info.GetTextEntry(106);
CommandString = info.GetTextEntry(301);
var buttonId = info.ButtonID;
switch (buttonId)
{
case 0: // Close
{
return;
}
case 1: // Select all toggle
{
AllSelected = !AllSelected;
if (SearchResults?.Length > 0)
{
for (var i = 0; i < SearchResults.Length; i++)
{
SearchResults[i].Selected = AllSelected;
}
}
break;
}
case 2: // Select all displayed
{
if (SearchResults?.Length > 0)
{
var allSelected = true;
var max = DisplayFrom + MaxEntries;
for (var i = DisplayFrom; i < max; i++)
{
if (i >= SearchResults.Length)
{
break;
}
var entry = SearchResults[i];
if (!entry.Selected)
{
allSelected = false;
break;
}
}
for (var i = DisplayFrom; i < max; i++)
{
if (i >= SearchResults.Length)
{
break;
}
var entry = SearchResults[i];
entry.Selected = !allSelected;
}
}
break;
}
case 3: // Search
{
DoSearch(from);
return; // Don't resend the gump, it's sent async
}
case 154: // Bring
{
if (SearchResults?.Length > 0)
{
var count = 0;
for (var i = 0; i < SearchResults.Length; i++)
{
if (SearchResults[i].Selected)
{
count++;
}
}
if (count > 0)
{
from.SendGump(new AdvancedSearchConfirmBringGump(this, count));
}
}
return;
}
case 156: // Delete
{
if (SearchResults?.Length > 0)
{
var count = 0;
for (var i = 0; i < SearchResults.Length; i++)
{
if (SearchResults[i].Selected)
{
count++;
}
}
if (count > 0)
{
from.SendGump(new AdvancedSearchConfirmDeleteGump(this, count));
}
}
return;
}
case 157: // Reset
{
if (SearchResults?.Length > 0)
{
for (var i = 0; i < SearchResults.Length; i++)
{
var entry = SearchResults[i];
if (entry.Selected)
{
(entry.Entity as Spawner)?.Reset();
}
}
}
break;
}
case 158: // Respawn
{
if (SearchResults?.Length > 0)
{
for (var i = 0; i < SearchResults.Length; i++)
{
var entry = SearchResults[i];
if (entry.Selected && entry.Entity is ISpawner { Running: true } spawner)
{
spawner.Respawn();
}
}
}
break;
}
case 160: // Command
{
ExecuteCommand(from, CommandString);
break;
}
case 201: // Forward
{
if (SearchResults?.Length > 0)
{
var maxEntryIndex = Math.Min(DisplayFrom + MaxEntries, SearchResults.Length - 1);
if (maxEntryIndex < SearchResults.Length - 1)
{
DisplayFrom = maxEntryIndex;
}
}
break;
}
case 202: // Backward
{
if (SearchResults?.Length > 0)
{
DisplayFrom = Math.Clamp(DisplayFrom - MaxEntries, 0, SearchResults.Length - 1);
}
break;
}
case 700: // Sort
{
Sort(from);
break;
}
case 701: // Change sort order
{
SortDescending = !SortDescending;
var radioSwitch = info.Switches.Length > 0 ? info.Switches[0] : -1;
if (radioSwitch is >= 0 and <= 4)
{
Sort(from);
}
break;
}
case >= 1000 and <= 1999: // Goto
{
var index = buttonId - 1000 + DisplayFrom;
if (!(SearchResults?.Length > 0) || index >= SearchResults.Length)
{
break;
}
var entry = SearchResults[index];
var loc = entry.GetLocation();
var map = entry.GetMap();
if (map == null || map == Map.Internal)
{
break;
}
from.MoveToWorld(loc, map);
break;
}
case <= 2999: // Open Spawner
{
var index = buttonId - 2000 + DisplayFrom;
if (!(SearchResults?.Length > 0) || index >= SearchResults.Length)
{
break;
}
var entry = SearchResults[index];
if (entry.Entity?.Deleted != false)
{
break;
}
if (entry.Entity.Map == null || entry.Entity.Map == Map.Internal)
{
break;
}
Resend(from);
// Open the spawner
(entry.Entity as BaseSpawner)?.OnDoubleClick(from);
return;
}
case <= 3999: // Props
{
var index = buttonId - 3000 + DisplayFrom;
if (!(SearchResults?.Length > 0) || index >= SearchResults.Length)
{
break;
}
var entry = SearchResults[index];
var entity = entry.Entity;
if (entity?.Deleted != false)
{
break;
}
if (!BaseCommand.IsAccessible(from, entity))
{
from.SendLocalizedMessage(500447); // That is not accessible.
break;
}
Resend(from);
from.SendGump(new PropertiesGump(from, entry.Entity));
return;
}
case <= 4999: // Select
{
var index = buttonId - 4000 + DisplayFrom;
if (!(SearchResults?.Length > 0) || index >= SearchResults.Length)
{
break;
}
var entry = SearchResults[index];
entry.Selected = !entry.Selected;
break;
}
}
Resend(from);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PushToWorkers(IEntity e)
{
_threadWorkers[_threadId++].Push(e);
if (_threadId == _threadWorkers.Length)
{
_threadId = 0;
}
}
private void DoSearch(Mobile from)
{
if (!World.Running)
{
from.SendMessage("You cannot search while the world is saving.");
return;
}
if (!TryBeginSearch())
{
from.SendMessage("A search is already running. Please wait for it to finish.");
return;
}
_threadId = 0;
var autoSave = AutoSave.SavesEnabled;
AutoSave.SavesEnabled = false;
try
{
_threadWorkers ??= new AdvancedSearchThreadWorker[Math.Max(Environment.ProcessorCount - 1, 1)];
var ignoreQueue = new ConcurrentQueue<IEntity>();
var results = new ConcurrentQueue<AdvancedSearchResult>();
var worldLocation = new WorldLocation(from.Location, from.Map);
for (var i = 0; i < _threadWorkers.Length; i++)
{
(_threadWorkers[i] ??= new AdvancedSearchThreadWorker()).Wake(worldLocation, Filter, results, ignoreQueue);
}
var type = Filter.FilterType ? Filter.Type : null;
// Push the entities. Workers read entity state concurrently with the main loop —
// see AdvancedSearchThreadWorker for the accepted, bounded race.
foreach (var item in World.Items.Values)
{
if (type == null || type.IsInstanceOfType(item))
{
PushToWorkers(item);
}
}
foreach (var m in World.Mobiles.Values)
{
if (type == null || type.IsInstanceOfType(m))
{
PushToWorkers(m);
}
}
ThreadPool.QueueUserWorkItem(state =>
{
try
{
// Block until everything is processed
for (var i = 0; i < _threadWorkers.Length; i++)
{
_threadWorkers[i].Sleep();
}
var ignoredEntities = new HashSet<IEntity>(ignoreQueue);
// Force the GC to collect the ignored entities
ignoreQueue.Clear();
var resultsList = new List<AdvancedSearchResult>(results.Count);
foreach (var result in results)
{
if (!ignoredEntities.Contains(result.Entity))
{
resultsList.Add(result);
}
}
SearchResults = resultsList.ToArray();
// Force the GC to collect the results
resultsList.Clear();
resultsList.TrimExcess();
// Send the gump on the main thread
Core.LoopContext.Post(() => Resend(from));
}
catch (Exception ex)
{
// A drain-phase throw here would terminate the process; the finally still
// restores autosave and releases the guard.
_logger.Warning(ex, "AdvancedSearch: search drain failed");
}
finally
{
AutoSave.SavesEnabled = (bool)state!;
EndSearch();
}
}, autoSave);
}
catch
{
// Setup failed before the work item took ownership of the release.
AutoSave.SavesEnabled = autoSave;
EndSearch();
throw;
}
}
private void SetSortSwitches(int radioSwitch)
{
SortByType = radioSwitch == 0;
SortByName = radioSwitch == 1;
SortByRange = radioSwitch == 2;
SortByMap = radioSwitch == 3;
SortBySelected = radioSwitch == 4;
}
public void Sort(Mobile from)
{
if (SearchResults == null || SearchResults.Length == 0)
{
return;
}
IComparer<AdvancedSearchResult> comparer;
if (SortByType)
{
comparer = SortDescending
? AdvancedSearchResultTypeComparer.InstanceReverse
: AdvancedSearchResultTypeComparer.Instance;
}
else if (SortByName)
{
comparer = SortDescending
? AdvancedSearchResultNameComparer.InstanceReverse
: AdvancedSearchResultNameComparer.Instance;
}
else if (SortByRange)
{
comparer = new AdvancedSearchRangeComparer(from, SortDescending);
}
else if (SortByMap)
{
comparer = SortDescending
? AdvancedSearchResultMapComparer.InstanceReverse
: AdvancedSearchResultMapComparer.Instance;
}
else if (SortBySelected)
{
comparer = SortDescending
? AdvancedSearchResultSelectedComparer.InstanceReverse
: AdvancedSearchResultSelectedComparer.Instance;
}
else
{
return;
}
Array.Sort(SearchResults, comparer);
}
private void ExecuteCommand(Mobile from, string commandString)
{
if (string.IsNullOrWhiteSpace(commandString))
{
return;
}
var list = new List<object>();
if (SearchResults?.Length > 0)
{
for (var i = 0; i < SearchResults.Length; i++)
{
var entry = SearchResults[i];
if (entry.Selected && entry.Entity != null)
{
list.Add(entry.Entity);
}
}
}
if (list.Count == 0)
{
return;
}
var commandSpan = commandString.AsSpan().Trim();
string command = null;
var commandArgs = new string[commandSpan.Count(" ")];
var index = 0;
foreach (var part in commandSpan.Tokenize(' '))
{
if (index == 0)
{
command = part.ToString();
}
else
{
commandArgs[index - 1] = part.ToString();
}
index++;
}
BaseCommand cmd = null;
foreach (var c in TargetCommands.AllCommands)
{
for (var i = 0; i < c.Commands.Length; i++)
{
if (command.InsensitiveEquals(c.Commands[i]))
{
cmd = c;
break;
}
}
}
if (cmd == null)
{
from.SendMessage($"Invalid command: {commandSpan}");
return;
}
var cmdEventArgs = new CommandEventArgs(from, command, commandString, commandArgs);
var flushToLog = false;
// execute the command on the objects in the list
if (list.Count > 20)
{
CommandLogging.Enabled = false;
}
cmd.ExecuteList(cmdEventArgs, list);
if (list.Count > 20)
{
flushToLog = true;
CommandLogging.Enabled = true;
}
cmd.Flush(from, flushToLog);
}
public void Resend(Mobile m)
{
Reset();
Build();
m.SendGump(this);
}
}