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(); + + 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); + } +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchResult.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchResult.cs new file mode 100644 index 000000000..5c8cf1969 --- /dev/null +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchResult.cs @@ -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; + } +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchResultComparers.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchResultComparers.cs new file mode 100644 index 000000000..8842b8f80 --- /dev/null +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchResultComparers.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; + +namespace Server.Engines.AdvancedSearch; + +public class AdvancedSearchResultTypeComparer : IComparer +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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); + } +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs new file mode 100644 index 000000000..fd042d9ea --- /dev/null +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs @@ -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 _entities; + private ConcurrentQueue _results; + private ConcurrentQueue _ignoreQueue; + private WorldLocation _worldLocation; + private AdvancedSearchFilter _filter; + + public AdvancedSearchThreadWorker() + { + _startEvent = new AutoResetEvent(false); + _stopEvent = new AutoResetEvent(false); + _entities = new ConcurrentQueue(); + _thread = new Thread(Execute); + _thread.Start(this); + } + + public void Wake( + WorldLocation worldLocation, + AdvancedSearchFilter filter, + ConcurrentQueue results, + ConcurrentQueue ignoreQueue + ) + { + _worldLocation = worldLocation; + _filter = filter; + _ignoreQueue = ignoreQueue; + _results = results; + _startEvent.Set(); + } + + public void Sleep() + { + Volatile.Write(ref _pause, true); + _stopEvent.WaitOne(); + } + + public void Exit() + { + _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 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 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; + } +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs new file mode 100644 index 000000000..68813cba6 --- /dev/null +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs @@ -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 _operators = SearchValues.Create(['=', '!', '>', '<', '~']); + + public static ReadOnlySpan FindOperatorIndex(ReadOnlySpan expression, out int index) + { + index = expression.IndexOfAny(_operators); + if (index == -1) + { + return ReadOnlySpan.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 valuePart, ReadOnlySpan operatorSpan) + { + // TODO: Add support for implicit conversion types like Serial -> uint + + if (propertyType == typeof(long)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((long)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(ulong)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((ulong)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(int)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((int)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(uint)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((uint)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(short)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((short)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(ushort)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((ushort)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(sbyte)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((sbyte)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(byte)) + { + var parsedValue = ParseValue(valuePart); + return CompareNumeric((byte)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(float)) + { + var parsedValue = ParseValue(valuePart); + return Compare((float)propertyValue!, parsedValue, valuePart, operatorSpan); + } + if (propertyType == typeof(double)) + { + var parsedValue = ParseValue(valuePart); + return Compare((double)propertyValue!, parsedValue, valuePart, operatorSpan); + } + if (propertyType == typeof(string)) + { + var parsedValue = ParseValue(valuePart); + return Compare((string)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(TimeSpan)) + { + var parsedValue = ParseValue(valuePart); + return Compare((TimeSpan)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(DateTime)) + { + var parsedValue = ParseValue(valuePart); + return Compare((DateTime)propertyValue!, parsedValue, operatorSpan); + } + if (propertyType == typeof(bool)) + { + var parsedValue = ParseValue(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(valuePart); + return CompareReference(propertyValue!, parsedValue, operatorSpan); + } + + return false; + } + + public static bool CompareNumeric(T propertyValue, T parsedValue, ReadOnlySpan operatorSpan) where T : INumber => + 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 originalValue, + ReadOnlySpan 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 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 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 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 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 operatorSpan) => + operatorSpan switch + { + "=" or "==" => propertyValue == parsedValue, + "!" or "!=" => propertyValue != parsedValue, + _ => false + }; + + public static bool CompareReference(T propertyValue, T parsedValue, ReadOnlySpan operatorSpan) => + operatorSpan switch + { + "=" or "==" => propertyValue.Equals(parsedValue), + "!" or "!=" => !propertyValue.Equals(parsedValue), + ">" => Comparer.Default.Compare(propertyValue, parsedValue) > 0, + "<" => Comparer.Default.Compare(propertyValue, parsedValue) < 0, + ">=" => Comparer.Default.Compare(propertyValue, parsedValue) >= 0, + "<=" => Comparer.Default.Compare(propertyValue, parsedValue) <= 0, + _ => false + }; + + public static T ParseValue(ReadOnlySpan 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(valuePart); + } + + if (typeof(T) == typeof(ulong)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(int)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(uint)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(short)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(ushort)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(sbyte)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(byte)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(float)) + { + return ParseNumericValue(valuePart); + } + + if (typeof(T) == typeof(double)) + { + return ParseNumericValue(valuePart); + } + + // Default parsing for other types + return (T)Convert.ChangeType(valuePart.ToString(), typeof(T)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static R ParseNumericValue(ReadOnlySpan valuePart) where T : INumber => + 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 + }; +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchWarningGump.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchWarningGump.cs new file mode 100644 index 000000000..f80c1a7d0 --- /dev/null +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchWarningGump.cs @@ -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, + $"{header}" + ); + + 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, + $"{content}", + 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); +}