From fbbc4079544972e72687bc1af23f21925c779b58 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 28 Dec 2023 18:55:47 -0800
Subject: [PATCH] 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
---
.../Advanced Search/AdvancedSearchCommand.cs | 22 +
.../AdvancedSearchConfirmBringGump.cs | 39 +
.../AdvancedSearchConfirmDeleteGump.cs | 35 +
.../Advanced Search/AdvancedSearchFilter.cs | 147 +++
.../Advanced Search/AdvancedSearchGump.cs | 962 ++++++++++++++++++
.../Advanced Search/AdvancedSearchResult.cs | 39 +
.../AdvancedSearchResultComparers.cs | 128 +++
.../AdvancedSearchThreadWorker.cs | 363 +++++++
.../AdvancedSearchUtilities.cs | 337 ++++++
.../AdvancedSearchWarningGump.cs | 57 ++
10 files changed, 2129 insertions(+)
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmBringGump.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmDeleteGump.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchFilter.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchResult.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchResultComparers.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs
create mode 100644 Projects/UOContent/Engines/Advanced Search/AdvancedSearchWarningGump.cs
diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs
new file mode 100644
index 000000000..c752a1511
--- /dev/null
+++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchCommand.cs
@@ -0,0 +1,22 @@
+namespace Server.Engines.AdvancedSearch;
+
+public static class AdvancedSearchCommand
+{
+ public static void Initialize()
+ {
+ CommandSystem.Register("XmlFind", AccessLevel.Player, OnCommand); // For those old school peeps!
+ CommandSystem.Register("AdvancedSearch", AccessLevel.Player, OnCommand);
+ CommandSystem.Register("AdvSrch", AccessLevel.Player, OnCommand);
+ CommandSystem.Register("AS", AccessLevel.Player, OnCommand);
+ }
+
+ [Usage("AdvancedSearch")]
+ [Description("Opens the advanced search gump.")]
+ private static void OnCommand(CommandEventArgs e)
+ {
+ var from = e.Mobile;
+
+ from.CloseGump();
+ from.SendGump(new AdvancedSearchGump(from));
+ }
+}
diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmBringGump.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmBringGump.cs
new file mode 100644
index 000000000..bcb49feb9
--- /dev/null
+++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmBringGump.cs
@@ -0,0 +1,39 @@
+using Server.Network;
+
+namespace Server.Engines.AdvancedSearch;
+
+public class AdvancedSearchConfirmBringGump : AdvancedSearchWarningGump
+{
+ private readonly AdvancedSearchGump _gump;
+
+ public AdvancedSearchConfirmBringGump(AdvancedSearchGump gump, int count) : base(
+ "Bring selected objects",
+ 30720,
+ $"Bring {count} objects?",
+ 0xFFC000,
+ 480,
+ 360
+ ) => _gump = gump;
+
+ protected override void OnClickResponse(NetState sender, bool okay)
+ {
+ var from = sender.Mobile;
+ var loc = from.Location;
+ var map = from.Map;
+
+ if (okay && _gump.SearchResults != null)
+ {
+ for (var i = 0; i < _gump.SearchResults.Length; i++)
+ {
+ var entry = _gump.SearchResults[i];
+
+ if (entry.Selected)
+ {
+ entry.Entity?.MoveToWorld(loc, map);
+ }
+ }
+ }
+
+ _gump.Resend(from);
+ }
+}
diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmDeleteGump.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmDeleteGump.cs
new file mode 100644
index 000000000..083498eef
--- /dev/null
+++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchConfirmDeleteGump.cs
@@ -0,0 +1,35 @@
+using Server.Network;
+
+namespace Server.Engines.AdvancedSearch;
+
+public class AdvancedSearchConfirmDeleteGump : AdvancedSearchWarningGump
+{
+ private readonly AdvancedSearchGump _gump;
+
+ public AdvancedSearchConfirmDeleteGump(AdvancedSearchGump gump, int count) : base(
+ "Delete selected objects",
+ 30720,
+ $"Delete {count} objects?",
+ 0xFFC000,
+ 480,
+ 360
+ ) => _gump = gump;
+
+ protected override void OnClickResponse(NetState sender, bool okay)
+ {
+ if (okay && _gump.SearchResults != null)
+ {
+ for (var i = 0; i < _gump.SearchResults.Length; i++)
+ {
+ var entry = _gump.SearchResults[i];
+
+ if (entry.Selected)
+ {
+ entry.Entity?.Delete();
+ }
+ }
+ }
+
+ _gump.Resend(sender.Mobile);
+ }
+}
diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchFilter.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchFilter.cs
new file mode 100644
index 000000000..61dd44eec
--- /dev/null
+++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchFilter.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Server.Engines.AdvancedSearch;
+
+[Flags]
+public enum AdvancedSearchFilterOptions : long
+{
+ None,
+ FilterType = 0x00000001,
+ FilterName = 0x00000002,
+ FilterRange = 0x00000004,
+ FilterRegion = 0x00000008,
+ FilterPropertyTest = 0x00000040,
+ FilterInternalMap = 0x00000080,
+ FilterNullMap = 0x00000100,
+ FilterAge = 0x00000200,
+ FilterAgeDirection = 0x00000400,
+ HideValidInternalMap = 0x00000800,
+ FilterFelucca = 0x00001000,
+ FilterTrammel = 0x00002000,
+ FilterIlshenar = 0x00004000,
+ FilterMalas = 0x00008000,
+ FilterTokuno = 0x00010000,
+ FilterTerMur = 0x00020000,
+}
+
+public record AdvancedSearchFilter
+{
+ private AdvancedSearchFilterOptions _options;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool GetOptionsFlag(AdvancedSearchFilterOptions option) => (_options & option) != 0;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SetOptionsFlag(AdvancedSearchFilterOptions option, bool value) =>
+ _options = value ? _options | option : _options & ~option;
+
+ public bool FilterType
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterType);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterType, value);
+ }
+
+ public bool FilterName
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterName);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterName, value);
+ }
+
+ public bool FilterRange
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterRange);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterRange, value);
+ }
+
+ public bool FilterRegion
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterRegion);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterRegion, value);
+ }
+
+ public bool FilterPropertyTest
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterPropertyTest);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterPropertyTest, value);
+ }
+
+ public bool FilterInternalMap
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterInternalMap);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterInternalMap, value);
+ }
+
+ public bool FilterNullMap
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterNullMap);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterNullMap, value);
+ }
+
+ public bool FilterAge
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterAge);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterAge, value);
+ }
+
+ public bool FilterAgeDirection
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterAgeDirection);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterAgeDirection, value);
+ }
+
+ public bool HideValidInternalMap
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.HideValidInternalMap);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.HideValidInternalMap, value);
+ }
+
+ public bool FilterFelucca
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterFelucca);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterFelucca, value);
+ }
+
+ public bool FilterTrammel
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterTrammel);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterTrammel, value);
+ }
+
+ public bool FilterIlshenar
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterIlshenar);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterIlshenar, value);
+ }
+
+ public bool FilterMalas
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterMalas);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterMalas, value);
+ }
+
+ public bool FilterTokuno
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterTokuno);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterTokuno, value);
+ }
+
+ public bool FilterTerMur
+ {
+ get => GetOptionsFlag(AdvancedSearchFilterOptions.FilterTerMur);
+ set => SetOptionsFlag(AdvancedSearchFilterOptions.FilterTerMur, value);
+ }
+
+ // Must be older or younger than this Age based on FilterAgeDirection
+ public TimeSpan? Age { get; set; }
+
+ public int? Range { get; set; }
+
+ public string? RegionName { get; set; }
+
+ public string? PropertyTest { get; set; }
+
+ public Type? Type { get; set; }
+
+ public string? Name { get; set; }
+}
diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs
new file mode 100644
index 000000000..80fe12751
--- /dev/null
+++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs
@@ -0,0 +1,962 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using CommunityToolkit.HighPerformance;
+using Server.Commands;
+using Server.Commands.Generic;
+using Server.Engines.Spawners;
+using Server.Gumps;
+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 const int MaxEntries = 18;
+
+ private static int _threadId;
+ private static AdvancedSearchThreadWorker[] _threadWorkers;
+
+ 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(Mobile from) : base(50, 50) => Build(from);
+
+ private void Build(Mobile from)
+ {
+ 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(150, y - 25, 0xFA8, 0xFAA, 159);
+ // AddLabel(183, y - 25, 0x384, "Save to file:");
+ //
+ // AddImageTiled(270, y - 25, 180, 19, 0xBBC);
+ // AddTextEntry(270, y - 25, 180, 19, 0, 300, SaveFilename);
+
+ 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
+ int count = 0;
+ foreach (AdvancedSearchResult 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;
+ for (int i = 0; i < MaxEntries; i++)
+ {
+ var index = i + DisplayFrom;
+ if (index >= SearchResults.Length)
+ {
+ break;
+ }
+
+ 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 + i);
+ }
+
+ // Goto button
+ AddButton(205, 22 * i + 30, 0xFAE, 0xFAF, 1000 + i);
+
+ // Interface button
+ AddButton(175, 22 * i + 30, 0xFAB, 0xFAD, 3000 + i);
+
+ 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 + i);
+ }
+
+ AddButton(730, 5, allDisplayedSelected ? 0xD3 : 0xD2, allDisplayedSelected ? 0xD2 : 0xD3, 2); // Select all displayed
+ }
+ }
+
+ public override void OnResponse(NetState state, 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)?.Text;
+ Filter.Range = rangeText != null ? Utility.ToInt32(rangeText) : null;
+
+ var filterText = info.GetTextEntry(101)?.Text;
+ Filter.Type = filterText != null ? AssemblyHandler.FindTypeByName(filterText) : null;
+
+ Filter.Name = info.GetTextEntry(102)?.Text;
+ Filter.PropertyTest = info.GetTextEntry(104)?.Text;
+
+ var ageText = info.GetTextEntry(105)?.Text;
+ Filter.Age = ageText != null ? Utility.ToTimeSpan(ageText) : null;
+
+ Filter.RegionName = info.GetTextEntry(106)?.Text;
+
+ CommandString = info.GetTextEntry(301)?.Text;
+
+ 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);
+ break;
+ }
+ 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 as 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 or > 4)
+ {
+ Array.Reverse(SearchResults);
+ }
+ else
+ {
+ 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;
+ }
+
+ var autoSave = AutoSave.SavesEnabled;
+ if (autoSave)
+ {
+ AutoSave.SavesEnabled = false;
+ }
+
+ _threadWorkers ??= new AdvancedSearchThreadWorker[Math.Max(Environment.ProcessorCount - 1, 1)];
+
+ var ignoreQueue = new ConcurrentQueue();
+ var results = new ConcurrentQueue();
+ 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
+ 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);
+ }
+ }
+
+ // Block until everything is processed
+ for (var i = 0; i < _threadWorkers.Length; i++)
+ {
+ _threadWorkers[i].Sleep();
+ }
+
+ var ignoredEntities = new HashSet(ignoreQueue);
+
+ // Force the GC to collect the ignored entities
+ ignoreQueue.Clear();
+
+ var resultsList = new List(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();
+
+ AutoSave.SavesEnabled = autoSave;
+ }
+
+ 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 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