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:
parent
fe6e9dfe5c
commit
fbbc407954
10 changed files with 2129 additions and 0 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
962
Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs
Normal file
962
Projects/UOContent/Engines/Advanced Search/AdvancedSearchGump.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue