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:
Kamron Batman 2023-12-28 18:55:47 -08:00 committed by GitHub
parent fe6e9dfe5c
commit fbbc407954
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 2129 additions and 0 deletions

View file

@ -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<AdvancedSearchGump>();
from.SendGump(new AdvancedSearchGump(from));
}
}

View file

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

View file

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

View file

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

View file

@ -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<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
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<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();
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<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;
string[] 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;
}
CommandEventArgs cmdEventArgs = new CommandEventArgs(from, command, commandString, commandArgs);
bool 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)
{
TextEntries = 0;
Switches = 0;
Entries.Clear();
Strings.Clear();
Build(m);
m.SendGump(this);
}
}

View file

@ -0,0 +1,39 @@
using System;
namespace Server.Engines.AdvancedSearch;
public record AdvancedSearchResult(string Name, Type Type, Point3D Location, Map Map, IEntity Parent)
{
public IEntity Entity { get; set; }
public bool Selected { get; set; }
public Point3D GetLocation()
{
if (Parent?.Deleted == false)
{
return Parent.Location;
}
if (Entity?.Deleted == false)
{
return Entity.Location;
}
return Location;
}
public Map GetMap()
{
if (Parent?.Deleted == false)
{
return Parent.Map;
}
if (Entity?.Deleted == false)
{
return Entity.Map;
}
return Map;
}
}

View file

@ -0,0 +1,128 @@
using System.Collections.Generic;
namespace Server.Engines.AdvancedSearch;
public class AdvancedSearchResultTypeComparer : IComparer<AdvancedSearchResult>
{
public static readonly AdvancedSearchResultTypeComparer Instance = new();
public static readonly AdvancedSearchResultTypeComparer InstanceReverse = new(true);
private readonly bool _reverse;
public AdvancedSearchResultTypeComparer(bool reverse = false) => _reverse = reverse;
public int Compare(AdvancedSearchResult x, AdvancedSearchResult y)
{
var a = x?.Entity?.GetType().Name;
var b = y?.Entity?.GetType().Name;
return _reverse ? b.InsensitiveCompare(a) : a.InsensitiveCompare(b);
}
}
public class AdvancedSearchResultNameComparer : IComparer<AdvancedSearchResult>
{
public static readonly AdvancedSearchResultNameComparer Instance = new();
public static readonly AdvancedSearchResultNameComparer InstanceReverse = new(true);
private readonly bool _reverse;
public AdvancedSearchResultNameComparer(bool reverse = false) => _reverse = reverse;
public int Compare(AdvancedSearchResult x, AdvancedSearchResult y)
{
var a = x?.Name;
var b = y?.Name;
return _reverse ? b.InsensitiveCompare(a) : a.InsensitiveCompare(b);
}
}
public class AdvancedSearchResultMapComparer : IComparer<AdvancedSearchResult>
{
public static readonly AdvancedSearchResultMapComparer Instance = new();
public static readonly AdvancedSearchResultMapComparer InstanceReverse = new(true);
private readonly bool _reverse;
public AdvancedSearchResultMapComparer(bool reverse = false) => _reverse = reverse;
public int Compare(AdvancedSearchResult x, AdvancedSearchResult y)
{
var a = x?.Map?.MapID ?? -1;
var b = y?.Map?.MapID ?? -1;
return _reverse ? b.CompareTo(a) : a.CompareTo(b);
}
}
public class AdvancedSearchRangeComparer : IComparer<AdvancedSearchResult>
{
private readonly bool _reverse;
private readonly Mobile _from;
public AdvancedSearchRangeComparer(Mobile from, bool reverse = false)
{
_from = from;
_reverse = reverse;
}
public int Compare(AdvancedSearchResult x, AdvancedSearchResult y)
{
if (_from == null || x == null && y == null)
{
return 0;
}
if (x == null)
{
return _reverse ? 1 : -1;
}
if (y == null)
{
return _reverse ? -1 : 1;
}
var fromMap = _from.Map;
if (x.Map != fromMap && y.Map != fromMap)
{
return 0;
}
if (x.Map == fromMap && y.Map != fromMap)
{
return _reverse ? 1 : -1;
}
if (x.Map != fromMap && y.Map == fromMap)
{
return _reverse ? -1 : 1;
}
var xDist = _from.GetDistanceToSqrt(x.Location);
var yDist = _from.GetDistanceToSqrt(y.Location);
return _reverse ? yDist.CompareTo(xDist) : xDist.CompareTo(yDist);
}
}
public class AdvancedSearchResultSelectedComparer : IComparer<AdvancedSearchResult>
{
public static readonly AdvancedSearchResultSelectedComparer Instance = new();
public static readonly AdvancedSearchResultSelectedComparer InstanceReverse = new(true);
private readonly bool _reverse;
public AdvancedSearchResultSelectedComparer(bool reverse = false) => _reverse = reverse;
public int Compare(AdvancedSearchResult x, AdvancedSearchResult y)
{
var a = x?.Selected ?? false;
var b = y?.Selected ?? false;
// True then false, which is 1 then 0, so the comparison is reverse of integers
return _reverse ? a.CompareTo(b) : b.CompareTo(a);
}
}

