using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Server.Commands.Generic; using Server.Network; using static Server.Types; using static Server.Attributes; using static Server.Gumps.PropsConfig; namespace Server.Gumps { public class StackEntry { public object m_Object; public PropertyInfo m_Property; public StackEntry(object obj, PropertyInfo prop) { m_Object = obj; m_Property = prop; } } public class PropertiesGump : Gump { private static readonly bool PrevLabel = OldStyle; private static readonly bool NextLabel = OldStyle; private static readonly bool TypeLabel = !OldStyle; public static readonly int PrevLabelOffsetX = PrevWidth + 1; private static readonly int PrevLabelOffsetY = 0; private static readonly int NextLabelOffsetX = -29; private static readonly int NextLabelOffsetY = 0; private static readonly int NameWidth = 107; private static readonly int ValueWidth = 128; public static readonly int MaxEntriesPerPage = 15; private static readonly int TypeWidth = NameWidth + OffsetSize + ValueWidth; private static readonly int TotalWidth = OffsetSize + NameWidth + OffsetSize + ValueWidth + OffsetSize + SetWidth + OffsetSize; protected static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; protected readonly List m_List; protected readonly Mobile m_Mobile; protected readonly object m_Object; protected readonly Stack m_Stack; protected readonly Type m_Type; protected int m_Page; protected int m_EntryCount; protected virtual int TotalHeight => OffsetSize + (EntryHeight + OffsetSize) * (m_EntryCount + 1); public PropertiesGump(Mobile mobile, object o) : base(GumpOffsetX, GumpOffsetY) { m_Mobile = mobile; m_Object = o; m_Type = o.GetType(); m_List = BuildList(); Initialize(0); } public PropertiesGump(Mobile mobile, object o, Stack stack, StackEntry parent) : base( GumpOffsetX, GumpOffsetY ) { m_Mobile = mobile; m_Object = o; m_Type = o.GetType(); m_Stack = stack; m_List = BuildList(); if (parent != null) { m_Stack ??= new Stack(); m_Stack.Push(parent); } Initialize(0); } public PropertiesGump(Mobile mobile, object o, Stack stack, List list, int page) : base( GumpOffsetX, GumpOffsetY ) { m_Mobile = mobile; m_Object = o; if (o != null) { m_Type = o.GetType(); } m_List = list; m_Stack = stack; Initialize(page); } protected virtual void Initialize(int page) { m_Page = page; var indexOnPage = m_Page * MaxEntriesPerPage; m_EntryCount = Math.Clamp(m_List.Count - indexOnPage, 0, MaxEntriesPerPage); var lastIndex = indexOnPage + m_EntryCount - 1; if (lastIndex >= 0 && lastIndex < m_List.Count && m_List[lastIndex] == null) { --m_EntryCount; } var totalHeight = TotalHeight; AddPage(0); AddBackground(0, 0, BackWidth, BorderSize + totalHeight + BorderSize, BackGumpID); AddImageTiled( BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), OffsetSize + (EntryHeight + OffsetSize) * (m_EntryCount + 1), OffsetGumpID ); var x = BorderSize + OffsetSize; var y = BorderSize + OffsetSize; var emptyWidth = TotalWidth - PrevWidth - NextWidth - OffsetSize * 4 - (OldStyle ? SetWidth + OffsetSize : 0); if (OldStyle) { AddImageTiled(x, y, TotalWidth - OffsetSize * 3 - SetWidth, EntryHeight, HeaderGumpID); } else { AddImageTiled(x, y, PrevWidth, EntryHeight, HeaderGumpID); } if (page > 0) { AddButton(x + PrevOffsetX, y + PrevOffsetY, PrevButtonID1, PrevButtonID2, 1); if (PrevLabel) { AddLabel(x + PrevLabelOffsetX, y + PrevLabelOffsetY, TextHue, "Previous"); } } x += PrevWidth + OffsetSize; if (!OldStyle) { AddImageTiled(x, y, emptyWidth, EntryHeight, HeaderGumpID); } if (TypeLabel && m_Type != null) { AddHtml( x, y, emptyWidth, EntryHeight, m_Type.Name.Center(0xFAFAFA) ); } x += emptyWidth + OffsetSize; if (!OldStyle) { AddImageTiled(x, y, NextWidth, EntryHeight, HeaderGumpID); } if ((page + 1) * MaxEntriesPerPage < m_List.Count) { AddButton(x + NextOffsetX, y + NextOffsetY, NextButtonID1, NextButtonID2, 2, GumpButtonType.Reply, 1); if (NextLabel) { AddLabel(x + NextLabelOffsetX, y + NextLabelOffsetY, TextHue, "Next"); } } for (int i = 0, index = page * MaxEntriesPerPage; i < m_EntryCount && index < m_List.Count; ++i, ++index) { x = BorderSize + OffsetSize; y += EntryHeight + OffsetSize; var o = m_List[index]; if (o == null) { AddImageTiled(x - OffsetSize, y, TotalWidth, EntryHeight, BackGumpID + 4); } else if (o is Type type) { AddImageTiled(x, y, TypeWidth, EntryHeight, EntryGumpID); AddLabelCropped(x + TextOffsetX, y, TypeWidth - TextOffsetX, EntryHeight, TextHue, type.Name); x += TypeWidth + OffsetSize; if (SetGumpID != 0) { AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); } } else if (o is PropertyInfo prop) { AddImageTiled(x, y, NameWidth, EntryHeight, EntryGumpID); AddLabelCropped(x + TextOffsetX, y, NameWidth - TextOffsetX, EntryHeight, TextHue, prop.Name); x += NameWidth + OffsetSize; AddImageTiled(x, y, ValueWidth, EntryHeight, EntryGumpID); AddLabelCropped(x + TextOffsetX, y, ValueWidth - TextOffsetX, EntryHeight, TextHue, ValueToString(prop)); x += ValueWidth + OffsetSize; if (SetGumpID != 0) { AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); } var cpa = GetCPA(prop); if (cpa?.ReadOnly == false && m_Mobile.AccessLevel >= cpa.WriteLevel && (prop.CanWrite || cpa.CanModify)) { AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, i + 3); } } } } public override void OnResponse(NetState state, in RelayInfo info) { var from = state.Mobile; if (!BaseCommand.IsAccessible(from, m_Object)) { from.SendMessage("You may no longer access their properties."); return; } switch (info.ButtonID) { case 0: // Closed { if (m_Stack?.Count > 0) { var entry = m_Stack.Pop(); from.SendGump(new PropertiesGump(from, entry.m_Object, m_Stack, null)); } break; } case 1: // Previous { if (m_Page > 0) { from.SendGump(new PropertiesGump(from, m_Object, m_Stack, m_List, m_Page - 1)); } break; } case 2: // Next { if ((m_Page + 1) * MaxEntriesPerPage < m_List.Count) { from.SendGump(new PropertiesGump(from, m_Object, m_Stack, m_List, m_Page + 1)); } break; } default: { var index = m_Page * MaxEntriesPerPage + (info.ButtonID - 3); if (index < 0 || index >= m_List.Count) { break; } var prop = m_List[index] as PropertyInfo; if (prop == null) { return; } var cpa = GetCPA(prop); if (cpa == null || !(prop.CanWrite || cpa.CanModify) || cpa.ReadOnly || from.AccessLevel < cpa.WriteLevel) { return; } var type = prop.PropertyType; if (IsType(type, OfEntity)) { from.SendGump(new SetObjectGump(prop, from, m_Object, type, this)); } else if (IsType(type, OfType)) { from.Target = new SetObjectTarget(prop, from, m_Object, type, this); } else if (IsType(type, OfPoint3D)) { from.SendGump(new SetPoint3DGump(prop, from, m_Object, this)); } else if (IsType(type, OfPoint2D)) { from.SendGump(new SetPoint2DGump(prop, from, m_Object, this)); } else if (IsType(type, OfTimeSpan)) { from.SendGump(new SetTimeSpanGump(prop, from, m_Object, this)); } else if (IsCustomEnum(type)) { from.SendGump( new SetCustomEnumGump( prop, from, m_Object, this, GetCustomEnumNames(type) ) ); } else if (IsType(type, OfEnum)) { from.SendGump( new SetListOptionGump( prop, from, m_Object, this, Enum.GetNames(type), GetObjects(Enum.GetValues(type)) ) ); } else if (IsType(type, OfBool)) { from.SendGump( new SetListOptionGump( prop, from, m_Object, this, BoolNames, BoolValues ) ); } else if (IsType(type, OfPoison)) { from.SendGump( new SetListOptionGump( prop, from, m_Object, this, PoisonNames, PoisonValues ) ); } else if (IsType(type, OfMap)) { from.SendGump( new SetListOptionGump( prop, from, m_Object, this, Map.GetMapNames(), Map.GetMapValues().ToArray() ) ); } else if (IsType(type, OfSkills) && m_Object is Mobile mobile) { from.SendGump(new PropertiesGump(from, mobile, m_Stack, m_List, m_Page)); from.SendGump(new SkillsGump(from, mobile)); } else if (HasAttribute(type, OfPropertyObject, true)) { var obj = prop.GetValue(m_Object, null); from.SendGump( obj != null ? new PropertiesGump(from, obj, m_Stack, new StackEntry(m_Object, prop)) : new PropertiesGump(from, m_Object, m_Stack, m_List, m_Page) ); } else if (IsType(type, OfString) || IsParsable(type)) { from.SendGump(new SetGump(prop, from, m_Object, this)); } break; } } } public virtual void SendPropertiesGump() => m_Mobile.SendGump(new PropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); private static object[] GetObjects(Array a) { var list = new object[a.Length]; for (var i = 0; i < list.Length; ++i) { list[i] = a.GetValue(i); } return list; } private static bool IsCustomEnum(Type type) => type.IsDefined(OfCustomEnum, false); public void OnValueChanged(object obj, PropertyInfo prop) { if (m_Stack == null || m_Stack.Count == 0) { return; } if (!prop.PropertyType.IsValueType) { return; } var peek = m_Stack.Peek(); if (peek.m_Property.CanWrite) { peek.m_Property.SetValue(peek.m_Object, obj, null); } } private static string[] GetCustomEnumNames(Type type) { var attrs = type.GetCustomAttributes(OfCustomEnum, false); if (attrs.Length == 0) { return Array.Empty(); } if (attrs[0] is not CustomEnumAttribute ce) { return Array.Empty(); } return ce.Names; } private static bool HasAttribute(Type type, Type check, bool inherit) => type.GetCustomAttributes(check, inherit).Length > 0; private string ValueToString(PropertyInfo prop) => ValueToString(m_Object, prop); public static string ValueToString(object obj, PropertyInfo prop) { try { return ValueToString(prop.GetValue(obj, null)); } catch (Exception e) { return $"!{e.GetType()}!"; } } private static string ValueToString(object o) { if (o == null) { return "-null-"; } if (o is string s) { return $"\"{s}\""; } if (o is bool) { return o.ToString(); } if (o is char c) { return $"0x{(int)c:X} '{c}'"; } if (o is Serial serial) { if (serial.IsValid) { if (serial.IsItem) { return $"(I) {serial}"; } if (serial.IsMobile) { return $"(M) {serial}"; } } return $"(?) {serial}"; } if (o is byte or sbyte or short or ushort or int or uint or long or ulong) { return $"{o} (0x{o:X})"; } if (o is Mobile mobile) { return $"(M) {mobile.Serial} \"{mobile.Name}\""; } if (o is Item item) { return $"(I) {item.Serial}"; } if (o is Type type) { return type.Name; } if (o is TextDefinition definition) { return definition.Format() ?? "-empty-"; } return o.ToString(); } private List BuildList() { var list = new List(); if (m_Type == null) { return list; } var props = m_Type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); var groups = GetGroups(m_Type, props); for (var i = 0; i < groups.Count; ++i) { var kvp = groups[i]; if (!HasAttribute(kvp.Key, OfNoSort, false)) { kvp.Value.Sort(PropertySorter.Instance); } if (i != 0) { list.Add(null); } list.Add(kvp.Key); foreach (var item in kvp.Value) { if (ShowAttribute(item.Name)) { list.Add(item); } } } return list; } protected virtual bool ShowAttribute(string name) => true; private List>> GetGroups(Type objectType, PropertyInfo[] props) { var groups = new Dictionary>(); for (var i = 0; i < props.Length; ++i) { var prop = props[i]; if (prop.CanRead) { var attr = GetCPA(prop); if (attr != null && m_Mobile.AccessLevel >= attr.ReadLevel) { var type = prop.DeclaringType; while (true) { var baseType = type?.BaseType; if (baseType == OfObject || baseType?.GetProperty(prop.Name, prop.PropertyType) == null) { break; } type = baseType; } if (type != null) { if (groups.TryGetValue(type, out var result)) { result.Add(prop); } else { groups[type] = new List { prop }; } } } } } var list = groups.ToList(); list.Sort(new GroupComparer(objectType)); return list; } private class PropertySorter : IComparer { public static readonly PropertySorter Instance = new(); private PropertySorter() { } public int Compare(PropertyInfo x, PropertyInfo y) { if (x == null && y == null) { return 0; } if (x == null) { return -1; } return x.Name.CompareOrdinal(y?.Name); } } private class GroupComparer : IComparer>> { private readonly Type m_Start; public GroupComparer(Type start) => m_Start = start; public int Compare(KeyValuePair> x, KeyValuePair> y) => GetDistance(x.Key).CompareTo(GetDistance(y.Key)); private int GetDistance(Type type) { var current = m_Start; int dist; for (dist = 0; current != null && current != OfObject && current != type; ++dist) { current = current.BaseType; } return dist; } } } }