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, in RelayInfo info) { var from = state.Mobile; if (from == null) { return; } SetSortSwitches(info.Switches.Length > 0 ? info.Switches[0] : -1); Filter ??= new AdvancedSearchFilter(); Filter.FilterAgeDirection = info.IsSwitched(302); Filter.FilterAge = info.IsSwitched(303); Filter.FilterRange = info.IsSwitched(304); Filter.FilterType = info.IsSwitched(305); Filter.FilterName = info.IsSwitched(306); Filter.FilterFelucca = info.IsSwitched(308); Filter.FilterTrammel = info.IsSwitched(309); Filter.FilterMalas = info.IsSwitched(310); Filter.FilterIlshenar = info.IsSwitched(311); Filter.FilterInternalMap = info.IsSwitched(312); Filter.FilterNullMap = info.IsSwitched(314); Filter.FilterPropertyTest = info.IsSwitched(315); Filter.HideValidInternalMap = info.IsSwitched(316); Filter.FilterTokuno = info.IsSwitched(318); Filter.FilterRegion = info.IsSwitched(319); Filter.FilterTerMur = info.IsSwitched(320); var rangeText = info.GetTextEntry(100); Filter.Range = rangeText != null ? Utility.ToInt32(rangeText) : null; var filterText = info.GetTextEntry(101); Filter.Type = filterText != null ? AssemblyHandler.FindTypeByName(filterText) : null; Filter.Name = info.GetTextEntry(102); Filter.PropertyTest = info.GetTextEntry(104); var ageText = info.GetTextEntry(105); Filter.Age = ageText != null ? Utility.ToTimeSpan(ageText) : null; Filter.RegionName = info.GetTextEntry(106); CommandString = info.GetTextEntry(301); var buttonId = info.ButtonID; switch (buttonId) { case 0: // Close { return; } case 1: // Select all toggle { AllSelected = !AllSelected; if (SearchResults?.Length > 0) { for (var i = 0; i < SearchResults.Length; i++) { SearchResults[i].Selected = AllSelected; } } break; } case 2: // Select all displayed { if (SearchResults?.Length > 0) { var allSelected = true; var max = DisplayFrom + MaxEntries; for (var i = DisplayFrom; i < max; i++) { if (i >= SearchResults.Length) { break; } var entry = SearchResults[i]; if (!entry.Selected) { allSelected = false; break; } } for (var i = DisplayFrom; i < max; i++) { if (i >= SearchResults.Length) { break; } var entry = SearchResults[i]; entry.Selected = !allSelected; } } break; } case 3: // Search { DoSearch(from); 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) { Reset(); Build(m); m.SendGump(this); } }