View file

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

View file

@ -0,0 +1,337 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Server.Engines.AdvancedSearch;
public static class AdvancedSearchUtilities
{
private static readonly SearchValues<char> _operators = SearchValues.Create(['=', '!', '>', '<', '~']);
public static ReadOnlySpan<char> FindOperatorIndex(ReadOnlySpan<char> expression, out int index)
{
index = expression.IndexOfAny(_operators);
if (index == -1)
{
return ReadOnlySpan<char>.Empty;
}
// We are at the end
if (index + 1 == expression.Length)
{
return expression.Slice(index, 1);
}
// Look for double character
// <=, >=, ~<, ~>, ~~, ~=, ~!
var op = expression[index];
var next = expression[index + 1];
if (next is '=' && op is '=' or '<' or '>' or '~' or '!' || op is '~' && next is '<' or '>' or '~' or '!')
{
return expression.Slice(index, 2);
}
return expression.Slice(index, 1);
}
public static bool CompareValues(Type propertyType, object propertyValue, ReadOnlySpan<char> valuePart, ReadOnlySpan<char> operatorSpan)
{
// TODO: Add support for implicit conversion types like Serial -> uint
if (propertyType == typeof(long))
{
var parsedValue = ParseValue<long>(valuePart);
return CompareNumeric((long)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(ulong))
{
var parsedValue = ParseValue<ulong>(valuePart);
return CompareNumeric((ulong)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(int))
{
var parsedValue = ParseValue<int>(valuePart);
return CompareNumeric((int)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(uint))
{
var parsedValue = ParseValue<uint>(valuePart);
return CompareNumeric((uint)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(short))
{
var parsedValue = ParseValue<short>(valuePart);
return CompareNumeric((short)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(ushort))
{
var parsedValue = ParseValue<ushort>(valuePart);
return CompareNumeric((ushort)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(sbyte))
{
var parsedValue = ParseValue<sbyte>(valuePart);
return CompareNumeric((sbyte)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(byte))
{
var parsedValue = ParseValue<byte>(valuePart);
return CompareNumeric((byte)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(float))
{
var parsedValue = ParseValue<float>(valuePart);
return Compare((float)propertyValue!, parsedValue, valuePart, operatorSpan);
}
if (propertyType == typeof(double))
{
var parsedValue = ParseValue<double>(valuePart);
return Compare((double)propertyValue!, parsedValue, valuePart, operatorSpan);
}
if (propertyType == typeof(string))
{
var parsedValue = ParseValue<string>(valuePart);
return Compare((string)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(TimeSpan))
{
var parsedValue = ParseValue<TimeSpan>(valuePart);
return Compare((TimeSpan)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(DateTime))
{
var parsedValue = ParseValue<DateTime>(valuePart);
return Compare((DateTime)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType == typeof(bool))
{
var parsedValue = ParseValue<bool>(valuePart);
return Compare((bool)propertyValue!, parsedValue, operatorSpan);
}
if (propertyType.IsEnum)
{
var valueEnum = Enum.Parse(propertyType, valuePart, false);
return GetEnumSize(propertyType) switch
{
1 => CompareNumeric((byte)propertyValue!, (byte)valueEnum, operatorSpan),
2 => CompareNumeric((short)propertyValue!, (short)valueEnum, operatorSpan),
4 => CompareNumeric((int)propertyValue!, (int)valueEnum, operatorSpan),
8 => CompareNumeric((long)propertyValue!, (long)valueEnum, operatorSpan),
};
}
if (!propertyType.IsValueType)
{
var parsedValue = ParseValue<object>(valuePart);
return CompareReference(propertyValue!, parsedValue, operatorSpan);
}
return false;
}
public static bool CompareNumeric<T>(T propertyValue, T parsedValue, ReadOnlySpan<char> operatorSpan) where T : INumber<T> =>
operatorSpan switch
{
"=" or "==" => propertyValue == parsedValue,
"!" or "!=" => propertyValue != parsedValue,
">" => propertyValue > parsedValue,
"<" => propertyValue < parsedValue,
">=" => propertyValue >= parsedValue,
"<=" => propertyValue <= parsedValue,
_ => false
};
public static bool Compare(
double propertyValue,
double parsedValue,
ReadOnlySpan<char> originalValue,
ReadOnlySpan<char> operatorSpan
)
{
double epsilon = CalculateEpsilon(originalValue);
return operatorSpan switch
{
"=" or "==" => Math.Abs(propertyValue - parsedValue) < epsilon,
"!" or "!=" => Math.Abs(propertyValue - parsedValue) >= epsilon,
">" => propertyValue > parsedValue + epsilon,
"<" => propertyValue < parsedValue - epsilon,
">=" => propertyValue >= parsedValue - epsilon,
"<=" => propertyValue <= parsedValue + epsilon,
_ => throw new ArgumentException("Invalid operator")
};
}
public static double CalculateEpsilon(ReadOnlySpan<char> value)
{
int decimalPlace = value.IndexOf('.');
if (decimalPlace == -1)
{
// No decimal point, so use a default small epsilon
return 1E-10;
}
// Convert decimal places to a negative power of 10
return (value.Length - decimalPlace - 1) switch
{
< 10 => 1E-10,
10 => 1E-11,
11 => 1E-12,
12 => 1E-13,
13 => 1E-14,
14 => 1E-15,
_ => 1E-16
};
}
public static bool Compare(string propertyValue, string parsedValue, ReadOnlySpan<char> operatorSpan) =>
operatorSpan switch
{
"=" or "==" => propertyValue.EqualsOrdinal(parsedValue),
"!" or "!=" => !propertyValue.EqualsOrdinal(parsedValue),
">" => propertyValue.StartsWithOrdinal(parsedValue),
"<" => propertyValue.EndsWithOrdinal(parsedValue),
"~" => propertyValue.Contains(parsedValue),
"~<" => propertyValue.InsensitiveEndsWith(parsedValue),
"~>" => propertyValue.InsensitiveStartsWith(parsedValue),
"~~" => propertyValue.InsensitiveContains(parsedValue),
"~=" => propertyValue.InsensitiveEquals(parsedValue),
"~!" => !propertyValue.InsensitiveEquals(parsedValue),
_ => false
};
public static bool Compare(TimeSpan propertyValue, TimeSpan parsedValue, ReadOnlySpan<char> operatorSpan) =>
operatorSpan switch
{
"=" or "==" => propertyValue == parsedValue,
"!" or "!=" => propertyValue != parsedValue,
">" => propertyValue > parsedValue,
"<" => propertyValue < parsedValue,
">=" => propertyValue >= parsedValue,
"<=" => propertyValue <= parsedValue,
_ => false
};
public static bool Compare(DateTime propertyValue, DateTime parsedValue, ReadOnlySpan<char> operatorSpan) =>
operatorSpan switch
{
"=" or "==" => propertyValue == parsedValue,
"!" or "!=" => propertyValue != parsedValue,
">" => propertyValue > parsedValue,
"<" => propertyValue < parsedValue,
">=" => propertyValue >= parsedValue,
"<=" => propertyValue <= parsedValue,
_ => false
};
public static bool Compare(bool propertyValue, bool parsedValue, ReadOnlySpan<char> operatorSpan) =>
operatorSpan switch
{
"=" or "==" => propertyValue == parsedValue,
"!" or "!=" => propertyValue != parsedValue,
_ => false
};
public static bool CompareReference<T>(T propertyValue, T parsedValue, ReadOnlySpan<char> operatorSpan) =>
operatorSpan switch
{
"=" or "==" => propertyValue.Equals(parsedValue),
"!" or "!=" => !propertyValue.Equals(parsedValue),
">" => Comparer<T>.Default.Compare(propertyValue, parsedValue) > 0,
"<" => Comparer<T>.Default.Compare(propertyValue, parsedValue) < 0,
">=" => Comparer<T>.Default.Compare(propertyValue, parsedValue) >= 0,
"<=" => Comparer<T>.Default.Compare(propertyValue, parsedValue) <= 0,
_ => false
};
public static T ParseValue<T>(ReadOnlySpan<char> valuePart)
{
// Special handling for boolean and hexadecimal values
if (typeof(T) == typeof(bool))
{
string val = valuePart.ToString().ToLower();
if (val is "true" or "1" or "enabled" or "on")
{
return (T)(object)true;
}
if (val is "false" or "0" or "disabled" or "off")
{
return (T)(object)false;
}
}
if (typeof(T) == typeof(long))
{
return ParseNumericValue<long, T>(valuePart);
}
if (typeof(T) == typeof(ulong))
{
return ParseNumericValue<ulong, T>(valuePart);
}
if (typeof(T) == typeof(int))
{
return ParseNumericValue<int, T>(valuePart);
}
if (typeof(T) == typeof(uint))
{
return ParseNumericValue<uint, T>(valuePart);
}
if (typeof(T) == typeof(short))
{
return ParseNumericValue<short, T>(valuePart);
}
if (typeof(T) == typeof(ushort))
{
return ParseNumericValue<ushort, T>(valuePart);
}
if (typeof(T) == typeof(sbyte))
{
return ParseNumericValue<sbyte, T>(valuePart);
}
if (typeof(T) == typeof(byte))
{
return ParseNumericValue<byte, T>(valuePart);
}
if (typeof(T) == typeof(float))
{
return ParseNumericValue<float, T>(valuePart);
}
if (typeof(T) == typeof(double))
{
return ParseNumericValue<double, T>(valuePart);
}
// Default parsing for other types
return (T)Convert.ChangeType(valuePart.ToString(), typeof(T));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static R ParseNumericValue<T, R>(ReadOnlySpan<char> valuePart) where T : INumber<T> =>
valuePart.StartsWith("0x")
? (R)(object)T.Parse(valuePart[2..], NumberStyles.HexNumber, null)
: (R)(object)T.Parse(valuePart, null);
private static int GetEnumSize(Type enumType) =>
Type.GetTypeCode(Enum.GetUnderlyingType(enumType)) switch
{
TypeCode.Byte or TypeCode.SByte => sizeof(byte),
TypeCode.Int16 or TypeCode.UInt16 => sizeof(ushort),
TypeCode.Int32 or TypeCode.UInt32 => sizeof(uint),
TypeCode.Int64 or TypeCode.UInt64 => sizeof(ulong),
_ => 4
};
}

View file

@ -0,0 +1,57 @@
using Server.Gumps;
using Server.Network;
namespace Server.Engines.AdvancedSearch;
public abstract class AdvancedSearchWarningGump : Gump
{
public AdvancedSearchWarningGump(
string header, int headerColor, string content, int contentColor, int width, int height
) : base((640 - width) / 2, (480 - height) / 2)
{
Closable = false;
AddPage(0);
AddBackground(0, 0, width, height, 5054);
AddImageTiled(10, 10, width - 20, 20, 2624);
AddAlphaRegion(10, 10, width - 20, 20);
AddHtml(
10,
10,
width - 20,
20,
$"<BASEFONT COLOR=#{headerColor:X6}>{header}</BASEFONT>"
);
AddImageTiled(10, 40, width - 20, height - 80, 2624);
AddAlphaRegion(10, 40, width - 20, height - 80);
if (!string.IsNullOrWhiteSpace(content))
{
AddHtml(
10,
40,
width - 20,
height - 80,
$"<BASEFONT COLOR=#{contentColor:X6}>{content}</BASEFONT>",
false,
true
);
}
AddImageTiled(10, height - 30, width - 20, 20, 2624);
AddAlphaRegion(10, height - 30, width - 20, 20);
AddButton(10, height - 30, 4005, 4007, 1);
AddHtmlLocalized(40, height - 30, 170, 20, 1011036, 32767); // OKAY
AddButton(10 + (width - 20) / 2, height - 30, 4005, 4007, 0);
AddHtmlLocalized(40 + (width - 20) / 2, height - 30, 170, 20, 1011012, 32767); // CANCEL
}
public override void OnResponse(NetState sender, RelayInfo info) => OnClickResponse(sender, info.ButtonID == 1);
protected abstract void OnClickResponse(NetState sender, bool okay);
}